Skip to content
Merged
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
@@ -0,0 +1,99 @@
/*
* 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.apikey;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

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

public abstract class AbstractCreateApiKeyRequest extends ActionRequest {
public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL;
protected final String id;
protected String name;
protected TimeValue expiration;
protected Map<String, Object> metadata;
protected List<RoleDescriptor> roleDescriptors = Collections.emptyList();
protected WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY;

public AbstractCreateApiKeyRequest() {
super();
// we generate the API key id soonest so it's part of the request body so it is audited
this.id = UUIDs.base64UUID(); // because auditing can currently only catch requests but not responses,
}

public AbstractCreateApiKeyRequest(StreamInput in) throws IOException {
super(in);
this.id = doReadId(in);
}

protected abstract String doReadId(StreamInput in) throws IOException;

public String getId() {
return id;
}

public String getName() {
return name;
}

public abstract ApiKey.Type getType();

public TimeValue getExpiration() {
return expiration;
}

public List<RoleDescriptor> getRoleDescriptors() {
return roleDescriptors;
}

public WriteRequest.RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

public Map<String, Object> getMetadata() {
return metadata;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(name)) {
validationException = addValidationError("api key name is required", validationException);
} else {
if (name.length() > 256) {
validationException = addValidationError("api key name may not be more than 256 characters long", validationException);
}
if (name.equals(name.trim()) == false) {
validationException = addValidationError("api key name may not begin or end with whitespace", validationException);
}
if (name.startsWith("_")) {
validationException = addValidationError("api key name may not begin with an underscore", validationException);
}
}
if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) {
validationException = addValidationError(
"API key metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]",
validationException
);
}
return validationException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
Expand All @@ -36,6 +37,29 @@
*/
public final class ApiKey implements ToXContentObject, Writeable {

public enum Type {
/**
* REST type API keys can authenticate on the HTTP interface
*/
REST,
/**
* Cross cluster type API keys can authenticate on the dedicated remote cluster server interface
*/
CROSS_CLUSTER;

public static Type parse(String value) {
return switch (value.toLowerCase(Locale.ROOT)) {
case "rest" -> REST;
case "cross_cluster" -> CROSS_CLUSTER;
default -> throw new IllegalArgumentException("unknown API key type [" + value + "]");
};
}

public String value() {
return name().toLowerCase(Locale.ROOT);
}
}

private final String name;
private final String id;
private final Instant creation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,29 @@
package org.elasticsearch.xpack.core.security.action.apikey;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

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

/**
* Request class used for the creation of an API key. The request requires a name to be provided
* and optionally an expiration time and permission limitation can be provided.
*/
public final class CreateApiKeyRequest extends ActionRequest {
public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL;

private final String id;
private String name;
private TimeValue expiration;
private Map<String, Object> metadata;
private List<RoleDescriptor> roleDescriptors = Collections.emptyList();
private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY;
public final class CreateApiKeyRequest extends AbstractCreateApiKeyRequest {

public CreateApiKeyRequest() {
super();
this.id = UUIDs.base64UUID(); // because auditing can currently only catch requests but not responses,
// we generate the API key id soonest so it's part of the request body so it is audited
}

/**
Expand Down Expand Up @@ -74,11 +58,6 @@ public CreateApiKeyRequest(

public CreateApiKeyRequest(StreamInput in) throws IOException {
super(in);
if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) {
this.id = in.readString();
} else {
this.id = UUIDs.base64UUID();
}
if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_5_0)) {
this.name = in.readOptionalString();
} else {
Expand All @@ -94,77 +73,48 @@ public CreateApiKeyRequest(StreamInput in) throws IOException {
}
}

public String getId() {
return id;
@Override
protected String doReadId(StreamInput in) throws IOException {
if (in.getTransportVersion().onOrAfter(TransportVersion.V_7_10_0)) {
return in.readString();
} else {
return UUIDs.base64UUID();
}
}

public void setId() {
throw new UnsupportedOperationException("The API Key Id cannot be set, it must be generated randomly");
@Override
public ApiKey.Type getType() {
return ApiKey.Type.REST;
}

public String getName() {
return name;
public void setId() {
throw new UnsupportedOperationException("The API Key Id cannot be set, it must be generated randomly");
}

public void setName(String name) {
this.name = name;
}

public TimeValue getExpiration() {
return expiration;
}

public void setExpiration(@Nullable TimeValue expiration) {
this.expiration = expiration;
}

public List<RoleDescriptor> getRoleDescriptors() {
return roleDescriptors;
}

public void setRoleDescriptors(@Nullable List<RoleDescriptor> roleDescriptors) {
this.roleDescriptors = (roleDescriptors == null) ? List.of() : List.copyOf(roleDescriptors);
}

public WriteRequest.RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
}

public Map<String, Object> getMetadata() {
return metadata;
}

public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(name)) {
validationException = addValidationError("api key name is required", validationException);
} else {
if (name.length() > 256) {
validationException = addValidationError("api key name may not be more than 256 characters long", validationException);
}
if (name.equals(name.trim()) == false) {
validationException = addValidationError("api key name may not begin or end with whitespace", validationException);
}
if (name.startsWith("_")) {
validationException = addValidationError("api key name may not begin with an underscore", validationException);
}
}
if (metadata != null && MetadataUtils.containsReservedMetadata(metadata)) {
validationException = addValidationError(
"API key metadata keys may not start with [" + MetadataUtils.RESERVED_PREFIX + "]",
validationException
);
}
for (RoleDescriptor roleDescriptor : roleDescriptors) {
ActionRequestValidationException validationException = super.validate();
for (RoleDescriptor roleDescriptor : getRoleDescriptors()) {
validationException = RoleDescriptorRequestValidator.validate(roleDescriptor, validationException);
}
return validationException;
Expand All @@ -182,7 +132,7 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
}
out.writeOptionalTimeValue(expiration);
out.writeList(roleDescriptors);
out.writeList(getRoleDescriptors());
refreshPolicy.writeTo(out);
if (out.getTransportVersion().onOrAfter(TransportVersion.V_7_13_0)) {
out.writeGenericMap(metadata);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.apikey;

import org.elasticsearch.action.ActionType;

/**
* ActionType for the creation of a cross-cluster API key
*/
public final class CreateCrossClusterApiKeyAction extends ActionType<CreateApiKeyResponse> {

public static final String NAME = "cluster:admin/xpack/security/cross_cluster/api_key/create";
public static final CreateCrossClusterApiKeyAction INSTANCE = new CreateCrossClusterApiKeyAction();

private CreateCrossClusterApiKeyAction() {
super(NAME, CreateApiKeyResponse::new);
}

}
Loading