Skip to content

Commit 34aa55a

Browse files
authored
Authorization engines evaluate privileges for APIs (#38219)
This commit moves the evaluation of privileges from a few transport actions into the authorization engine. The APIs are used by other applications for making decisions and if a different authorization engine is used that is not role based, we should still allow these APIs to work. By moving this evaluation out of the transport action, the transport actions no longer have a dependency on roles.
1 parent 54d7b4c commit 34aa55a

File tree

10 files changed

+967
-851
lines changed

10 files changed

+967
-851
lines changed

plugins/examples/security-authorization-engine/src/main/java/org/elasticsearch/example/CustomAuthorizationEngine.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,35 @@
2121

2222
import org.elasticsearch.action.ActionListener;
2323
import org.elasticsearch.cluster.metadata.AliasOrIndex;
24+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
25+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
26+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse.Indices;
27+
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
28+
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
29+
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse.ResourcePrivileges;
2430
import org.elasticsearch.xpack.core.security.authc.Authentication;
2531
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
2632
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
33+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
34+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;
2735
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
2836
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl.IndexAccessControl;
2937
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
38+
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
39+
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
3040
import org.elasticsearch.xpack.core.security.user.User;
3141

3242
import java.util.ArrayList;
3343
import java.util.Arrays;
44+
import java.util.Collection;
3445
import java.util.Collections;
3546
import java.util.HashMap;
47+
import java.util.LinkedHashMap;
3648
import java.util.List;
3749
import java.util.Map;
50+
import java.util.Set;
3851
import java.util.function.Function;
52+
import java.util.stream.Collectors;
3953

4054
/**
4155
* A custom implementation of an authorization engine. This engine is extremely basic in that it
@@ -115,6 +129,89 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza
115129
}
116130
}
117131

132+
@Override
133+
public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo,
134+
HasPrivilegesRequest hasPrivilegesRequest,
135+
Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
136+
ActionListener<HasPrivilegesResponse> listener) {
137+
if (isSuperuser(authentication.getUser())) {
138+
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, true));
139+
} else {
140+
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, false));
141+
}
142+
}
143+
144+
@Override
145+
public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request,
146+
ActionListener<GetUserPrivilegesResponse> listener) {
147+
if (isSuperuser(authentication.getUser())) {
148+
listener.onResponse(getUserPrivilegesResponse(true));
149+
} else {
150+
listener.onResponse(getUserPrivilegesResponse(false));
151+
}
152+
}
153+
154+
private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentication, HasPrivilegesRequest hasPrivilegesRequest,
155+
boolean authorized) {
156+
Map<String, Boolean> clusterPrivMap = new HashMap<>();
157+
for (String clusterPriv : hasPrivilegesRequest.clusterPrivileges()) {
158+
clusterPrivMap.put(clusterPriv, authorized);
159+
}
160+
final Map<String, ResourcePrivileges> indices = new LinkedHashMap<>();
161+
for (IndicesPrivileges check : hasPrivilegesRequest.indexPrivileges()) {
162+
for (String index : check.getIndices()) {
163+
final Map<String, Boolean> privileges = new HashMap<>();
164+
final HasPrivilegesResponse.ResourcePrivileges existing = indices.get(index);
165+
if (existing != null) {
166+
privileges.putAll(existing.getPrivileges());
167+
}
168+
for (String privilege : check.getPrivileges()) {
169+
privileges.put(privilege, authorized);
170+
}
171+
indices.put(index, new ResourcePrivileges(index, privileges));
172+
}
173+
}
174+
final Map<String, Collection<ResourcePrivileges>> privilegesByApplication = new HashMap<>();
175+
Set<String> applicationNames = Arrays.stream(hasPrivilegesRequest.applicationPrivileges())
176+
.map(RoleDescriptor.ApplicationResourcePrivileges::getApplication)
177+
.collect(Collectors.toSet());
178+
for (String applicationName : applicationNames) {
179+
final Map<String, HasPrivilegesResponse.ResourcePrivileges> appPrivilegesByResource = new LinkedHashMap<>();
180+
for (RoleDescriptor.ApplicationResourcePrivileges p : hasPrivilegesRequest.applicationPrivileges()) {
181+
if (applicationName.equals(p.getApplication())) {
182+
for (String resource : p.getResources()) {
183+
final Map<String, Boolean> privileges = new HashMap<>();
184+
final HasPrivilegesResponse.ResourcePrivileges existing = appPrivilegesByResource.get(resource);
185+
if (existing != null) {
186+
privileges.putAll(existing.getPrivileges());
187+
}
188+
for (String privilege : p.getPrivileges()) {
189+
privileges.put(privilege, authorized);
190+
}
191+
appPrivilegesByResource.put(resource, new HasPrivilegesResponse.ResourcePrivileges(resource, privileges));
192+
}
193+
}
194+
}
195+
privilegesByApplication.put(applicationName, appPrivilegesByResource.values());
196+
}
197+
return new HasPrivilegesResponse(authentication.getUser().principal(), authorized, clusterPrivMap, indices.values(),
198+
privilegesByApplication);
199+
}
200+
201+
private GetUserPrivilegesResponse getUserPrivilegesResponse(boolean isSuperuser) {
202+
final Set<String> cluster = isSuperuser ? Collections.singleton("ALL") : Collections.emptySet();
203+
final Set<ConditionalClusterPrivilege> conditionalCluster = Collections.emptySet();
204+
final Set<GetUserPrivilegesResponse.Indices> indices = isSuperuser ? Collections.singleton(new Indices(Collections.singleton("*"),
205+
Collections.singleton("*"), Collections.emptySet(), Collections.emptySet(), true)) : Collections.emptySet();
206+
207+
final Set<RoleDescriptor.ApplicationResourcePrivileges> application = isSuperuser ?
208+
Collections.singleton(
209+
RoleDescriptor.ApplicationResourcePrivileges.builder().application("*").privileges("*").resources("*").build()) :
210+
Collections.emptySet();
211+
final Set<String> runAs = isSuperuser ? Collections.singleton("*") : Collections.emptySet();
212+
return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs);
213+
}
214+
118215
public static class CustomAuthorizationInfo implements AuthorizationInfo {
119216

120217
private final String[] roles;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.cluster.metadata.AliasOrIndex;
1111
import org.elasticsearch.transport.TransportRequest;
12+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
13+
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
14+
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
15+
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
1216
import org.elasticsearch.xpack.core.security.authc.Authentication;
1317
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
18+
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
1419
import org.elasticsearch.xpack.core.security.user.User;
1520

21+
import java.util.Collection;
1622
import java.util.Collections;
1723
import java.util.List;
1824
import java.util.Map;
@@ -141,6 +147,7 @@ void authorizeIndexAction(RequestInfo requestInfo, AuthorizationInfo authorizati
141147
void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizationInfo,
142148
Map<String, AliasOrIndex> aliasAndIndexLookup, ActionListener<List<String>> listener);
143149

150+
144151
/**
145152
* Asynchronously checks that the permissions a user would have for a given list of names do
146153
* not exceed their permissions for a given name. This is used to ensure that a user cannot
@@ -161,6 +168,35 @@ void loadAuthorizedIndices(RequestInfo requestInfo, AuthorizationInfo authorizat
161168
void validateIndexPermissionsAreSubset(RequestInfo requestInfo, AuthorizationInfo authorizationInfo,
162169
Map<String, List<String>> indexNameToNewNames, ActionListener<AuthorizationResult> listener);
163170

171+
/**
172+
* Checks the current user's privileges against those that being requested to check in the
173+
* request. This provides a way for an application to ask if a user has permission to perform
174+
* an action or if they have permissions to an application resource.
175+
*
176+
* @param authentication the authentication that is associated with this request
177+
* @param authorizationInfo information needed from authorization that was previously retrieved
178+
* from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)}
179+
* @param hasPrivilegesRequest the request that contains the privileges to check for the user
180+
* @param applicationPrivilegeDescriptors a collection of application privilege descriptors
181+
* @param listener the listener to be notified of the has privileges response
182+
*/
183+
void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, HasPrivilegesRequest hasPrivilegesRequest,
184+
Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
185+
ActionListener<HasPrivilegesResponse> listener);
186+
187+
/**
188+
* Retrieve's the current user's privileges in a standard format that can be rendered via an
189+
* API for an application to understand the privileges that the current user has.
190+
*
191+
* @param authentication the authentication that is associated with this request
192+
* @param authorizationInfo information needed from authorization that was previously retrieved
193+
* from {@link #resolveAuthorizationInfo(RequestInfo, ActionListener)}
194+
* @param request the request for retrieving the user's privileges
195+
* @param listener the listener to be notified of the has privileges response
196+
*/
197+
void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request,
198+
ActionListener<GetUserPrivilegesResponse> listener);
199+
164200
/**
165201
* Interface for objects that contains the information needed to authorize a request
166202
*/

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportGetUserPrivilegesAction.java

Lines changed: 7 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,132 +5,47 @@
55
*/
66
package org.elasticsearch.xpack.security.action.user;
77

8-
import org.apache.logging.log4j.message.ParameterizedMessage;
9-
import org.apache.lucene.util.automaton.Operations;
108
import org.elasticsearch.action.ActionListener;
119
import org.elasticsearch.action.support.ActionFilters;
1210
import org.elasticsearch.action.support.HandledTransportAction;
13-
import org.elasticsearch.common.bytes.BytesReference;
14-
import org.elasticsearch.common.collect.Tuple;
1511
import org.elasticsearch.common.inject.Inject;
16-
import org.elasticsearch.common.settings.Settings;
1712
import org.elasticsearch.tasks.Task;
1813
import org.elasticsearch.threadpool.ThreadPool;
1914
import org.elasticsearch.transport.TransportService;
2015
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction;
2116
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
2217
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
2318
import org.elasticsearch.xpack.core.security.authc.Authentication;
24-
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
25-
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsCache;
26-
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition;
27-
import org.elasticsearch.xpack.core.security.authz.permission.IndicesPermission;
28-
import org.elasticsearch.xpack.core.security.authz.permission.Role;
29-
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
30-
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
31-
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
32-
import org.elasticsearch.xpack.core.security.authz.privilege.Privilege;
3319
import org.elasticsearch.xpack.core.security.user.User;
34-
import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore;
35-
36-
import java.util.Arrays;
37-
import java.util.Collections;
38-
import java.util.HashSet;
39-
import java.util.LinkedHashSet;
40-
import java.util.Set;
41-
import java.util.TreeSet;
42-
43-
import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString;
20+
import org.elasticsearch.xpack.security.authz.AuthorizationService;
4421

4522
/**
4623
* Transport action for {@link GetUserPrivilegesAction}
4724
*/
4825
public class TransportGetUserPrivilegesAction extends HandledTransportAction<GetUserPrivilegesRequest, GetUserPrivilegesResponse> {
4926

5027
private final ThreadPool threadPool;
51-
private final CompositeRolesStore rolesStore;
28+
private final AuthorizationService authorizationService;
5229

5330
@Inject
5431
public TransportGetUserPrivilegesAction(ThreadPool threadPool, TransportService transportService,
55-
ActionFilters actionFilters, CompositeRolesStore rolesStore) {
32+
ActionFilters actionFilters, AuthorizationService authorizationService) {
5633
super(GetUserPrivilegesAction.NAME, transportService, actionFilters, GetUserPrivilegesRequest::new);
5734
this.threadPool = threadPool;
58-
this.rolesStore = rolesStore;
35+
this.authorizationService = authorizationService;
5936
}
6037

6138
@Override
6239
protected void doExecute(Task task, GetUserPrivilegesRequest request, ActionListener<GetUserPrivilegesResponse> listener) {
6340
final String username = request.username();
6441

65-
final User user = Authentication.getAuthentication(threadPool.getThreadContext()).getUser();
42+
final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext());
43+
final User user = authentication.getUser();
6644
if (user.principal().equals(username) == false) {
6745
listener.onFailure(new IllegalArgumentException("users may only list the privileges of their own account"));
6846
return;
6947
}
7048

71-
// FIXME reuse field permissions cache!
72-
rolesStore.getRoles(user, new FieldPermissionsCache(Settings.EMPTY), ActionListener.wrap(
73-
role -> listener.onResponse(buildResponseObject(role)),
74-
listener::onFailure));
75-
}
76-
77-
// package protected for testing
78-
GetUserPrivilegesResponse buildResponseObject(Role userRole) {
79-
logger.trace(() -> new ParameterizedMessage("List privileges for role [{}]", arrayToCommaDelimitedString(userRole.names())));
80-
81-
// We use sorted sets for Strings because they will typically be small, and having a predictable order allows for simpler testing
82-
final Set<String> cluster = new TreeSet<>();
83-
// But we don't have a meaningful ordering for objects like ConditionalClusterPrivilege, so the tests work with "random" ordering
84-
final Set<ConditionalClusterPrivilege> conditionalCluster = new HashSet<>();
85-
for (Tuple<ClusterPrivilege, ConditionalClusterPrivilege> tup : userRole.cluster().privileges()) {
86-
if (tup.v2() == null) {
87-
if (ClusterPrivilege.NONE.equals(tup.v1()) == false) {
88-
cluster.addAll(tup.v1().name());
89-
}
90-
} else {
91-
conditionalCluster.add(tup.v2());
92-
}
93-
}
94-
95-
final Set<GetUserPrivilegesResponse.Indices> indices = new LinkedHashSet<>();
96-
for (IndicesPermission.Group group : userRole.indices().groups()) {
97-
final Set<BytesReference> queries = group.getQuery() == null ? Collections.emptySet() : group.getQuery();
98-
final Set<FieldPermissionsDefinition.FieldGrantExcludeGroup> fieldSecurity = group.getFieldPermissions().hasFieldLevelSecurity()
99-
? group.getFieldPermissions().getFieldPermissionsDefinition().getFieldGrantExcludeGroups() : Collections.emptySet();
100-
indices.add(new GetUserPrivilegesResponse.Indices(
101-
Arrays.asList(group.indices()),
102-
group.privilege().name(),
103-
fieldSecurity,
104-
queries,
105-
group.allowRestrictedIndices()
106-
));
107-
}
108-
109-
final Set<RoleDescriptor.ApplicationResourcePrivileges> application = new LinkedHashSet<>();
110-
for (String applicationName : userRole.application().getApplicationNames()) {
111-
for (ApplicationPrivilege privilege : userRole.application().getPrivileges(applicationName)) {
112-
final Set<String> resources = userRole.application().getResourcePatterns(privilege);
113-
if (resources.isEmpty()) {
114-
logger.trace("No resources defined in application privilege {}", privilege);
115-
} else {
116-
application.add(RoleDescriptor.ApplicationResourcePrivileges.builder()
117-
.application(applicationName)
118-
.privileges(privilege.name())
119-
.resources(resources)
120-
.build());
121-
}
122-
}
123-
}
124-
125-
final Privilege runAsPrivilege = userRole.runAs().getPrivilege();
126-
final Set<String> runAs;
127-
if (Operations.isEmpty(runAsPrivilege.getAutomaton())) {
128-
runAs = Collections.emptySet();
129-
} else {
130-
runAs = runAsPrivilege.name();
131-
}
132-
133-
return new GetUserPrivilegesResponse(cluster, conditionalCluster, indices, application, runAs);
49+
authorizationService.retrieveUserPrivileges(authentication, request, listener);
13450
}
135-
13651
}

0 commit comments

Comments
 (0)