Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add audit logging for bulk role APIs #110410

Merged
merged 9 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class BulkPutRoleRequestBuilder extends ActionRequestBuilder<BulkPutRoles
}

public BulkPutRoleRequestBuilder(ElasticsearchClient client) {
super(client, ActionTypes.BULK_PUT_ROLES, new BulkPutRolesRequest());
super(client, ActionTypes.BULK_PUT_ROLES, new BulkPutRolesRequest(List.of()));
}

public BulkPutRoleRequestBuilder content(BytesReference content, XContentType xContentType) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ public class BulkPutRolesRequest extends ActionRequest {

private List<RoleDescriptor> roles;

public BulkPutRolesRequest() {}
public BulkPutRolesRequest(List<RoleDescriptor> roles) {
this.roles = roles;
}

public void setRoles(List<RoleDescriptor> roles) {
this.roles = roles;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.elasticsearch.xcontent.json.JsonStringEncoder;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.ActionTypes;
import org.elasticsearch.xpack.core.security.action.Grant;
import org.elasticsearch.xpack.core.security.action.apikey.AbstractCreateApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.apikey.BaseSingleUpdateApiKeyRequest;
Expand Down Expand Up @@ -72,6 +73,8 @@
import org.elasticsearch.xpack.core.security.action.profile.SetProfileEnabledRequest;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataAction;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataRequest;
import org.elasticsearch.xpack.core.security.action.role.BulkDeleteRolesRequest;
import org.elasticsearch.xpack.core.security.action.role.BulkPutRolesRequest;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleRequest;
import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
Expand Down Expand Up @@ -291,6 +294,8 @@ public class LoggingAuditTrail implements AuditTrail, ClusterStateListener {
PutUserAction.NAME,
PutRoleAction.NAME,
PutRoleMappingAction.NAME,
ActionTypes.BULK_PUT_ROLES.name(),
ActionTypes.BULK_DELETE_ROLES.name(),
TransportSetEnabledAction.TYPE.name(),
TransportChangePasswordAction.TYPE.name(),
CreateApiKeyAction.NAME,
Expand Down Expand Up @@ -731,6 +736,11 @@ public void accessGranted(
} else if (msg instanceof PutRoleRequest) {
assert PutRoleAction.NAME.equals(action);
securityChangeLogEntryBuilder(requestId).withRequestBody((PutRoleRequest) msg).build();
} else if (msg instanceof BulkPutRolesRequest bulkPutRolesRequest) {
assert ActionTypes.BULK_PUT_ROLES.name().equals(action);
for (RoleDescriptor roleDescriptor : bulkPutRolesRequest.getRoles()) {
securityChangeLogEntryBuilder(requestId).withRequestBody(roleDescriptor.getName(), roleDescriptor).build();
}
} else if (msg instanceof PutRoleMappingRequest) {
assert PutRoleMappingAction.NAME.equals(action);
securityChangeLogEntryBuilder(requestId).withRequestBody((PutRoleMappingRequest) msg).build();
Expand All @@ -755,6 +765,11 @@ public void accessGranted(
} else if (msg instanceof DeleteRoleRequest) {
assert DeleteRoleAction.NAME.equals(action);
securityChangeLogEntryBuilder(requestId).withRequestBody((DeleteRoleRequest) msg).build();
} else if (msg instanceof BulkDeleteRolesRequest bulkDeleteRolesRequest) {
assert ActionTypes.BULK_DELETE_ROLES.name().equals(action);
for (String roleName : bulkDeleteRolesRequest.getRoleNames()) {
securityChangeLogEntryBuilder(requestId).withDeleteRole(roleName).build();
}
} else if (msg instanceof DeleteRoleMappingRequest) {
assert DeleteRoleMappingAction.NAME.equals(action);
securityChangeLogEntryBuilder(requestId).withRequestBody((DeleteRoleMappingRequest) msg).build();
Expand Down Expand Up @@ -1160,15 +1175,19 @@ LogEntryBuilder withRequestBody(ChangePasswordRequest changePasswordRequest) thr
}

LogEntryBuilder withRequestBody(PutRoleRequest putRoleRequest) throws IOException {
return withRequestBody(putRoleRequest.name(), putRoleRequest.roleDescriptor());
}

LogEntryBuilder withRequestBody(String roleName, RoleDescriptor roleDescriptor) throws IOException {
logEntry.with(EVENT_ACTION_FIELD_NAME, "put_role");
XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
builder.startObject()
.startObject("role")
.field("name", putRoleRequest.name())
.field("name", roleName)
// the "role_descriptor" nested structure, where the "name" is left out, is closer to the event structure
// for creating API Keys
.field("role_descriptor");
withRoleDescriptor(builder, putRoleRequest.roleDescriptor());
withRoleDescriptor(builder, roleDescriptor);
builder.endObject() // role
.endObject();
logEntry.with(PUT_CONFIG_FIELD_NAME, Strings.toString(builder));
Expand Down Expand Up @@ -1350,7 +1369,7 @@ private static void withRoleDescriptor(XContentBuilder builder, RoleDescriptor r
withIndicesPrivileges(builder, indicesPrivileges);
}
builder.endArray();
// the toXContent method of the {@code RoleDescriptor.ApplicationResourcePrivileges) does a good job
// the toXContent method of the {@code RoleDescriptor.ApplicationResourcePrivileges} does a good job
builder.xContentList(RoleDescriptor.Fields.APPLICATIONS.getPreferredName(), roleDescriptor.getApplicationPrivileges());
builder.array(RoleDescriptor.Fields.RUN_AS.getPreferredName(), roleDescriptor.getRunAs());
if (roleDescriptor.getMetadata() != null && false == roleDescriptor.getMetadata().isEmpty()) {
Expand Down Expand Up @@ -1401,15 +1420,7 @@ LogEntryBuilder withRequestBody(DeleteUserRequest deleteUserRequest) throws IOEx
}

LogEntryBuilder withRequestBody(DeleteRoleRequest deleteRoleRequest) throws IOException {
logEntry.with(EVENT_ACTION_FIELD_NAME, "delete_role");
XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
builder.startObject()
.startObject("role")
.field("name", deleteRoleRequest.name())
.endObject() // role
.endObject();
logEntry.with(DELETE_CONFIG_FIELD_NAME, Strings.toString(builder));
return this;
return withDeleteRole(deleteRoleRequest.name());
}

LogEntryBuilder withRequestBody(DeleteRoleMappingRequest deleteRoleMappingRequest) throws IOException {
Expand Down Expand Up @@ -1532,6 +1543,18 @@ LogEntryBuilder withRequestBody(SetProfileEnabledRequest setProfileEnabledReques
return this;
}

LogEntryBuilder withDeleteRole(String roleName) throws IOException {
logEntry.with(EVENT_ACTION_FIELD_NAME, "delete_role");
XContentBuilder builder = JsonXContent.contentBuilder().humanReadable(true);
builder.startObject()
.startObject("role")
.field("name", roleName)
.endObject() // role
.endObject();
logEntry.with(DELETE_CONFIG_FIELD_NAME, Strings.toString(builder));
return this;
}

static void withGrant(XContentBuilder builder, Grant grant) throws IOException {
builder.startObject("grant").field("type", grant.getType());
if (grant.getUsername() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.ActionTypes;
import org.elasticsearch.xpack.core.security.action.apikey.ApiKeyTests;
import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.apikey.BulkUpdateApiKeyRequest;
Expand All @@ -73,6 +74,8 @@
import org.elasticsearch.xpack.core.security.action.profile.SetProfileEnabledRequest;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataAction;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataRequest;
import org.elasticsearch.xpack.core.security.action.role.BulkDeleteRolesRequest;
import org.elasticsearch.xpack.core.security.action.role.BulkPutRolesRequest;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleRequest;
import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
Expand Down Expand Up @@ -772,20 +775,19 @@ public void testSecurityConfigChangeEventFormattingForRoles() throws IOException
auditTrail.accessGranted(requestId, authentication, PutRoleAction.NAME, putRoleRequest, authorizationInfo);
output = CapturingLogger.output(logger.getName(), Level.INFO);
assertThat(output.size(), is(2));
String generatedPutRoleAuditEventString = output.get(1);
String expectedPutRoleAuditEventString = Strings.format("""
"put":{"role":{"name":"%s","role_descriptor":%s}}\
""", putRoleRequest.name(), auditedRolesMap.get(putRoleRequest.name()));
assertThat(generatedPutRoleAuditEventString, containsString(expectedPutRoleAuditEventString));
generatedPutRoleAuditEventString = generatedPutRoleAuditEventString.replace(", " + expectedPutRoleAuditEventString, "");
checkedFields = new HashMap<>(commonFields);
checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
checkedFields.put("type", "audit");
checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change");
checkedFields.put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "put_role");
checkedFields.put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
assertMsg(generatedPutRoleAuditEventString, checkedFields);
assertPutRoleAuditLogLine(putRoleRequest.name(), output.get(1), auditedRolesMap, requestId);
// clear log
CapturingLogger.output(logger.getName(), Level.INFO).clear();

BulkPutRolesRequest bulkPutRolesRequest = new BulkPutRolesRequest(allTestRoleDescriptors);
bulkPutRolesRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
auditTrail.accessGranted(requestId, authentication, ActionTypes.BULK_PUT_ROLES.name(), bulkPutRolesRequest, authorizationInfo);
output = CapturingLogger.output(logger.getName(), Level.INFO);
assertThat(output.size(), is(allTestRoleDescriptors.size() + 1));

for (int i = 0; i < allTestRoleDescriptors.size(); i++) {
assertPutRoleAuditLogLine(allTestRoleDescriptors.get(i).getName(), output.get(i + 1), auditedRolesMap, requestId);
}
// clear log
CapturingLogger.output(logger.getName(), Level.INFO).clear();

Expand All @@ -795,25 +797,64 @@ public void testSecurityConfigChangeEventFormattingForRoles() throws IOException
auditTrail.accessGranted(requestId, authentication, DeleteRoleAction.NAME, deleteRoleRequest, authorizationInfo);
output = CapturingLogger.output(logger.getName(), Level.INFO);
assertThat(output.size(), is(2));
String generatedDeleteRoleAuditEventString = output.get(1);
assertDeleteRoleAuditLogLine(putRoleRequest.name(), output.get(1), requestId);
// clear log
CapturingLogger.output(logger.getName(), Level.INFO).clear();

BulkDeleteRolesRequest bulkDeleteRolesRequest = new BulkDeleteRolesRequest(
allTestRoleDescriptors.stream().map(RoleDescriptor::getName).toList()
);
bulkDeleteRolesRequest.setRefreshPolicy(randomFrom(WriteRequest.RefreshPolicy.values()));
auditTrail.accessGranted(
requestId,
authentication,
ActionTypes.BULK_DELETE_ROLES.name(),
bulkDeleteRolesRequest,
authorizationInfo
);
output = CapturingLogger.output(logger.getName(), Level.INFO);
assertThat(output.size(), is(allTestRoleDescriptors.size() + 1));
for (int i = 0; i < allTestRoleDescriptors.size(); i++) {
assertDeleteRoleAuditLogLine(allTestRoleDescriptors.get(i).getName(), output.get(i + 1), requestId);
}
}

private void assertPutRoleAuditLogLine(String roleName, String logLine, Map<String, String> expectedLogByRoleName, String requestId) {
String expectedPutRoleAuditEventString = Strings.format("""
"put":{"role":{"name":"%s","role_descriptor":%s}}\
""", roleName, expectedLogByRoleName.get(roleName));

assertThat(logLine, containsString(expectedPutRoleAuditEventString));
String reducedLogLine = logLine.replace(", " + expectedPutRoleAuditEventString, "");
Map<String, String> checkedFields = new HashMap<>(commonFields);
checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
checkedFields.put("type", "audit");
checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change");
checkedFields.put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "put_role");
checkedFields.put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
assertMsg(reducedLogLine, checkedFields);
}

private void assertDeleteRoleAuditLogLine(String roleName, String logLine, String requestId) {
StringBuilder deleteRoleStringBuilder = new StringBuilder().append("\"delete\":{\"role\":{\"name\":");
if (deleteRoleRequest.name() == null) {
if (roleName == null) {
deleteRoleStringBuilder.append("null");
} else {
deleteRoleStringBuilder.append("\"").append(deleteRoleRequest.name()).append("\"");
deleteRoleStringBuilder.append("\"").append(roleName).append("\"");
}
deleteRoleStringBuilder.append("}}");
String expectedDeleteRoleAuditEventString = deleteRoleStringBuilder.toString();
assertThat(generatedDeleteRoleAuditEventString, containsString(expectedDeleteRoleAuditEventString));
generatedDeleteRoleAuditEventString = generatedDeleteRoleAuditEventString.replace(", " + expectedDeleteRoleAuditEventString, "");
checkedFields = new HashMap<>(commonFields);
assertThat(logLine, containsString(expectedDeleteRoleAuditEventString));
String reducedLogLine = logLine.replace(", " + expectedDeleteRoleAuditEventString, "");
Map<String, String> checkedFields = new HashMap<>(commonFields);
checkedFields.remove(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME);
checkedFields.remove(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME);
checkedFields.put("type", "audit");
checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, "security_config_change");
checkedFields.put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "delete_role");
checkedFields.put(LoggingAuditTrail.REQUEST_ID_FIELD_NAME, requestId);
assertMsg(generatedDeleteRoleAuditEventString, checkedFields);
assertMsg(reducedLogLine, checkedFields);
}

public void testSecurityConfigChangeEventForCrossClusterApiKeys() throws IOException {
Expand Down Expand Up @@ -1975,6 +2016,11 @@ public void testSecurityConfigChangedEventSelection() {
Tuple<String, TransportRequest> actionAndRequest = randomFrom(
new Tuple<>(PutUserAction.NAME, new PutUserRequest()),
new Tuple<>(PutRoleAction.NAME, new PutRoleRequest()),
new Tuple<>(
ActionTypes.BULK_PUT_ROLES.name(),
new BulkPutRolesRequest(List.of(new RoleDescriptor(randomAlphaOfLength(20), null, null, null)))
),
new Tuple<>(ActionTypes.BULK_DELETE_ROLES.name(), new BulkDeleteRolesRequest(List.of(randomAlphaOfLength(20)))),
new Tuple<>(PutRoleMappingAction.NAME, new PutRoleMappingRequest()),
new Tuple<>(TransportSetEnabledAction.TYPE.name(), new SetEnabledRequest()),
new Tuple<>(TransportChangePasswordAction.TYPE.name(), new ChangePasswordRequest()),
Expand Down