Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -52,7 +52,7 @@ public ActionRequestValidationException validate() {
if (name == null) {
validationException = addValidationError("role name is missing", validationException);
}
if(applicationPrivileges != null) {
if (applicationPrivileges != null) {
for (RoleDescriptor.ApplicationResourcePrivileges privilege : applicationPrivileges) {
try {
ApplicationPrivilege.validateApplicationNameOrWildcard(privilege.getApplication());
Expand Down Expand Up @@ -83,11 +83,11 @@ public void cluster(String... clusterPrivileges) {
this.clusterPrivileges = clusterPrivileges;
}

void conditionalCluster(ConditionalClusterPrivilege... conditionalClusterPrivileges) {
public void conditionalClusterPrivileges(ConditionalClusterPrivilege... conditionalClusterPrivileges) {
this.conditionalClusterPrivileges = conditionalClusterPrivileges;
}

void addIndex(RoleDescriptor.IndicesPrivileges... privileges) {
public void addIndicesPrivileges(RoleDescriptor.IndicesPrivileges... privileges) {
this.indicesPrivileges.addAll(Arrays.asList(privileges));
}

Expand All @@ -102,7 +102,7 @@ public void addIndex(String[] indices, String[] privileges, String[] grantedFiel
.build());
}

void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) {
public void addApplicationPrivileges(RoleDescriptor.ApplicationResourcePrivileges... privileges) {
this.applicationPrivileges.addAll(Arrays.asList(privileges));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public PutRoleRequestBuilder source(String name, BytesReference source, XContent
assert name.equals(descriptor.getName());
request.name(name);
request.cluster(descriptor.getClusterPrivileges());
request.conditionalCluster(descriptor.getConditionalClusterPrivileges());
request.addIndex(descriptor.getIndicesPrivileges());
request.conditionalClusterPrivileges(descriptor.getConditionalClusterPrivileges());
request.addIndicesPrivileges(descriptor.getIndicesPrivileges());
request.addApplicationPrivileges(descriptor.getApplicationPrivileges());
request.runAs(descriptor.getRunAs());
request.metadata(descriptor.getMetadata());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,6 @@
"index": false,
"doc_values": false
},
"role_descriptors" : {
"type" : "object",
"dynamic" : true
},
"version" : {
"type" : "integer"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private PutRoleRequest buildRandomRequest() {

if (randomBoolean()) {
final String[] appNames = randomArray(1, 4, String[]::new, stringWithInitialLowercase);
request.conditionalCluster(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames)));
request.conditionalClusterPrivileges(new ConditionalClusterPrivileges.ManageApplicationPrivileges(Sets.newHashSet(appNames)));
}

request.runAs(generateRandomStringArray(4, 3, false, true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.DocWriteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexAction;
Expand All @@ -28,9 +30,16 @@
import org.elasticsearch.xpack.core.XPackSettings;
import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest;
import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleRequest;
import org.elasticsearch.xpack.core.security.action.role.PutRoleAction;
import org.elasticsearch.xpack.core.security.action.role.PutRoleRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.support.Hasher;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;

Expand All @@ -40,8 +49,11 @@
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -79,8 +91,8 @@ public class ApiKeyService {
private final Hasher hasher;
private final boolean enabled;

public ApiKeyService(Settings settings, Clock clock, Client client,
SecurityIndexManager securityIndex, ClusterService clusterService) {
public ApiKeyService(Settings settings, Clock clock, Client client, SecurityIndexManager securityIndex,
ClusterService clusterService) {
this.clock = clock;
this.client = client;
this.securityIndex = securityIndex;
Expand Down Expand Up @@ -109,7 +121,11 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ
"able to use api keys", Version.V_7_0_0);
}

final String id = UUIDs.base64UUID();
final String roleName = "_es_apikey_role_" + id;
final PutRoleRequest putRoleRequest = mergeRoleDescriptors(roleName, request.getRoleDescriptors());
final char[] keyHash = hasher.hash(apiKey);
IndexRequest indexRequest;
try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
builder.startObject()
.field("doc_type", "api_key")
Expand All @@ -126,7 +142,7 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ
}
}

builder.array("role_descriptors", request.getRoleDescriptors())
builder.array("roles", roleName)
.field("name", request.getName())
.field("version", version.id)
.startObject("creator")
Expand All @@ -136,24 +152,74 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ
authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName())
.endObject()
.endObject();
final IndexRequest indexRequest =
client.prepareIndex(SecurityIndexManager.SECURITY_INDEX_NAME, TYPE)
indexRequest = client.prepareIndex(SecurityIndexManager.SECURITY_INDEX_NAME, TYPE, id)
.setOpType(DocWriteRequest.OpType.CREATE)
.setSource(builder)
.setRefreshPolicy(request.getRefreshPolicy())
.request();
securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () ->
executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexRequest,
ActionListener.wrap(indexResponse ->
listener.onResponse(new CreateApiKeyResponse(request.getName(), indexResponse.getId(), apiKey, expiration)),
listener::onFailure)));
} catch (IOException e) {
listener.onFailure(e);
return;
} finally {
Arrays.fill(keyHash, (char) 0);
}

if (indexRequest == null) {
throw new IllegalStateException("index request cannot be null");
}

client.execute(PutRoleAction.INSTANCE, putRoleRequest, ActionListener.wrap(response ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this execute performed withOrigin?
We shouldn't assume/require that all users who can create API keys can Put Roles.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 good call

executeAsyncWithOrigin(client, SECURITY_ORIGIN, IndexAction.INSTANCE, indexRequest, ActionListener.wrap(indexResponse ->
listener.onResponse(new CreateApiKeyResponse(request.getName(), indexResponse.getId(), apiKey, expiration)),
e -> {
final DeleteRoleRequest deleteRoleRequest = new DeleteRoleRequest();
deleteRoleRequest.name(roleName);
client.execute(DeleteRoleAction.INSTANCE, deleteRoleRequest, ActionListener.wrap(deleteRoleResponse -> {
if (deleteRoleResponse.found()) {
logger.info("api key creation failed, but successfully cleaned up role: " + roleName);
} else {
logger.info("api key creation failed and no role found with name: " + roleName);
}
}, deleteEx -> logger.error(
new ParameterizedMessage("failed to clean up role [{}] after api key creation failure", roleName), deleteEx)));
listener.onFailure(e);
})),
listener::onFailure));
}
}

/**
* Combines the provided role descriptors into a put role request with the specified name
*/
private PutRoleRequest mergeRoleDescriptors(String name, List<RoleDescriptor> descriptors) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we will test creation of role after merging in integTest or do we want to add a unit test for this?

final List<RoleDescriptor.IndicesPrivileges> indicesPrivileges = new ArrayList<>();
final Map<String, Object> metadata = new HashMap<>();
final List<String> clusterPrivileges = new ArrayList<>();
final List<String> runAsPrivileges = new ArrayList<>();
final List<RoleDescriptor.ApplicationResourcePrivileges> applicationResourcePrivileges = new ArrayList<>();
final List<ConditionalClusterPrivilege> conditionalClusterPrivileges = new ArrayList<>();
for (RoleDescriptor descriptor : descriptors) {
indicesPrivileges.addAll(Arrays.asList(descriptor.getIndicesPrivileges()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jaymode, I think I missed this and while working on subset code I realized whether we should be merging the indices privileges if the index names match? (Something on the lines that we do with MergeableIndicesPrivileges)

metadata.putAll(descriptor.getMetadata().entrySet().stream()
.filter(e -> e.getKey().startsWith(MetadataUtils.RESERVED_PREFIX) == false)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
clusterPrivileges.addAll(Arrays.asList(descriptor.getClusterPrivileges()));
runAsPrivileges.addAll(Arrays.asList(descriptor.getRunAs()));
applicationResourcePrivileges.addAll(Arrays.asList(descriptor.getApplicationPrivileges()));
conditionalClusterPrivileges.addAll(Arrays.asList(descriptor.getConditionalClusterPrivileges()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could create a role descriptor that cannot be converted to XContent.
If two role descriptors have

  • global: { application: { manage: { applications: [ "app1" ] } } }
  • global: { application: { manage: { applications: [ "app2 "] } } }

then the merged role descriptor cannot produce valid XContent.

The GetUserPrivileges action had this problem and had to use a different output format to accommodate the potential for multivariate fields.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point and would break the get api once implemented.

}

final PutRoleRequest request = new PutRoleRequest();
request.name(name);
request.cluster(clusterPrivileges.toArray(Strings.EMPTY_ARRAY));
request.addIndicesPrivileges(indicesPrivileges.toArray(new RoleDescriptor.IndicesPrivileges[0]));
request.addApplicationPrivileges(applicationResourcePrivileges.toArray(new RoleDescriptor.ApplicationResourcePrivileges[0]));
request.conditionalClusterPrivileges(conditionalClusterPrivileges.toArray(new ConditionalClusterPrivilege[0]));
request.runAs(runAsPrivileges.toArray(Strings.EMPTY_ARRAY));
request.metadata(metadata);
return request;
}

/**
* Checks for the presence of a {@code Authorization} header with a value that starts with
* {@code ApiKey }. If found this will attempt to authenticate the key.
Expand Down Expand Up @@ -211,12 +277,14 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) {
final String principal = Objects.requireNonNull((String) source.get("principal"));
final Map<String, Object> metadata = (Map<String, Object>) source.get("metadata");
final List<Map<String, Object>> roleDescriptors = (List<Map<String, Object>>) source.get("role_descriptors");
final String[] roleNames = roleDescriptors.stream()
.map(rdSource -> (String) rdSource.get("name"))
.collect(Collectors.toList())
.toArray(Strings.EMPTY_ARRAY);
final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true);
final List<String> roles;
final Object rolesObj = source.get("roles");
if (rolesObj instanceof String) {
roles = Collections.singletonList((String) rolesObj);
} else {
roles = (List<String>) rolesObj;
}
final User apiKeyUser = new User(principal, roles.toArray(Strings.EMPTY_ARRAY), null, null, metadata, true);
listener.onResponse(AuthenticationResult.success(apiKeyUser));
} else {
listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void testValidateApiKey() throws Exception {
sourceMap.put("api_key_hash", new String(hash));
sourceMap.put("principal", "test_user");
sourceMap.put("metadata", Collections.emptyMap());
sourceMap.put("role_descriptors", Collections.singletonList(Collections.singletonMap("name", "a role")));
sourceMap.put("roles", Collections.singletonList("a role"));


ApiKeyService.ApiKeyCredentials creds =
Expand Down