Skip to content

Commit b8dd909

Browse files
s-neltvernum
andauthored
Include user's privileges actions in IdP plugin _has_privileges request (#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]>
1 parent c292d7b commit b8dd909

File tree

5 files changed

+117
-11
lines changed

5 files changed

+117
-11
lines changed

docs/changelog/104026.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 104026
2+
summary: Include user's privileges actions in IdP plugin `_has_privileges` request
3+
area: IdentityProvider
4+
type: enhancement
5+
issues: []

x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import java.util.Map;
3131
import java.util.Set;
3232

33-
import static org.hamcrest.Matchers.contains;
33+
import static org.hamcrest.Matchers.containsInAnyOrder;
3434
import static org.hamcrest.Matchers.containsString;
3535
import static org.hamcrest.Matchers.equalTo;
3636
import static org.hamcrest.Matchers.hasSize;
@@ -185,8 +185,8 @@ private void authenticateWithSamlResponse(String samlResponse, @Nullable String
185185
equalTo("urn:oasis:names:tc:SAML:2.0:nameid-format:transient")
186186
);
187187
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), instanceOf(List.class));
188-
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), hasSize(1));
189-
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), contains("viewer"));
188+
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), hasSize(2));
189+
assertThat(ObjectPath.eval("metadata.saml_roles", authMap), containsInAnyOrder("viewer", "custom"));
190190
}
191191
}
192192

x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/resources/roles.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ idp_user:
88
applications:
99
- application: elastic-cloud
1010
resources: ["ec:123456:abcdefg"]
11-
privileges: ["sso:viewer"]
11+
privileges: ["sso:viewer", "sso:custom"]

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolver.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@
1010
import org.apache.logging.log4j.LogManager;
1111
import org.apache.logging.log4j.Logger;
1212
import org.elasticsearch.action.ActionListener;
13+
import org.elasticsearch.action.support.GroupedActionListener;
1314
import org.elasticsearch.client.internal.Client;
1415
import org.elasticsearch.common.Strings;
1516
import org.elasticsearch.xpack.core.security.SecurityContext;
17+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
18+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
19+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequestBuilder;
1620
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
1721
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
1822
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
1923
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
2024
import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
2125

26+
import java.util.Arrays;
2227
import java.util.Map;
2328
import java.util.Objects;
2429
import java.util.Set;
@@ -128,7 +133,8 @@ private void buildResourcePrivilege(
128133
ServiceProviderPrivileges service,
129134
ActionListener<RoleDescriptor.ApplicationResourcePrivileges> listener
130135
) {
131-
actionsResolver.getActions(service.getApplicationName(), listener.delegateFailureAndWrap((delegate, actions) -> {
136+
var groupedListener = new GroupedActionListener<Set<String>>(2, listener.delegateFailureAndWrap((delegate, actionSets) -> {
137+
final Set<String> actions = actionSets.stream().flatMap(Set::stream).collect(Collectors.toUnmodifiableSet());
132138
if (actions == null || actions.isEmpty()) {
133139
logger.warn("No application-privilege actions defined for application [{}]", service.getApplicationName());
134140
delegate.onResponse(null);
@@ -141,5 +147,24 @@ private void buildResourcePrivilege(
141147
delegate.onResponse(builder.build());
142148
}
143149
}));
150+
151+
// We need to enumerate possible actions that might be authorized for the user. Here we combine actions that
152+
// have been granted to the user via roles and other actions that are registered privileges for the given
153+
// application. These actions will be checked by a has-privileges check above
154+
final GetUserPrivilegesRequest request = new GetUserPrivilegesRequestBuilder(client).username(securityContext.getUser().principal())
155+
.request();
156+
client.execute(
157+
GetUserPrivilegesAction.INSTANCE,
158+
request,
159+
groupedListener.map(
160+
userPrivileges -> userPrivileges.getApplicationPrivileges()
161+
.stream()
162+
.filter(appPriv -> appPriv.getApplication().equals(service.getApplicationName()))
163+
.map(appPriv -> appPriv.getPrivileges())
164+
.flatMap(Arrays::stream)
165+
.collect(Collectors.toUnmodifiableSet())
166+
)
167+
);
168+
actionsResolver.getActions(service.getApplicationName(), groupedListener);
144169
}
145170
}

x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/privileges/UserPrivilegeResolverTests.java

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717
import org.elasticsearch.core.Tuple;
1818
import org.elasticsearch.test.ESTestCase;
1919
import org.elasticsearch.xpack.core.security.SecurityContext;
20+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
21+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
22+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
2023
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
2124
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
2225
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
2326
import org.elasticsearch.xpack.core.security.authc.Authentication;
2427
import org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper;
28+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
2529
import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
2630
import org.elasticsearch.xpack.core.security.user.User;
2731
import org.junit.Before;
28-
import org.mockito.Mockito;
2932

33+
import java.util.Arrays;
3034
import java.util.Collection;
3135
import java.util.HashMap;
3236
import java.util.Map;
@@ -50,11 +54,14 @@ public class UserPrivilegeResolverTests extends ESTestCase {
5054
private SecurityContext securityContext;
5155
private UserPrivilegeResolver resolver;
5256

57+
private String app;
58+
5359
@Before
5460
@SuppressWarnings("unchecked")
5561
public void setupTest() {
5662
client = mock(Client.class);
5763
securityContext = new SecurityContext(Settings.EMPTY, new ThreadContext(Settings.EMPTY));
64+
app = randomAlphaOfLengthBetween(3, 8);
5865
final ApplicationActionsResolver actionsResolver = mock(ApplicationActionsResolver.class);
5966
doAnswer(inv -> {
6067
final Object[] args = inv.getArguments();
@@ -63,12 +70,41 @@ public void setupTest() {
6370
listener.onResponse(Set.of("role:cluster:view", "role:cluster:admin", "role:cluster:operator", "role:cluster:monitor"));
6471
return null;
6572
}).when(actionsResolver).getActions(anyString(), any(ActionListener.class));
73+
doAnswer(inv -> {
74+
final Object[] args = inv.getArguments();
75+
assertThat(args, arrayWithSize(3));
76+
ActionListener<GetUserPrivilegesResponse> listener = (ActionListener<GetUserPrivilegesResponse>) args[args.length - 1];
77+
RoleDescriptor.ApplicationResourcePrivileges appPriv1 = RoleDescriptor.ApplicationResourcePrivileges.builder()
78+
.application(app)
79+
.resources("resource1")
80+
.privileges("role:extra1")
81+
.build();
82+
RoleDescriptor.ApplicationResourcePrivileges appPriv2 = RoleDescriptor.ApplicationResourcePrivileges.builder()
83+
.application(app)
84+
.resources("resource1")
85+
.privileges("role:extra2", "role:extra3")
86+
.build();
87+
RoleDescriptor.ApplicationResourcePrivileges discardedAppPriv = RoleDescriptor.ApplicationResourcePrivileges.builder()
88+
.application(randomAlphaOfLengthBetween(3, 8))
89+
.resources("resource1")
90+
.privileges("role:discarded")
91+
.build();
92+
GetUserPrivilegesResponse response = new GetUserPrivilegesResponse(
93+
Set.of(),
94+
Set.of(),
95+
Set.of(),
96+
Set.of(appPriv1, appPriv2, discardedAppPriv),
97+
Set.of(),
98+
Set.of()
99+
);
100+
listener.onResponse(response);
101+
return null;
102+
}).when(client).execute(same(GetUserPrivilegesAction.INSTANCE), any(GetUserPrivilegesRequest.class), any(ActionListener.class));
66103
resolver = new UserPrivilegeResolver(client, securityContext, actionsResolver);
67104
}
68105

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

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

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

144178
public void testResolveSsoWithMultipleRoles() throws Exception {
145179
final String username = randomAlphaOfLengthBetween(4, 12);
146-
final String app = randomAlphaOfLengthBetween(3, 8);
147180
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
148181
final String viewerAction = "role:cluster:view";
149182
final String adminAction = "role:cluster:admin";
@@ -183,6 +216,35 @@ public void testResolveSsoWithMultipleRoles() throws Exception {
183216
});
184217
}
185218

219+
public void testResolveSsoWithActionDefinedInUserPrivileges() throws Exception {
220+
final String username = randomAlphaOfLengthBetween(4, 12);
221+
final String resource = "cluster:" + MessageDigests.toHexString(randomByteArrayOfLength(16));
222+
final String actionInUserPrivs = "role:extra2";
223+
final String adminAction = "role:cluster:admin";
224+
225+
setupUser(username, () -> {
226+
setupHasPrivileges(username, app, access(resource, actionInUserPrivs, true), access(resource, adminAction, false));
227+
228+
final PlainActionFuture<UserPrivilegeResolver.UserPrivileges> future = new PlainActionFuture<>();
229+
final Function<String, Set<String>> roleMapping = Map.of(
230+
actionInUserPrivs,
231+
Set.of("extra2"),
232+
adminAction,
233+
Set.of("admin")
234+
)::get;
235+
resolver.resolve(service(app, resource, roleMapping), future);
236+
final UserPrivilegeResolver.UserPrivileges privileges;
237+
try {
238+
privileges = future.get();
239+
} catch (Exception e) {
240+
throw new RuntimeException(e);
241+
}
242+
assertThat(privileges.principal, equalTo(username));
243+
assertThat(privileges.hasAccess, equalTo(true));
244+
assertThat(privileges.roles, containsInAnyOrder("extra2"));
245+
});
246+
}
247+
186248
private ServiceProviderPrivileges service(String appName, String resource, Function<String, Set<String>> roleMapping) {
187249
return new ServiceProviderPrivileges(appName, resource, roleMapping);
188250
}
@@ -209,10 +271,24 @@ private HasPrivilegesResponse setupHasPrivileges(
209271
final Map<String, Collection<ResourcePrivileges>> appPrivs = Map.of(appName, privileges);
210272
final HasPrivilegesResponse response = new HasPrivilegesResponse(username, isCompleteMatch, Map.of(), Set.of(), appPrivs);
211273

212-
Mockito.doAnswer(inv -> {
274+
doAnswer(inv -> {
213275
final Object[] args = inv.getArguments();
214276
assertThat(args.length, equalTo(3));
215277
ActionListener<HasPrivilegesResponse> listener = (ActionListener<HasPrivilegesResponse>) args[args.length - 1];
278+
HasPrivilegesRequest request = (HasPrivilegesRequest) args[1];
279+
Set<String> gotPriviliges = Arrays.stream(request.applicationPrivileges())
280+
.flatMap(appPriv -> Arrays.stream(appPriv.getPrivileges()))
281+
.collect(Collectors.toUnmodifiableSet());
282+
Set<String> expectedPrivileges = Set.of(
283+
"role:cluster:view",
284+
"role:cluster:admin",
285+
"role:cluster:operator",
286+
"role:cluster:monitor",
287+
"role:extra1",
288+
"role:extra2",
289+
"role:extra3"
290+
);
291+
assertEquals(expectedPrivileges, gotPriviliges);
216292
listener.onResponse(response);
217293
return null;
218294
}).when(client).execute(same(HasPrivilegesAction.INSTANCE), any(HasPrivilegesRequest.class), any(ActionListener.class));

0 commit comments

Comments
 (0)