Skip to content

Commit

Permalink
Has privileges API for profiles (#85898)
Browse files Browse the repository at this point in the history
This introduces a new Security API `_security/profile/_has_privileges`
that can be used to verify which Users have the requested privileges,
given their associated User Profiles. Multiple profile uids can be specified
in a single has privileges request.

This is analogous to the existing Has privileges API. It also uses the same
format for specifying the privileges to be checked, and should be used in
the same situations (ie to run an authorization preflight check or to verify
privileges over application resources). However, unlike the existing
has privilege API, this can be used to check the privileges of multiple
users (not only of the currently authenticated one), but the users must
have an existing profile, and the response is binary only (either it has or
it does not have the requested privileges).
Calling this API requires the `manage_user_profile` cluster privilege.
  • Loading branch information
albertzaharovits committed May 6, 2022
1 parent 1bc90ea commit 3d4234e
Show file tree
Hide file tree
Showing 36 changed files with 1,969 additions and 588 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/85898.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 85898
summary: Has privileges API for profiles
area: Authorization
type: feature
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.metadata.IndexAbstraction;
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.GetUserPrivilegesResponse.Indices;
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.Subject;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesToCheck;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.PrivilegesCheckResult;
import org.elasticsearch.xpack.core.security.authz.ResolvedIndices;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;
Expand Down Expand Up @@ -56,6 +56,11 @@ public void resolveAuthorizationInfo(RequestInfo requestInfo, ActionListener<Aut
}
}

@Override
public void resolveAuthorizationInfo(Subject subject, ActionListener<AuthorizationInfo> listener) {
listener.onResponse(new CustomAuthorizationInfo(subject.getUser().roles(), null));
}

@Override
public void authorizeRunAs(RequestInfo requestInfo, AuthorizationInfo authorizationInfo, ActionListener<AuthorizationResult> listener) {
if (isSuperuser(requestInfo.getAuthentication().getAuthenticatingSubject().getUser())) {
Expand Down Expand Up @@ -117,35 +122,34 @@ public void validateIndexPermissionsAreSubset(RequestInfo requestInfo, Authoriza
}

@Override
public void checkPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo,
HasPrivilegesRequest hasPrivilegesRequest,
public void checkPrivileges(AuthorizationInfo authorizationInfo,
PrivilegesToCheck privilegesToCheck,
Collection<ApplicationPrivilegeDescriptor> applicationPrivilegeDescriptors,
ActionListener<HasPrivilegesResponse> listener) {
if (isSuperuser(authentication.getUser())) {
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, true));
ActionListener<PrivilegesCheckResult> listener) {
if (isSuperuser(authorizationInfo)) {
listener.onResponse(getPrivilegesCheckResult(privilegesToCheck, true));
} else {
listener.onResponse(getHasPrivilegesResponse(authentication, hasPrivilegesRequest, false));
listener.onResponse(getPrivilegesCheckResult(privilegesToCheck, false));
}
}

@Override
public void getUserPrivileges(Authentication authentication, AuthorizationInfo authorizationInfo, GetUserPrivilegesRequest request,
public void getUserPrivileges(AuthorizationInfo authorizationInfo,
ActionListener<GetUserPrivilegesResponse> listener) {
if (isSuperuser(authentication.getUser())) {
if (isSuperuser(authorizationInfo)) {
listener.onResponse(getUserPrivilegesResponse(true));
} else {
listener.onResponse(getUserPrivilegesResponse(false));
}
}

private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentication, HasPrivilegesRequest hasPrivilegesRequest,
boolean authorized) {
private PrivilegesCheckResult getPrivilegesCheckResult(PrivilegesToCheck privilegesToCheck, boolean authorized) {
Map<String, Boolean> clusterPrivMap = new HashMap<>();
for (String clusterPriv : hasPrivilegesRequest.clusterPrivileges()) {
for (String clusterPriv : privilegesToCheck.cluster()) {
clusterPrivMap.put(clusterPriv, authorized);
}
final Map<String, ResourcePrivileges> indices = new LinkedHashMap<>();
for (IndicesPrivileges check : hasPrivilegesRequest.indexPrivileges()) {
for (IndicesPrivileges check : privilegesToCheck.index()) {
for (String index : check.getIndices()) {
final Map<String, Boolean> privileges = new HashMap<>();
final ResourcePrivileges existing = indices.get(index);
Expand All @@ -159,12 +163,12 @@ private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentica
}
}
final Map<String, Collection<ResourcePrivileges>> privilegesByApplication = new HashMap<>();
Set<String> applicationNames = Arrays.stream(hasPrivilegesRequest.applicationPrivileges())
Set<String> applicationNames = Arrays.stream(privilegesToCheck.application())
.map(RoleDescriptor.ApplicationResourcePrivileges::getApplication)
.collect(Collectors.toSet());
for (String applicationName : applicationNames) {
final Map<String, ResourcePrivileges> appPrivilegesByResource = new LinkedHashMap<>();
for (RoleDescriptor.ApplicationResourcePrivileges p : hasPrivilegesRequest.applicationPrivileges()) {
for (RoleDescriptor.ApplicationResourcePrivileges p : privilegesToCheck.application()) {
if (applicationName.equals(p.getApplication())) {
for (String resource : p.getResources()) {
final Map<String, Boolean> privileges = new HashMap<>();
Expand All @@ -181,8 +185,7 @@ private HasPrivilegesResponse getHasPrivilegesResponse(Authentication authentica
}
privilegesByApplication.put(applicationName, appPrivilegesByResource.values());
}
return new HasPrivilegesResponse(authentication.getUser().principal(), authorized, clusterPrivMap, indices.values(),
privilegesByApplication);
return new PrivilegesCheckResult(authorized, clusterPrivMap, indices, privilegesByApplication);
}

private GetUserPrivilegesResponse getUserPrivilegesResponse(boolean isSuperuser) {
Expand Down Expand Up @@ -223,4 +226,9 @@ public CustomAuthorizationInfo getAuthenticatedUserAuthorizationInfo() {
private boolean isSuperuser(User user) {
return Arrays.asList(user.roles()).contains("custom_superuser");
}

private boolean isSuperuser(AuthorizationInfo authorizationInfo) {
assert authorizationInfo instanceof CustomAuthorizationInfo;
return Arrays.asList(((CustomAuthorizationInfo)authorizationInfo).asMap().get("roles")).contains("custom_superuser");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import org.elasticsearch.action.ActionListener;

import java.util.Collection;

import static org.mockito.ArgumentMatchers.any;

/**
Expand All @@ -26,4 +28,14 @@ public abstract class ActionListenerUtils {
public static <T> ActionListener<T> anyActionListener() {
return any(ActionListener.class);
}

/**
* Returns a Mockito matcher for any argument that is a {@link Collection}.
* @param <T> the action listener type that the caller expects. Do not specify this, it will be inferred
* @return a collection matcher
*/
@SuppressWarnings("unchecked")
public static <T> Collection<T> anyCollection() {
return any(Collection.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.authc.support.SecondaryAuthentication;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.user.SystemUser;
import org.elasticsearch.xpack.core.security.user.User;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;

import static org.elasticsearch.xpack.core.security.authc.AuthenticationField.AUTHENTICATION_KEY;
import static org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField.AUTHORIZATION_INFO_KEY;

/**
* A lightweight utility that can find the current user and authentication information for the local thread.
Expand Down Expand Up @@ -76,6 +79,10 @@ public Authentication getAuthentication() {
}
}

public AuthorizationEngine.AuthorizationInfo getAuthorizationInfoFromContext() {
return Objects.requireNonNull(threadContext.getTransient(AUTHORIZATION_INFO_KEY), "authorization info is missing from context");
}

/**
* Returns the "secondary authentication" (see {@link SecondaryAuthentication}) information,
* or {@code null} if the current request does not have a secondary authentication context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,20 @@
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.ApplicationResourcePrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;

import java.io.IOException;

import static org.elasticsearch.action.ValidateActions.addValidationError;

/**
* A request for checking a user's privileges
*/
public class HasPrivilegesRequest extends ActionRequest implements UserRequest {

private String username;
private String[] clusterPrivileges;
private RoleDescriptor.IndicesPrivileges[] indexPrivileges;
private IndicesPrivileges[] indexPrivileges;
private ApplicationResourcePrivileges[] applicationPrivileges;

public HasPrivilegesRequest() {}
Expand All @@ -35,42 +33,16 @@ public HasPrivilegesRequest(StreamInput in) throws IOException {
this.username = in.readString();
this.clusterPrivileges = in.readStringArray();
int indexSize = in.readVInt();
indexPrivileges = new RoleDescriptor.IndicesPrivileges[indexSize];
indexPrivileges = new IndicesPrivileges[indexSize];
for (int i = 0; i < indexSize; i++) {
indexPrivileges[i] = new RoleDescriptor.IndicesPrivileges(in);
indexPrivileges[i] = new IndicesPrivileges(in);
}
applicationPrivileges = in.readArray(ApplicationResourcePrivileges::new, ApplicationResourcePrivileges[]::new);
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (clusterPrivileges == null) {
validationException = addValidationError("clusterPrivileges must not be null", validationException);
}
if (indexPrivileges == null) {
validationException = addValidationError("indexPrivileges must not be null", validationException);
}
if (applicationPrivileges == null) {
validationException = addValidationError("applicationPrivileges must not be null", validationException);
} else {
for (ApplicationResourcePrivileges applicationPrivilege : applicationPrivileges) {
try {
ApplicationPrivilege.validateApplicationName(applicationPrivilege.getApplication());
} catch (IllegalArgumentException e) {
validationException = addValidationError(e.getMessage(), validationException);
}
}
}
if (clusterPrivileges != null
&& clusterPrivileges.length == 0
&& indexPrivileges != null
&& indexPrivileges.length == 0
&& applicationPrivileges != null
&& applicationPrivileges.length == 0) {
validationException = addValidationError("must specify at least one privilege", validationException);
}
return validationException;
return new AuthorizationEngine.PrivilegesToCheck(clusterPrivileges, indexPrivileges, applicationPrivileges).validate(null);
}

/**
Expand All @@ -92,7 +64,7 @@ public String[] usernames() {
return new String[] { username };
}

public RoleDescriptor.IndicesPrivileges[] indexPrivileges() {
public IndicesPrivileges[] indexPrivileges() {
return indexPrivileges;
}

Expand All @@ -104,7 +76,7 @@ public ApplicationResourcePrivileges[] applicationPrivileges() {
return applicationPrivileges;
}

public void indexPrivileges(RoleDescriptor.IndicesPrivileges... privileges) {
public void indexPrivileges(IndicesPrivileges... privileges) {
this.indexPrivileges = privileges;
}

Expand All @@ -122,10 +94,9 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(username);
out.writeStringArray(clusterPrivileges);
out.writeVInt(indexPrivileges.length);
for (RoleDescriptor.IndicesPrivileges priv : indexPrivileges) {
for (IndicesPrivileges priv : indexPrivileges) {
priv.writeTo(out);
}
out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.client.internal.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
Expand All @@ -35,11 +36,15 @@ public HasPrivilegesRequestBuilder username(String username) {
* Set whether the user should be enabled or not
*/
public HasPrivilegesRequestBuilder source(String username, BytesReference source, XContentType xContentType) throws IOException {
final RoleDescriptor role = RoleDescriptor.parsePrivilegesCheck(username + "/has_privileges", source, xContentType);
final AuthorizationEngine.PrivilegesToCheck privilegesToCheck = RoleDescriptor.parsePrivilegesToCheck(
username + "/has_privileges",
source,
xContentType
);
request.username(username);
request.indexPrivileges(role.getIndicesPrivileges());
request.clusterPrivileges(role.getClusterPrivileges());
request.applicationPrivileges(role.getApplicationPrivileges());
request.clusterPrivileges(privilegesToCheck.cluster());
request.indexPrivileges(privilegesToCheck.index());
request.applicationPrivileges(privilegesToCheck.application());
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.core.security.action.user;

import org.elasticsearch.action.ActionType;

public class ProfileHasPrivilegesAction extends ActionType<ProfileHasPrivilegesResponse> {

public static final ProfileHasPrivilegesAction INSTANCE = new ProfileHasPrivilegesAction();
public static final String NAME = "cluster:admin/xpack/security/profile/has_privileges";

private ProfileHasPrivilegesAction() {
super(NAME, ProfileHasPrivilegesResponse::new);
}
}

0 comments on commit 3d4234e

Please sign in to comment.