Skip to content

Commit

Permalink
Include user's privileges actions in IdP plugin _has_privileges req…
Browse files Browse the repository at this point in the history
…uest (#104026) (#105522)

* Include user's privileges actions in IdP plugin has privileges request

* Update docs/changelog/104026.yaml

* Use `GroupedActionListener` instead of nested listeners



* Fixes after applying review suggestion

* Fix IT flakiness

---------

Co-authored-by: Tim Vernum <[email protected]>
  • Loading branch information
s-nel and tvernum authored Feb 14, 2024
1 parent c292d7b commit b8dd909
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 11 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/104026.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 104026
summary: Include user's privileges actions in IdP plugin `_has_privileges` request
area: IdentityProvider
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import java.util.Map;
import java.util.Set;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
Expand Down Expand Up @@ -185,8 +185,8 @@ private void authenticateWithSamlResponse(String samlResponse, @Nullable String
equalTo("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")
);
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), instanceOf(List.class));
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), hasSize(1));
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), contains("viewer"));
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), hasSize(2));
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), containsInAnyOrder("viewer", "custom"));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ idp_user:
applications:
- application: elastic-cloud
resources: ["ec:123456:abcdefg"]
privileges: ["sso:viewer"]
privileges: ["sso:viewer", "sso:custom"]
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.GroupedActionListener;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.Strings;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequestBuilder;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;

import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -128,7 +133,8 @@ private void buildResourcePrivilege(
ServiceProviderPrivileges service,
ActionListener<RoleDescriptor.ApplicationResourcePrivileges> listener
) {
actionsResolver.getActions(service.getApplicationName(), listener.delegateFailureAndWrap((delegate, actions) -> {
var groupedListener = new GroupedActionListener<Set<String>>(2, listener.delegateFailureAndWrap((delegate, actionSets) -> {
final Set<String> actions = actionSets.stream().flatMap(Set::stream).collect(Collectors.toUnmodifiableSet());
if (actions == null || actions.isEmpty()) {
logger.warn("No application-privilege actions defined for application [{}]", service.getApplicationName());
delegate.onResponse(null);
Expand All @@ -141,5 +147,24 @@ private void buildResourcePrivilege(
delegate.onResponse(builder.build());
}
}));

// We need to enumerate possible actions that might be authorized for the user. Here we combine actions that
// have been granted to the user via roles and other actions that are registered privileges for the given
// application. These actions will be checked by a has-privileges check above
final GetUserPrivilegesRequest request = new GetUserPrivilegesRequestBuilder(client).username(securityContext.getUser().principal())
.request();
client.execute(
GetUserPrivilegesAction.INSTANCE,
request,
groupedListener.map(
userPrivileges -> userPrivileges.getApplicationPrivileges()
.stream()
.filter(appPriv -> appPriv.getApplication().equals(service.getApplicationName()))
.map(appPriv -> appPriv.getPrivileges())
.flatMap(Arrays::stream)
.collect(Collectors.toUnmodifiableSet())
)
);
actionsResolver.getActions(service.getApplicationName(), groupedListener);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@
import org.elasticsearch.core.Tuple;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
import org.elasticsearch.xpack.core.security.user.User;
import org.junit.Before;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -50,11 +54,14 @@ public class UserPrivilegeResolverTests extends ESTestCase {
private SecurityContext securityContext;
private UserPrivilegeResolver resolver;

private String app;

@Before
@SuppressWarnings("unchecked")
public void setupTest() {
client = mock(Client.class);
securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY));
app = randomAlphaOfLengthBetween(3, 8);
final ApplicationActionsResolver actionsResolver = mock(ApplicationActionsResolver.class);
doAnswer(inv -> {
final Object[] args = inv.getArguments();
Expand All @@ -63,12 +70,41 @@ public void setupTest() {
listener.onResponse(Set.of("role:cluster:view", "role:cluster:admin", "role:cluster:operator", "role:cluster:monitor"));
return null;
}).when(actionsResolver).getActions(anyString(), any(ActionListener.class));
doAnswer(inv -> {
final Object[] args = inv.getArguments();
assertThat(args, arrayWithSize(3));
ActionListener<GetUserPrivilegesResponse> listener = (ActionListener<GetUserPrivilegesResponse>) args[args.length - 1];
RoleDescriptor.ApplicationResourcePrivileges appPriv1 = RoleDescriptor.ApplicationResourcePrivileges.builder()
.application(app)
.resources("resource1")
.privileges("role:extra1")
.build();
RoleDescriptor.ApplicationResourcePrivileges appPriv2 = RoleDescriptor.ApplicationResourcePrivileges.builder()
.application(app)
.resources("resource1")
.privileges("role:extra2", "role:extra3")
.build();
RoleDescriptor.ApplicationResourcePrivileges discardedAppPriv = RoleDescriptor.ApplicationResourcePrivileges.builder()
.application(randomAlphaOfLengthBetween(3, 8))
.resources("resource1")
.privileges("role:discarded")
.build();
GetUserPrivilegesResponse response = new GetUserPrivilegesResponse(
Set.of(),
Set.of(),
Set.of(),
Set.of(appPriv1, appPriv2, discardedAppPriv),
Set.of(),
Set.of()
);
listener.onResponse(response);
return null;
}).when(client).execute(same(GetUserPrivilegesAction.INSTANCE), any(GetUserPrivilegesRequest.class), any(ActionListener.class));
resolver = new UserPrivilegeResolver(client, securityContext, actionsResolver);
}

public void testResolveZeroAccess() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
final String app = randomAlphaOfLengthBetween(3, 8);
setupUser(username, () -> {
setupHasPrivileges(username, app);
final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
Expand All @@ -93,7 +129,6 @@ public void testResolveZeroAccess() throws Exception {

public void testResolveSsoWithNoRoleAccess() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
final String app = randomAlphaOfLengthBetween(3, 8);
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
final String viewerAction = "role:cluster:view";
final String adminAction = "role:cluster:admin";
Expand All @@ -118,7 +153,6 @@ public void testResolveSsoWithNoRoleAccess() throws Exception {

public void testResolveSsoWithSingleRole() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
final String app = randomAlphaOfLengthBetween(3, 8);
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
final String viewerAction = "role:cluster:view";
final String adminAction = "role:cluster:admin";
Expand All @@ -143,7 +177,6 @@ public void testResolveSsoWithSingleRole() throws Exception {

public void testResolveSsoWithMultipleRoles() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
final String app = randomAlphaOfLengthBetween(3, 8);
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
final String viewerAction = "role:cluster:view";
final String adminAction = "role:cluster:admin";
Expand Down Expand Up @@ -183,6 +216,35 @@ public void testResolveSsoWithMultipleRoles() throws Exception {
});
}

public void testResolveSsoWithActionDefinedInUserPrivileges() throws Exception {
final String username = randomAlphaOfLengthBetween(4, 12);
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
final String actionInUserPrivs = "role:extra2";
final String adminAction = "role:cluster:admin";

setupUser(username, () -> {
setupHasPrivileges(username, app, access(resource, actionInUserPrivs, true), access(resource, adminAction, false));

final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
final Function<String, Set<String>> roleMapping = Map.of(
actionInUserPrivs,
Set.of("extra2"),
adminAction,
Set.of("admin")
)::get;
resolver.resolve(service(app, resource, roleMapping), future);
final UserPrivilegeResolver.UserPrivileges privileges;
try {
privileges = future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
assertThat(privileges.principal, equalTo(username));
assertThat(privileges.hasAccess, equalTo(true));
assertThat(privileges.roles, containsInAnyOrder("extra2"));
});
}

private ServiceProviderPrivileges service(String appName, String resource, Function<String, Set<String>> roleMapping) {
return new ServiceProviderPrivileges(appName, resource, roleMapping);
}
Expand All @@ -209,10 +271,24 @@ private HasPrivilegesResponse setupHasPrivileges(
final Map<String, Collection<ResourcePrivileges>> appPrivs = Map.of(appName, privileges);
final HasPrivilegesResponse response = new HasPrivilegesResponse(username, isCompleteMatch, Map.of(), Set.of(), appPrivs);

Mockito.doAnswer(inv -> {
doAnswer(inv -> {
final Object[] args = inv.getArguments();
assertThat(args.length, equalTo(3));
ActionListener<HasPrivilegesResponse> listener = (ActionListener<HasPrivilegesResponse>) args[args.length - 1];
HasPrivilegesRequest request = (HasPrivilegesRequest) args[1];
Set<String> gotPriviliges = Arrays.stream(request.applicationPrivileges())
.flatMap(appPriv -> Arrays.stream(appPriv.getPrivileges()))
.collect(Collectors.toUnmodifiableSet());
Set<String> expectedPrivileges = Set.of(
"role:cluster:view",
"role:cluster:admin",
"role:cluster:operator",
"role:cluster:monitor",
"role:extra1",
"role:extra2",
"role:extra3"
);
assertEquals(expectedPrivileges, gotPriviliges);
listener.onResponse(response);
return null;
}).when(client).execute(same(HasPrivilegesAction.INSTANCE), any(HasPrivilegesRequest.class), any(ActionListener.class));
Expand Down

0 comments on commit b8dd909

Please sign in to comment.