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

Limit service account permission #3607

Merged
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a1a9c50
Limit Service Accounts permissions
RyanL1997 Oct 26, 2023
37a0954
Correct the key check for service account attribute
RyanL1997 Oct 26, 2023
57bf240
Refactor the cluster permission filter of service accounts
RyanL1997 Oct 26, 2023
a167919
Add filter for service account access to non system indices
RyanL1997 Oct 26, 2023
b421ac8
Fix import
RyanL1997 Oct 26, 2023
81aa19a
Fix testSpecialUsernames() by make the test account non-service account
RyanL1997 Oct 26, 2023
4142890
Refactor isServiceAccount() into User class
RyanL1997 Oct 26, 2023
e0ce30d
Update the accounts number in UserServiceUnitTests
RyanL1997 Oct 27, 2023
ccc3bea
Minor fix of some formatting
RyanL1997 Oct 27, 2023
691b347
Remove the unused example
RyanL1997 Oct 27, 2023
6d0a820
Initial setup of service account testing env
RyanL1997 Oct 27, 2023
8ecb683
Add indices test case
RyanL1997 Oct 28, 2023
1dc296b
Fix lint
RyanL1997 Oct 28, 2023
ac9416e
Filter out the regular index when it is a mixed request with both sys…
RyanL1997 Oct 28, 2023
0853252
Finish the tests
RyanL1997 Oct 29, 2023
ec67ff2
Fix some comments
RyanL1997 Oct 30, 2023
1a811b1
Remove object mapper from the test case
RyanL1997 Oct 30, 2023
c697f95
Apply getName()
RyanL1997 Oct 30, 2023
fcf91eb
Fix some comments
RyanL1997 Oct 30, 2023
4e0c3ad
Fix lint
RyanL1997 Oct 30, 2023
3a22b79
Switch to reject the request, if this is a mixed situation
RyanL1997 Oct 30, 2023
2323a4a
Refactor
RyanL1997 Oct 30, 2023
0a6bd2d
Fix lint
RyanL1997 Oct 30, 2023
0c268bf
Fix some comments
RyanL1997 Oct 31, 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
@@ -0,0 +1,148 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

package org.opensearch.security.http;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.apache.hc.core5.http.HttpStatus;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.opensearch.test.framework.TestIndex;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNotNull;
import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY;
import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED;
import static org.opensearch.security.support.ConfigConstants.SECURITY_SYSTEM_INDICES_KEY;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class ServiceAccountAuthenticationTest {

public static final String DEFAULT_PASSWORD = "secret";

public static final String SERVICE_ATTRIBUTE = "service";

static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);

// CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings
public static final String SERVICE_ACCOUNT_USER_NAME = "admin-extension";
// CS-ENFORCE-SINGLE
RyanL1997 marked this conversation as resolved.
Show resolved Hide resolved

static final TestSecurityConfig.User SERVICE_ACCOUNT_ADMIN_USER = new TestSecurityConfig.User(SERVICE_ACCOUNT_USER_NAME).attr(
SERVICE_ATTRIBUTE,
"true"
)
.roles(
// CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings
new TestSecurityConfig.Role("admin-extension-role").clusterPermissions("*")
// CS-ENFORCE-SINGLE
.indexPermissions("*", "system:admin/system_index")
.on("*")
);

private static final TestIndex TEST_NON_SYS_INDEX = TestIndex.name("test-non-sys-index")
.setting("index.number_of_shards", 1)
.setting("index.number_of_replicas", 0)
.build();

private static final TestIndex TEST_SYS_INDEX = TestIndex.name("test-sys-index")
.setting("index.number_of_shards", 1)
.setting("index.number_of_replicas", 0)
.build();

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
.anonymousAuth(false)
.users(ADMIN_USER, SERVICE_ACCOUNT_ADMIN_USER)
.nodeSettings(
Map.of(
SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY,
true,
SECURITY_SYSTEM_INDICES_ENABLED_KEY,
true,
SECURITY_RESTAPI_ROLES_ENABLED,
List.of("user_admin__all_access"),
SECURITY_SYSTEM_INDICES_KEY,
List.of(TEST_SYS_INDEX.getName())
)
)
.authc(AUTHC_HTTPBASIC_INTERNAL)
.indices(TEST_NON_SYS_INDEX, TEST_SYS_INDEX)
.build();

@Test
public void testClusterHealthWithServiceAccountCred() {
try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_USER_NAME, DEFAULT_PASSWORD)) {
RyanL1997 marked this conversation as resolved.
Show resolved Hide resolved
client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME);
TestRestClient.HttpResponse response = client.get("_cluster/health");
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);

String responseBody = response.getBody();

assertNotNull("Response body should not be null", responseBody);
assertTrue(responseBody.contains("\"type\":\"security_exception\""));
}
}

@Test
public void testReadSysIndexWithServiceAccountCred() {
try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_USER_NAME, DEFAULT_PASSWORD)) {
client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME);
TestRestClient.HttpResponse response = client.get(TEST_SYS_INDEX.getName());
response.assertStatusCode(HttpStatus.SC_OK);

String responseBody = response.getBody();

assertNotNull("Response body should not be null", responseBody);
assertTrue(responseBody.contains(TEST_SYS_INDEX.getName()));
}
}

@Test
public void testReadNonSysIndexWithServiceAccountCred() {
try (TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_USER_NAME, DEFAULT_PASSWORD)) {
client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME);
TestRestClient.HttpResponse response = client.get(TEST_NON_SYS_INDEX.getName());
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);

String responseBody = response.getBody();

assertNotNull("Response body should not be null", responseBody);
assertTrue(responseBody.contains("\"type\":\"security_exception\""));
}
}

@Test
public void testReadBothWithServiceAccountCred() {
TestRestClient client = cluster.getRestClient(SERVICE_ACCOUNT_USER_NAME, DEFAULT_PASSWORD);
client.confirmCorrectCredentials(SERVICE_ACCOUNT_USER_NAME);
TestRestClient.HttpResponse response = client.get((TEST_SYS_INDEX.getName() + "," + TEST_NON_SYS_INDEX.getName()));
response.assertStatusCode(HttpStatus.SC_FORBIDDEN);

String responseBody = response.getBody();

assertNotNull("Response body should not be null", responseBody);
assertTrue(responseBody.contains("\"type\":\"security_exception\""));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,15 @@ public PrivilegesEvaluatorResponse evaluate(
namedXContentRegistry
);

final boolean serviceAccountUser = user.isServiceAccount();
if (isClusterPerm(action0)) {
if (serviceAccountUser) {
presponse.missingPrivileges.add(action0);
presponse.allowed = false;
log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0);
return presponse;
}

if (!securityRoles.impliesClusterPermissionPermission(action0)) {
presponse.missingPrivileges.add(action0);
presponse.allowed = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ private List<String> getAllProtectedSystemIndices(final Resolved requestedResolv
return new ArrayList<>(superAdminAccessOnlyIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList()));
}

/**
* Checks if the request contains any regular (non-system and non-protected) indices.
* Regular indices are those that are not categorized as system indices or protected system indices.
* This method helps in identifying requests that might be accessing regular indices alongside system indices.
* @param requestedResolved The resolved object of the request, which contains the list of indices from the original request.
* @return true if the request contains any regular indices, false otherwise.
*/
private boolean requestContainsAnyRegularIndices(final Resolved requestedResolved) {
Set<String> allIndices = requestedResolved.getAllIndices();

List<String> allSystemIndices = getAllSystemIndices(requestedResolved);
List<String> allProtectedSystemIndices = getAllProtectedSystemIndices(requestedResolved);

return allIndices.stream().anyMatch(index -> !allSystemIndices.contains(index) && !allProtectedSystemIndices.contains(index));
}

/**
* Is the current action allowed to be performed on security index
* @param action request action on security index
Expand Down Expand Up @@ -233,8 +249,35 @@ private void evaluateSystemIndicesAccess(
) {
// Perform access check is system index permissions are enabled
boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved);
boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved);
boolean serviceAccountUser = user.isServiceAccount();

if (isSystemIndexPermissionEnabled) {
if (serviceAccountUser) {
if (!containsSystemIndex && containsRegularIndex) {
auditLog.logSecurityIndexAttempt(request, action, task);
if (log.isInfoEnabled()) {
log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles);
}
presponse.allowed = false;
presponse.markComplete();
return;
} else if (containsSystemIndex && containsRegularIndex) {
if (log.isDebugEnabled()) {
List<String> regularIndices = requestedResolved.getAllIndices()
.stream()
.filter(
index -> !getAllSystemIndices(requestedResolved).contains(index)
&& !getAllProtectedSystemIndices(requestedResolved).contains(index)
)
.collect(Collectors.toList());
log.debug("Service account cannot access regular indices: {}", regularIndices);
}
presponse.allowed = false;
presponse.markComplete();
return;
}
}
RyanL1997 marked this conversation as resolved.
Show resolved Hide resolved
boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved);
if (containsProtectedIndex) {
auditLog.logSecurityIndexAttempt(request, action, task);
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/opensearch/security/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,14 @@ public final Set<String> getSecurityRoles() {
? Collections.synchronizedSet(Collections.emptySet())
: Collections.unmodifiableSet(this.securityRoles);
}

/**
* Check the custom attributes associated with this user
*
* @return true if it has a service account attributes. otherwise false
*/
public boolean isServiceAccount() {
Map<String, String> userAttributesMap = this.getCustomAttributesMap();
return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ public void testSpecialUsernames() throws Exception {
setup();
RestHelper rh = nonSslRestHelper();

Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("bug.99", "nagilum")).getStatusCode());
RyanL1997 marked this conversation as resolved.
Show resolved Hide resolved
Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("bug.88", "nagilum")).getStatusCode());
Assert.assertEquals(HttpStatus.SC_UNAUTHORIZED, rh.executeGetRequest("", encodeBasicHeader("a", "b")).getStatusCode());
Assert.assertEquals(
HttpStatus.SC_OK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class UserServiceUnitTests {
UserService userService;

final int SERVICE_ACCOUNTS_IN_SETTINGS = 1;
final int INTERNAL_ACCOUNTS_IN_SETTINGS = 66;
final int INTERNAL_ACCOUNTS_IN_SETTINGS = 67;
String serviceAccountUsername = "bug.99";
String internalAccountUsername = "sarek";

Expand Down
7 changes: 7 additions & 0 deletions src/test/resources/internal_users.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
_meta:
type: "internalusers"
config_version: 2
bug.88:
hash: "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m"
reserved: false
hidden: false
backend_roles: []
attributes: {}
description: "Migrated from v6"
bug.99:
hash: "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m"
reserved: false
Expand Down
Loading