Skip to content
Draft
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
5 changes: 5 additions & 0 deletions docs/changelog/137712.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 137712
summary: Add User Profile Size Limit Enforced During Profile Updates
area: Security
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public class ProfileService {
private static final BackoffPolicy DEFAULT_BACKOFF = BackoffPolicy.exponentialBackoff();
private static final int DIFFERENTIATOR_UPPER_LIMIT = 9;
private static final long ACTIVATE_INTERVAL_IN_MS = TimeValue.timeValueSeconds(30).millis();
private static final int MAX_PROFILE_SIZE_IN_BYTES = 10_000_000;

private final Settings settings;
private final Clock clock;
Expand Down Expand Up @@ -241,10 +242,58 @@ public void updateProfileData(UpdateProfileDataRequest request, ActionListener<A
return;
}

doUpdate(
buildUpdateRequest(request.getUid(), builder, request.getRefreshPolicy(), request.getIfPrimaryTerm(), request.getIfSeqNo()),
listener.map(updateResponse -> AcknowledgedResponse.TRUE)
);
getVersionedDocument(request.getUid(), ActionListener.wrap(doc -> {
validateProfileSize(doc, request, MAX_PROFILE_SIZE_IN_BYTES);

doUpdate(
buildUpdateRequest(request.getUid(), builder, request.getRefreshPolicy(), request.getIfPrimaryTerm(), request.getIfSeqNo()),
listener.map(updateResponse -> AcknowledgedResponse.TRUE)
);
}, listener::onFailure));
}

static void validateProfileSize(VersionedDocument doc, UpdateProfileDataRequest request, int limit) {
if (doc == null) {
return;
}
Map<String, Object> labels = combineMaps(doc.doc.labels(), request.getLabels());
Map<String, Object> data = combineMaps(mapFromBytesReference(doc.doc.applicationData()), request.getData());
int actualSize = serializationSize(labels) + serializationSize(data);
if (actualSize > limit) {
throw new ElasticsearchException(
Strings.format(
"cannot update profile [%s] because the combined profile size of [%s] bytes exceeds the maximum of [%s] bytes",
request.getUid(),
actualSize,
limit
)
);
}
}

static Map<String, Object> combineMaps(Map<String, Object> src, Map<String, Object> update) {
Map<String, Object> result = new HashMap<>(); // ensure mutable outer source map for update below
if (src != null) {
result.putAll(src);
}
XContentHelper.update(result, update, false);
return result;
}

static Map<String, Object> mapFromBytesReference(BytesReference bytesRef) {
if (bytesRef == null || bytesRef.length() == 0) {
return new HashMap<>();
}
return XContentHelper.convertToMap(bytesRef, false, XContentType.JSON).v2();
}

static int serializationSize(Map<String, Object> map) {
try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
builder.value(map);
return BytesReference.bytes(builder).length();
} catch (IOException e) {
throw new ElasticsearchException("Error occurred computing serialization size", e); // I/O error should never happen here
}
}

public void suggestProfile(SuggestProfilesRequest request, TaskId parentTaskId, ActionListener<SuggestProfilesResponse> listener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.elasticsearch.action.search.TransportMultiSearchAction;
import org.elasticsearch.action.search.TransportSearchAction;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.action.update.TransportUpdateAction;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateRequestBuilder;
Expand Down Expand Up @@ -72,6 +73,7 @@
import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesRequest;
import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesRequestTests;
import org.elasticsearch.xpack.core.security.action.profile.SuggestProfilesResponse;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTests;
Expand All @@ -90,7 +92,10 @@
import org.junit.Before;
import org.mockito.Mockito;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
Expand Down Expand Up @@ -1324,6 +1329,54 @@ public void testProfilesIndexMissingOrUnavailableWhenRetrievingProfilesOfApiKeyO
assertThat(e.getMessage(), containsString("test unavailable"));
}

public void testSerializationSize() {
assertThat(ProfileService.serializationSize(Map.of()), is(2));
assertThat(ProfileService.serializationSize(Map.of("foo", "bar")), is(13));
assertThrows(
IllegalArgumentException.class,
() -> ProfileService.serializationSize(Map.of("bad", new ByteArrayInputStream(new byte[0])))
);
}

public void testMapFromBytesReference() {
assertThat(ProfileService.mapFromBytesReference(null), is(Map.of()));
assertThat(ProfileService.mapFromBytesReference(BytesReference.fromByteBuffer(ByteBuffer.allocate(0))), is(Map.of()));
assertThat(ProfileService.mapFromBytesReference(newBytesReference("{}")), is(Map.of()));
assertThat(ProfileService.mapFromBytesReference(newBytesReference("{\"foo\":\"bar\"}")), is(Map.of("foo", "bar")));
}

public void testCombineMaps() {
assertThat(ProfileService.combineMaps(null, Map.of("a", 1)), is(Map.of("a", 1)));
assertThat(
ProfileService.combineMaps(new HashMap<>(Map.of("a", 1, "b", 2)), Map.of("b", 3, "c", 4)),
is(Map.of("a", 1, "b", 3, "c", 4))
);
assertThat(
ProfileService.combineMaps(new HashMap<>(Map.of("a", new HashMap<>(Map.of("b", "c")))), Map.of("a", Map.of("d", "e"))),
is(Map.of("a", Map.of("b", "c", "d", "e")))
);
}

public void testValidateProfileSize() {
var pd = new ProfileDocument("uid", true, 0L, null, Map.of(), newBytesReference("{}"));
var vd = new ProfileService.VersionedDocument(pd, 1L, 1L);
var up = new UpdateProfileDataRequest(
"uid",
Map.of("key", "value"),
Map.of("key", "value"),
1L,
1L,
WriteRequest.RefreshPolicy.NONE
);
assertThrows(ElasticsearchException.class, () -> ProfileService.validateProfileSize(vd, up, 0));
ProfileService.validateProfileSize(vd, up, 100);
ProfileService.validateProfileSize(null, up, 0);
}

private static BytesReference newBytesReference(String str) {
return BytesReference.fromByteBuffer(ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8)));
}

record SampleDocumentParameter(String uid, String username, List<String> roles, long lastSynchronized) {}

private void mockMultiGetRequest(List<SampleDocumentParameter> sampleDocumentParameters) {
Expand Down