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 backend filtering for legacy internal user and service accounts #2786

Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
8326734
initial draft
samuelcostae May 11, 2023
c18908a
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae May 19, 2023
7a836bd
Fix the `import org.opensearch.core.common.Strings;`
RyanL1997 May 18, 2023
8c43af9
cleanup
samuelcostae May 23, 2023
cbc1fb4
Update src/main/java/org/opensearch/security/user/UserService.java
samuelcostae May 23, 2023
d931698
Update src/main/java/org/opensearch/security/dlic/rest/api/InternalUs…
samuelcostae May 23, 2023
0bc21aa
Update src/main/java/org/opensearch/security/user/UserService.java
samuelcostae May 23, 2023
44bfe91
restoring new endpoints. removing unused imports
samuelcostae May 23, 2023
5bbc0ec
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae May 24, 2023
80dc161
refactoring
samuelcostae May 25, 2023
5fc1623
tests
samuelcostae May 29, 2023
d7e7498
Expliciting imports
samuelcostae May 30, 2023
ca3c27c
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Jun 26, 2023
a9e52c1
spotless apply
samuelcostae Jun 27, 2023
fc93999
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Jun 27, 2023
71a256d
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Jul 3, 2023
a1f42c4
#2704 Merge bug on SecurityDynamicConfiguration.java
samuelcostae Jul 10, 2023
37993d0
Update src/main/java/org/opensearch/security/user/UserService.java
samuelcostae Jul 28, 2023
5dd10b1
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Aug 9, 2023
dd3f453
Fixing imports and references for core changes on Strings class and X…
samuelcostae Aug 9, 2023
448cdf6
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Aug 9, 2023
7bca971
aggregating new filters to existing endpoint and filtering by request…
samuelcostae Aug 9, 2023
4e9fb82
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Aug 9, 2023
3bdbb01
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Aug 11, 2023
4816f90
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Aug 14, 2023
99fab2b
Merge branch 'opensearch-project:main' into 2704-filter-internal-and-…
samuelcostae Aug 16, 2023
8cc81f0
Fixing * Import
samuelcostae Aug 16, 2023
5c04681
Java doc for filter method and small fixes
samuelcostae Aug 18, 2023
6654233
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Aug 28, 2023
80925f4
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Aug 28, 2023
71b83f1
refactor and test changes
samuelcostae Aug 28, 2023
be44715
refactor and test changes
samuelcostae Aug 30, 2023
fce3ef9
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Sep 15, 2023
6a82370
draft changes after refactor
samuelcostae Sep 18, 2023
0351efa
draft changes after refactor
samuelcostae Sep 19, 2023
28ed6a7
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Sep 19, 2023
b9bf81b
changes from PR feedback
samuelcostae Sep 19, 2023
81cbbc0
Update src/main/java/org/opensearch/security/user/UserService.java
samuelcostae Sep 20, 2023
82708fd
small bug fix
samuelcostae Sep 20, 2023
0d3916d
spotless and draft unit test
samuelcostae Sep 20, 2023
80ecaa4
Merge branch 'main' into 2704-filter-internal-and-service-accounts
samuelcostae Sep 20, 2023
34a90dd
test and filter enum changes
samuelcostae Sep 21, 2023
d887b40
Service/Attributes bug on Handling new account put rest request
samuelcostae Sep 22, 2023
6c24fe2
Service/Attributes bug on Handling new account put rest request
samuelcostae Sep 22, 2023
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 @@ -27,6 +27,7 @@
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.inject.Inject;
import org.opensearch.common.settings.Settings;
import org.opensearch.core.common.Strings;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestController;
import org.opensearch.rest.RestRequest;
Expand All @@ -44,6 +45,7 @@
import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.ssl.transport.PrincipalExtractor;
import org.opensearch.security.support.SecurityJsonNode;
import org.opensearch.security.user.AccountType;
import org.opensearch.security.user.UserService;
import org.opensearch.security.user.UserServiceException;
import org.opensearch.threadpool.ThreadPool;
Expand All @@ -52,10 +54,19 @@
import static org.opensearch.security.dlic.rest.support.Utils.hash;

public class InternalUsersApiAction extends PatchableResourceApiAction {
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved

@Override
protected void consumeParameters(final RestRequest request) {
request.param("name");
request.param("filterBy");
}

static final List<String> RESTRICTED_FROM_USERNAME = ImmutableList.of(
":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057
);

public static final String LEGACY_OPENDISTRO_PREFIX = "_opendistro/_security";
public static final String PLUGINS_PREFIX = "_plugins/_security";
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
private static final List<Route> routes = addRoutesPrefix(
ImmutableList.of(
new Route(Method.GET, "/user/{name}"),
Expand Down Expand Up @@ -115,12 +126,42 @@ protected Endpoint getEndpoint() {
return Endpoint.INTERNALUSERS;
}

@Override
protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException {
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
final String resourcename = request.param("name");

SecurityDynamicConfiguration<?> configuration = load(getConfigName(), true);
filter(configuration);

String includeIfType = request.param("filterBy", "any");

if (includeIfType != "any") {
try {
userService.includeAccountsIfType(configuration, AccountType.valueOf(includeIfType.toUpperCase()));
} catch (UserServiceException ex) {
badRequestResponse(channel, ex.getMessage());
}
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
}

// no specific resource requested, return complete config
if (Strings.isNullOrEmpty(resourcename)) {
successResponse(channel, configuration);
return;
}
if (!configuration.exists(resourcename)) {
notFound(channel, "Resource '" + resourcename + "' not found.");
return;
}
configuration.removeOthers(resourcename);
peternied marked this conversation as resolved.
Show resolved Hide resolved
successResponse(channel, configuration);
}

@Override
protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content)
throws IOException {

final String username = request.param("name");

final boolean service = content.get("service").asBoolean();
SecurityDynamicConfiguration<?> internalUsersConfiguration = load(getConfigName(), false);

if (!isWriteable(channel, internalUsersConfiguration, username)) {
Expand All @@ -145,8 +186,9 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C
// when updating an existing user password hash can be blank, which means no
// changes


try {
if (request.hasParam("service")) {
if (service) {
((ObjectNode) content).put("service", request.param("service"));
}
if (request.hasParam("enabled")) {
Expand All @@ -165,7 +207,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C
if (userExisted && securityJsonNode.get("hash").asString() == null) {
// sanity check, this should usually not happen
final String hash = ((Hashed) internalUsersConfiguration.getCEntry(username)).getHash();
if (hash == null || hash.length() == 0) {
if (Strings.isNullOrEmpty(hash)) {
internalErrorResponse(
channel,
"Existing user " + username + " has no password, and no new password or hash was specified."
Expand Down Expand Up @@ -210,6 +252,7 @@ public void onResponse(IndexResponse response) {
* @param content The content of the request parsed into a node
* @throws IOException when parsing of configuration files fails (should not happen)
*/

samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
@Override
protected void handlePost(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException {

Expand All @@ -219,7 +262,7 @@ protected void handlePost(final RestChannel channel, RestRequest request, Client
filter(internalUsersConfiguration); // Hides hashes

// no specific resource requested
if (username == null || username.length() == 0) {
if (Strings.isNullOrEmpty(username)) {

notImplemented(channel, Method.POST);
return;
Expand Down Expand Up @@ -324,7 +367,8 @@ public Map<String, RequestContentValidator.DataType> allowedKeys() {
.put("opendistro_security_roles", DataType.ARRAY)
.put("hash", DataType.STRING)
.put("password", DataType.STRING)
.build();
.put("service", DataType.STRING)
.build();
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

Expand Down Expand Up @@ -285,7 +286,11 @@ public SecurityDynamicConfiguration<T> deepClone() {
@JsonIgnore
public void remove(String key) {
centries.remove(key);
}

@JsonIgnore
public void remove(List<String> keySet) {
keySet.stream().forEach(this::remove);
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
}

@SuppressWarnings({ "rawtypes", "unchecked" })
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/opensearch/security/user/AccountType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.opensearch.security.user;

public enum AccountType {
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved

INTERNAL("internal"),
SERVICE("service");
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved

private String name;

AccountType(String name){
this.name = name;
}

public String getName() {
return this.name;
}
public static AccountType fromString(String name) {
for (AccountType b : AccountType.values()) {
if (b.name.equalsIgnoreCase(name)) {
return b;
}
}
return null;
}
peternied marked this conversation as resolved.
Show resolved Hide resolved

}
48 changes: 48 additions & 0 deletions src/main/java/org/opensearch/security/user/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -44,6 +46,7 @@
import org.opensearch.security.securityconf.Hashed;
import org.opensearch.security.securityconf.impl.CType;
import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration;
import org.opensearch.security.securityconf.impl.v7.InternalUserV7;
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.security.support.SecurityJsonNode;
import org.passay.CharacterRule;
Expand Down Expand Up @@ -314,4 +317,49 @@ public static void saveAndUpdateConfigs(
throw ExceptionsHelper.convertToOpenSearchException(e);
}
}

/**
* Removes accounts that are not of the requested type from the SecurityDynamicConfiguration object passed.
*
* Accounts with the 'service' attribute set to true, are considered of type 'service'.
* Accounts with the 'service' attribute set to false or without the 'service' attribute, are considered of type 'internal'.
*
* @param configuration SecurityDynamicConfiguration object containing all accounts
* @param requestedAccountType The type of account to be kept. Should be "service" or "internal"
*
*/
public void includeAccountsIfType(SecurityDynamicConfiguration<?> configuration, AccountType requestedAccountType)
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
throws UserServiceException {
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
if (requestedAccountType != AccountType.INTERNAL && requestedAccountType != AccountType.SERVICE) {
throw new UserServiceException("Invalid user type {} " + requestedAccountType);
}
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
List<String> toBeRemoved = new ArrayList<>();

if (requestedAccountType == AccountType.SERVICE) {
for (Map.Entry<String, ?> entry : configuration.getCEntries().entrySet()) {
final InternalUserV7 internalUserEntry = (InternalUserV7) entry.getValue();
final Map accountAttributes = internalUserEntry.getAttributes();
final String accountName = entry.getKey();
final boolean isServiceAccount = Boolean.parseBoolean(accountAttributes.getOrDefault("service", "false").toString());

if (isServiceAccount == false) {
toBeRemoved.add(accountName);
}
}
}
else if (requestedAccountType == AccountType.INTERNAL) {

for (Map.Entry<String, ?> entry : configuration.getCEntries().entrySet()) {
final InternalUserV7 internalUserEntry = (InternalUserV7) entry.getValue();
final Map accountAttributes = internalUserEntry.getAttributes();
final String accountName = entry.getKey();
final boolean isServiceAccount = Boolean.parseBoolean(accountAttributes.getOrDefault("service", "false").toString());

if (isServiceAccount == true) {
toBeRemoved.add(accountName);
}
}
}
configuration.remove(toBeRemoved);
peternied marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@
import java.util.Base64;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.message.BasicHeader;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;

import org.opensearch.common.settings.Settings;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.security.DefaultObjectMapper;
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
import org.opensearch.security.dlic.rest.validation.RequestContentValidator;
import org.opensearch.security.securityconf.impl.CType;
Expand Down Expand Up @@ -55,7 +58,8 @@ protected String getEndpointPrefix() {

private static final String ENABLED_SERVICE_ACCOUNT_BODY = "{"
+ " \"attributes\": { \"service\": \"true\", "
+ "\"enabled\": \"true\"}"
+ " \"enabled \": \"true\"},"
+ " \"service\": \"true\" "
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
+ " }\n";

private static final String DISABLED_SERVICE_ACCOUNT_BODY = "{"
Expand Down Expand Up @@ -144,6 +148,57 @@ public void testParallelPutRequests() throws Exception {
deleteUser("test1");
}

@Test
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
public void testUserFilters() throws Exception {
setup();
rh.keystore = "restapi/kirk-keystore.jks";
rh.sendAdminCertificate = true;
final int SERVICE_ACCOUNTS_IN_SETTINGS = 1;
final int INTERNAL_ACCOUNTS_IN_SETTINGS = 19;
final String serviceAccountName = "JohnDoeService";
HttpResponse response;



response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=internal");

Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode());
JsonNode list = DefaultObjectMapper.readTree(response.getBody());
Assert.assertEquals(INTERNAL_ACCOUNTS_IN_SETTINGS, list.size());
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved

response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=service");
Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode());
list = DefaultObjectMapper.readTree(response.getBody());
assertThat(list, Matchers.emptyIterable());
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved

response = rh.executePutRequest(ENDPOINT + "/internalusers/" + serviceAccountName, ENABLED_SERVICE_ACCOUNT_BODY);


// repeat assertions after adding the service account

response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=internal");

Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode());
list = DefaultObjectMapper.readTree(response.getBody());
Assert.assertEquals(INTERNAL_ACCOUNTS_IN_SETTINGS, list.size());

response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=service");
Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode());
list = DefaultObjectMapper.readTree(response.getBody());
Assert.assertEquals(SERVICE_ACCOUNTS_IN_SETTINGS, list.size());
assertThat(response.findValueInJson(serviceAccountName), Matchers.notNullValue());
assertThat(response.findValueInJson(serviceAccountName + ".attributes.service"), containsString("true"));

response = rh.executeGetRequest(ENDPOINT + "/internalusers?filterBy=ssas");
Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode());

response = rh.executeGetRequest(ENDPOINT + "/internalusers?wrongparameter=jhondoe");
Assert.assertEquals(response.getBody(), HttpStatus.SC_BAD_REQUEST, response.getStatusCode());

response = rh.executePutRequest(ENDPOINT + "/internalusers", "{sample:value");
Assert.assertEquals(response.getBody(), HttpStatus.SC_METHOD_NOT_ALLOWED, response.getStatusCode());
}

@Test
public void testUserApi() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ private void throwIfNotValueNode(final JsonNode node) {
}
}

private JsonNode findObjectInJson(final String jsonDotPath) {
public JsonNode findObjectInJson(final String jsonDotPath) {
samuelcostae marked this conversation as resolved.
Show resolved Hide resolved
// Make sure its json / then parse it
if (!isJsonContentType()) {
throw new RuntimeException("Response was expected to be JSON, body was: \n" + body);
Expand Down