Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5dba6b4
update junit library to 5.11.4 to enable debugging in VScode. see htt…
markdboyd May 12, 2025
0dfeb66
update User.parse() to parse custom attribute names from string
markdboyd May 12, 2025
78b0b78
update unit test to verify that User.parse() parses custom attribute …
markdboyd May 12, 2025
f6b3e97
apply spotless formatting
markdboyd May 13, 2025
9cec5ac
add more debug logs
markdboyd May 14, 2025
40b6a5c
update debug statements
markdboyd May 14, 2025
0d78d8e
remove debug logging
markdboyd Jul 18, 2025
8346d84
remove changes to build.gradle
markdboyd Jul 18, 2025
f44f4b5
add logic in User.parse to handle case where tenant is "null" as a li…
markdboyd Jul 22, 2025
3b8fd74
add test for User.parse where tenant is literal string "null"
markdboyd Jul 22, 2025
24ad224
add initial pass at deserializing user custom attributes from thread …
markdboyd Jul 22, 2025
68ae6d6
apply spotless formatting
markdboyd Jul 22, 2025
7e42436
add Base64Helper for deserializing base 64 encoded content
markdboyd Jul 22, 2025
56efe37
move files into correct directory structure
markdboyd Jul 22, 2025
49a7209
update junit-jupiter-engine to 5.11.4
markdboyd Jul 22, 2025
56e5f1b
remove unnecessary throws IOException in definition of User.parse()
markdboyd Jul 22, 2025
bb23f06
update tests for User
markdboyd Jul 22, 2025
6304063
update InjectSecurityTest for compatibility with user custom attribut…
markdboyd Jul 22, 2025
0b859cd
update testParseUserString to test for serialized custom user attributes
markdboyd Jul 23, 2025
ccc8475
refactor parsing of tenant information from string in User.parse
markdboyd Jul 23, 2025
59b647f
add additional classes to SafeSerializationUtils.SAFE_CLASS_NAMES
markdboyd Jul 23, 2025
aebab23
Update src/test/java/org/opensearch/commons/authuser/UserTest.java
markdboyd Jul 23, 2025
805bcab
fix variable reference
markdboyd Jul 23, 2025
35443a1
stub out test for User.parse of XContent
markdboyd Jul 23, 2025
4e98ed3
fix arguments to create User in TestHelpers.kt
markdboyd Jul 24, 2025
f714da2
fix testParseUserXContent test and add more test assertions for other…
markdboyd Jul 24, 2025
17d2d75
add Base64HelperTest
markdboyd Jul 24, 2025
b604357
add SafeSerializationUtilsTest.java
markdboyd Jul 24, 2025
d300c3f
run spotlessApply
markdboyd Jul 24, 2025
918a27b
update User.toString() to use TreeMap for custom attributes to keep a…
markdboyd Jul 24, 2025
d00efdd
update User tests for parsing from JSON string to include custom attr…
markdboyd Jul 24, 2025
890d7ff
add test for parsing user custom attributes from JSON
markdboyd Jul 24, 2025
56cfc3d
update custom_attributes in XContentTests fixture
markdboyd Jul 24, 2025
23d9e43
remove special handling of requestedTenant in User.parse()
markdboyd Jul 24, 2025
ff78bfe
update expectation for tenant in testParseUserStringNameWithNullTenant
markdboyd Jul 24, 2025
a8df5d7
update User to preserve backwards compatibilty for custom attributes …
markdboyd Jul 24, 2025
59e163d
include parsing of custom attribute names from JSON in User.parse() f…
markdboyd Jul 24, 2025
64a5ff9
apply spotless formatting
markdboyd Jul 24, 2025
d3896b7
update ser/de for user to stream to handle backwards compatibility fo…
markdboyd Jul 24, 2025
c8fa0dd
add test for streaming user on older versions of OpenSearch for backw…
markdboyd Jul 24, 2025
7519da5
fix bad rebasing
markdboyd Aug 4, 2025
dae4485
fix bad rebasing
markdboyd Aug 4, 2025
32cda23
update comment on User.parse() for string to specify expected string …
markdboyd Aug 4, 2025
bfdd5c7
re-add constructor for User with custom attribute names for backwards…
markdboyd Aug 12, 2025
43a7386
re-add getter for custom attribute names to User for backwards compat…
markdboyd Aug 12, 2025
eeb800f
add test of User constructor with custom attribute names for backward…
markdboyd Aug 12, 2025
13f5854
fix spotless formatting issues
markdboyd Aug 12, 2025
b3b1e4e
update InjectSecurity.injectUserInfo() to set user custom attributes …
markdboyd Aug 13, 2025
16cc318
apply spotless formatting
markdboyd Aug 13, 2025
1bdbfad
update User.toXContent() to conditionally include custom attributes w…
markdboyd Aug 14, 2025
01e471d
fix unit test expectation
markdboyd Aug 14, 2025
1531830
apply spotless formatting
markdboyd Aug 15, 2025
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
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,12 @@ dependencies {
testImplementation "org.opensearch.test:framework:${opensearch_version}"
testImplementation "org.jetbrains.kotlin:kotlin-test:${kotlin_version}"
testImplementation "org.mockito:mockito-core:3.10.0"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4'
testImplementation 'org.mockito:mockito-junit-jupiter:3.10.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation "com.cronutils:cron-utils:9.1.6"
testImplementation "commons-validator:commons-validator:1.7"
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.4'

ktlint "com.pinterest:ktlint:0.47.1"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private static Setting<SecureString> createFallbackInsecureSetting(String key) {
);
public static final String OPENSEARCH_SECURITY_INJECTED_ROLES = "opendistro_security_injected_roles";
public static final String INJECTED_USER = "injected_user";
public static final String INJECTED_USER_CUSTOM_ATTRIBUTES = "injected_user_custom_attributes";
public static final String OPENSEARCH_SECURITY_USE_INJECTED_USER_FOR_PLUGINS = "plugins.security_use_injected_user_for_plugins";
public static final String OPENSEARCH_SECURITY_SSL_HTTP_ENABLED = "plugins.security.ssl.http.enabled";
public static final String OPENSEARCH_SECURITY_AUTHCZ_ADMIN_DN = "plugins.security.authcz.admin_dn";
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/opensearch/commons/InjectSecurity.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.opensearch.commons;

import static org.opensearch.commons.ConfigConstants.INJECTED_USER;
import static org.opensearch.commons.ConfigConstants.INJECTED_USER_CUSTOM_ATTRIBUTES;
import static org.opensearch.commons.ConfigConstants.OPENSEARCH_SECURITY_INJECTED_ROLES;
import static org.opensearch.commons.ConfigConstants.OPENSEARCH_SECURITY_USE_INJECTED_USER_FOR_PLUGINS;
import static org.opensearch.commons.authuser.Utils.escapePipe;
Expand Down Expand Up @@ -155,7 +156,15 @@ public void injectUserInfo(final User user) {
if (!Strings.isNullOrEmpty(requestedTenant)) {
joiner.add(escapePipe(requestedTenant));
}

threadContext.putTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString());

if (threadContext.getTransient(INJECTED_USER_CUSTOM_ATTRIBUTES) == null) {
threadContext.putTransient(INJECTED_USER_CUSTOM_ATTRIBUTES, user.getCustomAttributes());
log.debug("{}, InjectSecurity - inject user custom attributes: {}", Thread.currentThread().getName(), id);
} else {
log.error("{}, InjectSecurity - most likely thread context corruption : {}", Thread.currentThread().getName(), id);
}
}

/**
Expand Down
108 changes: 89 additions & 19 deletions src/main/java/org/opensearch/commons/authuser/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
Expand All @@ -26,6 +29,7 @@
import org.opensearch.common.xcontent.XContentHelper;
import org.opensearch.common.xcontent.json.JsonXContent;
import org.opensearch.commons.ConfigConstants;
import org.opensearch.commons.authuser.util.Base64Helper;
import org.opensearch.core.common.Strings;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
Expand All @@ -45,13 +49,14 @@ final public class User implements Writeable, ToXContent {
public static final String BACKEND_ROLES_FIELD = "backend_roles";
public static final String ROLES_FIELD = "roles";
public static final String CUSTOM_ATTRIBUTE_NAMES_FIELD = "custom_attribute_names";
public static final String CUSTOM_ATTRIBUTES_FIELD = "custom_attributes";
public static final String REQUESTED_TENANT_FIELD = "user_requested_tenant";
public static final String REQUESTED_TENANT_ACCESS = "user_requested_tenant_access";

private final String name;
private final List<String> backendRoles;
private final List<String> roles;
private final List<String> customAttNames;
private final Map<String, String> customAttributes;
@Nullable
private final String requestedTenant;
@Nullable
Expand All @@ -61,16 +66,25 @@ public User() {
name = "";
backendRoles = new ArrayList<>();
roles = new ArrayList<>();
customAttNames = new ArrayList<>();
customAttributes = new HashMap<>();
requestedTenant = null;
requestedTenantAccess = null;
}

public User(final String name, final List<String> backendRoles, List<String> roles, Map<String, String> customAttributes) {
this.name = name;
this.backendRoles = backendRoles;
this.roles = roles;
this.customAttributes = customAttributes;
this.requestedTenant = null;
this.requestedTenantAccess = null;
}

public User(final String name, final List<String> backendRoles, List<String> roles, List<String> customAttNames) {
this.name = name;
this.backendRoles = backendRoles;
this.roles = roles;
this.customAttNames = customAttNames;
this.customAttributes = this.convertCustomAttributeNamesToMap(customAttNames);
this.requestedTenant = null;
this.requestedTenantAccess = null;
}
Expand All @@ -79,13 +93,13 @@ public User(
final String name,
final List<String> backendRoles,
final List<String> roles,
final List<String> customAttNames,
final Map<String, String> customAttributes,
@Nullable final String requestedTenant
) {
this.name = name;
this.backendRoles = backendRoles;
this.roles = roles;
this.customAttNames = customAttNames;
this.customAttributes = customAttributes;
this.requestedTenant = requestedTenant;
this.requestedTenantAccess = null;
}
Expand All @@ -94,14 +108,14 @@ public User(
final String name,
final List<String> backendRoles,
final List<String> roles,
final List<String> customAttNames,
final Map<String, String> customAttributes,
@Nullable final String requestedTenant,
@Nullable final String requestedTenantAccess
) {
this.name = name;
this.backendRoles = backendRoles;
this.roles = roles;
this.customAttNames = customAttNames;
this.customAttributes = customAttributes;
this.requestedTenant = requestedTenant;
this.requestedTenantAccess = requestedTenantAccess;
}
Expand All @@ -125,7 +139,16 @@ public User(String json) {
name = (String) mapValue.get("user_name");
backendRoles = (List<String>) mapValue.get("backend_roles");
roles = (List<String>) mapValue.get("roles");
customAttNames = (List<String>) mapValue.get("custom_attribute_names");

Map<String, String> customAttributesFromJson = (Map<String, String>) mapValue.get("custom_attributes");
List<String> customAttNames = (List<String>) mapValue.get("custom_attribute_names");

if (customAttributesFromJson != null) {
customAttributes = customAttributesFromJson;
} else {
customAttributes = this.convertCustomAttributeNamesToMap(customAttNames);
}

requestedTenant = (String) mapValue.getOrDefault("user_requested_tenant", null);
requestedTenantAccess = (String) mapValue.getOrDefault("user_requested_tenant_access", null);
}
Expand All @@ -134,7 +157,12 @@ public User(StreamInput in) throws IOException {
name = in.readString();
backendRoles = in.readStringList();
roles = in.readStringList();
customAttNames = in.readStringList();
if (in.getVersion().onOrAfter(Version.V_3_2_0)) {
customAttributes = in.readMap(StreamInput::readString, StreamInput::readString);
} else {
List<String> customAttNames = in.readStringList();
customAttributes = this.convertCustomAttributeNamesToMap(customAttNames);
}
requestedTenant = in.readOptionalString();
if (in.getVersion().onOrAfter(Version.V_3_2_0)) {
requestedTenantAccess = in.readOptionalString();
Expand All @@ -147,7 +175,7 @@ public static User parse(XContentParser parser) throws IOException {
String name = "";
List<String> backendRoles = new ArrayList<>();
List<String> roles = new ArrayList<>();
List<String> customAttNames = new ArrayList<>();
Map<String, String> customAttributes = new HashMap<>();
String requestedTenant = null;
String requestedTenantAccess = null;

Expand All @@ -171,10 +199,19 @@ public static User parse(XContentParser parser) throws IOException {
roles.add(parser.text());
}
break;
case CUSTOM_ATTRIBUTES_FIELD:
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser);
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
String attrName = parser.currentName();
parser.nextToken();
String attrValue = parser.text();
customAttributes.put(attrName, attrValue);
}
break;
case CUSTOM_ATTRIBUTE_NAMES_FIELD:
ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser);
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
customAttNames.add(parser.text());
customAttributes.put(parser.text(), null);
}
break;
case REQUESTED_TENANT_FIELD:
Expand All @@ -187,14 +224,16 @@ public static User parse(XContentParser parser) throws IOException {
break;
}
}
return new User(name, backendRoles, roles, customAttNames, requestedTenant, requestedTenantAccess);

return new User(name, backendRoles, roles, customAttributes, requestedTenant, requestedTenantAccess);
}

/**
* User String format must be pipe separated as : user_name|backendrole1,backendrole2|roles1,role2
* User String format must be pipe separated as : user_name|backendrole1,backendrole2|roles1,role2|tenant|tenantAccess|base64-encoded(serialized(custom atttributes))
* @param userString
* @return
*/
@SuppressWarnings("unchecked")
public static User parse(final String userString) {
if (Strings.isNullOrEmpty(userString)) {
return null;
Expand All @@ -212,6 +251,7 @@ public static User parse(final String userString) {
List<String> roles = new ArrayList<>();
String requestedTenant = null;
String requestedTenantAccess = null;
Map<String, String> customAttributes = new HashMap<>();

if ((strs.length > 1) && !Strings.isNullOrEmpty(strs[1])) {
backendRoles.addAll(Arrays.stream(strs[1].split(",")).map(Utils::unescapePipe).toList());
Expand All @@ -225,7 +265,11 @@ public static User parse(final String userString) {
if ((strs.length > 4) && !Strings.isNullOrEmpty(strs[4])) {
requestedTenantAccess = strs[4].trim();
}
return new User(userName, backendRoles, roles, Arrays.asList(), requestedTenant, requestedTenantAccess);
if ((strs.length > 5) && !Strings.isNullOrEmpty(strs[5])) {
customAttributes = (Map<String, String>) Base64Helper.deserializeObject(strs[5]);
}

return new User(userName, backendRoles, roles, customAttributes, requestedTenant, requestedTenantAccess);
}

@Override
Expand All @@ -235,9 +279,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
.field(NAME_FIELD, name)
.field(BACKEND_ROLES_FIELD, backendRoles)
.field(ROLES_FIELD, roles)
.field(CUSTOM_ATTRIBUTE_NAMES_FIELD, customAttNames)
.field(REQUESTED_TENANT_FIELD, requestedTenant)
.field(REQUESTED_TENANT_ACCESS, requestedTenantAccess);

if (customAttributes.size() > 0) {
builder.field(CUSTOM_ATTRIBUTES_FIELD, customAttributes);
} else {
builder.field(CUSTOM_ATTRIBUTE_NAMES_FIELD, new ArrayList<>());
}

return builder.endObject();
}

Expand All @@ -246,7 +296,12 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
out.writeStringCollection(backendRoles);
out.writeStringCollection(roles);
out.writeStringCollection(customAttNames);
if (out.getVersion().onOrAfter(Version.V_3_2_0)) {
out.writeMap(customAttributes, StreamOutput::writeString, StreamOutput::writeString);
} else {
List<String> customAttributeNames = new ArrayList<>(customAttributes.keySet());
out.writeStringCollection(customAttributeNames);
}
out.writeOptionalString(requestedTenant);
if (out.getVersion().onOrAfter(Version.V_3_2_0)) {
out.writeOptionalString(requestedTenantAccess);
Expand All @@ -259,7 +314,9 @@ public String toString() {
builder.add(NAME_FIELD, name);
builder.add(BACKEND_ROLES_FIELD, backendRoles);
builder.add(ROLES_FIELD, roles);
builder.add(CUSTOM_ATTRIBUTE_NAMES_FIELD, customAttNames);
TreeMap<String, String> sortedCustomAttributes = new TreeMap<>();
sortedCustomAttributes.putAll(customAttributes);
builder.add(CUSTOM_ATTRIBUTES_FIELD, sortedCustomAttributes);
builder.add(REQUESTED_TENANT_FIELD, requestedTenant);
builder.add(REQUESTED_TENANT_ACCESS, requestedTenantAccess);
return builder.toString();
Expand All @@ -274,7 +331,7 @@ public boolean equals(Object obj) {
return this.name.equals(that.name)
&& this.getBackendRoles().equals(that.backendRoles)
&& this.getRoles().equals(that.roles)
&& this.getCustomAttNames().equals(that.customAttNames)
&& this.getCustomAttributes().equals(that.customAttributes)
&& (Objects.equals(this.requestedTenant, that.requestedTenant))
&& (Objects.equals(this.requestedTenantAccess, that.requestedTenantAccess));
}
Expand All @@ -291,8 +348,12 @@ public List<String> getRoles() {
return roles;
}

public Map<String, String> getCustomAttributes() {
return customAttributes;
}

public List<String> getCustomAttNames() {
return customAttNames;
return this.getCustomAttributeNamesFromMap(this.customAttributes);
}

@Nullable
Expand All @@ -312,4 +373,13 @@ public boolean isAdminDn(Settings settings) {
List<String> adminDns = settings.getAsList(ConfigConstants.OPENSEARCH_SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList());
return adminDns.contains(this.name);
}

private Map<String, String> convertCustomAttributeNamesToMap(List<String> customAttNames) {
return customAttNames.stream().collect(Collectors.toMap(key -> key, key -> "null"));
}

private List<String> getCustomAttributeNamesFromMap(Map<String, String> customAttributes) {
List<String> customAttNames = new ArrayList<>(this.customAttributes.keySet());
return customAttNames;
}
}
Loading
Loading