Skip to content

Add a new API to update RCS specific API keys #96085

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 22, 2023
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,59 @@
/*
* 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.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

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

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

public abstract class BaseBulkUpdateApiKeyRequest extends BaseUpdateApiKeyRequest {

private final List<String> ids;

public BaseBulkUpdateApiKeyRequest(
final List<String> ids,
@Nullable final List<RoleDescriptor> roleDescriptors,
@Nullable final Map<String, Object> metadata
) {
super(roleDescriptors, metadata);
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
}

public BaseBulkUpdateApiKeyRequest(StreamInput in) throws IOException {
super(in);
this.ids = in.readStringList();
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = super.validate();
if (ids.isEmpty()) {
validationException = addValidationError("Field [ids] cannot be empty", validationException);
}
return validationException;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringCollection(ids);
}

public List<String> getIds() {
return ids;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public List<RoleDescriptor> getRoleDescriptors() {
return roleDescriptors;
}

public abstract ApiKey.Type getType();

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,16 @@

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

import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

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

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

public final class BulkUpdateApiKeyRequest extends BaseUpdateApiKeyRequest {
public final class BulkUpdateApiKeyRequest extends BaseBulkUpdateApiKeyRequest {

public static BulkUpdateApiKeyRequest usingApiKeyIds(String... ids) {
return new BulkUpdateApiKeyRequest(Arrays.stream(ids).toList(), null, null);
Expand All @@ -31,38 +26,20 @@ public static BulkUpdateApiKeyRequest wrap(final UpdateApiKeyRequest request) {
return new BulkUpdateApiKeyRequest(List.of(request.getId()), request.getRoleDescriptors(), request.getMetadata());
}

private final List<String> ids;

public BulkUpdateApiKeyRequest(
final List<String> ids,
@Nullable final List<RoleDescriptor> roleDescriptors,
@Nullable final Map<String, Object> metadata
) {
super(roleDescriptors, metadata);
this.ids = Objects.requireNonNull(ids, "API key IDs must not be null");
super(ids, roleDescriptors, metadata);
}

public BulkUpdateApiKeyRequest(StreamInput in) throws IOException {
super(in);
this.ids = in.readStringList();
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = super.validate();
if (ids.isEmpty()) {
validationException = addValidationError("Field [ids] cannot be empty", validationException);
}
return validationException;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeStringCollection(ids);
}

public List<String> getIds() {
return ids;
public ApiKey.Type getType() {
return ApiKey.Type.REST;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import org.elasticsearch.core.Assertions;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.core.security.action.role.RoleDescriptorRequestValidator;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

Expand Down Expand Up @@ -99,14 +97,6 @@ public int hashCode() {
}

public static CreateCrossClusterApiKeyRequest withNameAndAccess(String name, String access) throws IOException {
return new CreateCrossClusterApiKeyRequest(
name,
CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, access),
null
),
null,
null
);
return new CreateCrossClusterApiKeyRequest(name, CrossClusterApiKeyRoleDescriptorBuilder.parse(access), null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.elasticsearch.xcontent.json.JsonXContent;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

Expand Down Expand Up @@ -87,6 +90,13 @@ public RoleDescriptor build() {
);
}

public static CrossClusterApiKeyRoleDescriptorBuilder parse(String access) throws IOException {
return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(
JsonXContent.jsonXContent.createParser(XContentParserConfiguration.EMPTY, access),
null
);
}

static void validate(RoleDescriptor roleDescriptor) {
if (false == ROLE_DESCRIPTOR_NAME.equals(roleDescriptor.getName())) {
throw new IllegalArgumentException("invalid role descriptor name [" + roleDescriptor.getName() + "]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,9 @@ public void writeTo(StreamOutput out) throws IOException {
public String getId() {
return id;
}

@Override
public ApiKey.Type getType() {
return ApiKey.Type.REST;
}
}
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.apikey;

import org.elasticsearch.action.ActionType;

public final class UpdateCrossClusterApiKeyAction extends ActionType<UpdateApiKeyResponse> {

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

private UpdateCrossClusterApiKeyAction() {
super(NAME, UpdateApiKeyResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.core.Nullable;

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

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

public final class UpdateCrossClusterApiKeyRequest extends BaseUpdateApiKeyRequest {
private final String id;

public UpdateCrossClusterApiKeyRequest(
final String id,
@Nullable CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder,
@Nullable final Map<String, Object> metadata
) {
super(roleDescriptorBuilder == null ? null : List.of(roleDescriptorBuilder.build()), metadata);
this.id = Objects.requireNonNull(id, "API key ID must not be null");
}

public UpdateCrossClusterApiKeyRequest(StreamInput in) throws IOException {
super(in);
this.id = in.readString();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(id);
}

public String getId() {
return id;
}

@Override
public ApiKey.Type getType() {
return ApiKey.Type.CROSS_CLUSTER;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = super.validate();
if (roleDescriptors == null && metadata == null) {
validationException = addValidationError(
"must update either [access] or [metadata] for cross-cluster API keys",
validationException
);
}
return validationException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,21 @@
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xcontent.XContentParserConfiguration;
import org.junit.Before;

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

import static org.elasticsearch.xcontent.json.JsonXContent.jsonXContent;

public class CreateCrossClusterApiKeyRequestTests extends AbstractWireSerializingTestCase<CreateCrossClusterApiKeyRequest> {

private static final List<String> ACCESS_CANDIDATES = List.of("""
{
"search": [ {"names": ["logs"]} ]
}""", """
{
"search": [ {"names": ["logs"], "query": "abc" } ]
}""", """
{
"search": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]} } ]
}""", """
{
"search": [ {"names": ["logs"], "query": "abc", "field_security": {"grant": ["*"], "except": ["private"]} } ]
}""", """
{
"replication": [ {"names": ["archive"], "allow_restricted_indices": true } ]
}""", """
{
"replication": [ {"names": ["archive"]} ]
}""", """
{
"search": [ {"names": ["logs"]} ],
"replication": [ {"names": ["archive"]} ]
}""");

private String access;
private CrossClusterApiKeyRoleDescriptorBuilder roleDescriptorBuilder;

@Before
public void init() {
access = randomFrom(ACCESS_CANDIDATES);
roleDescriptorBuilder = parseForCrossClusterApiKeyRoleDescriptorBuilder(access);
public void init() throws IOException {
access = randomCrossClusterApiKeyAccessField();
roleDescriptorBuilder = CrossClusterApiKeyRoleDescriptorBuilder.parse(access);
}

@Override
Expand Down Expand Up @@ -86,7 +57,9 @@ protected CreateCrossClusterApiKeyRequest mutateInstance(CreateCrossClusterApiKe
case 2 -> {
return new CreateCrossClusterApiKeyRequest(
instance.getName(),
parseForCrossClusterApiKeyRoleDescriptorBuilder(randomValueOtherThan(access, () -> randomFrom(ACCESS_CANDIDATES))),
CrossClusterApiKeyRoleDescriptorBuilder.parse(
randomValueOtherThan(access, CreateCrossClusterApiKeyRequestTests::randomCrossClusterApiKeyAccessField)
),
instance.getExpiration(),
instance.getMetadata()
);
Expand All @@ -110,15 +83,6 @@ protected CreateCrossClusterApiKeyRequest mutateInstance(CreateCrossClusterApiKe
}
}

private CrossClusterApiKeyRoleDescriptorBuilder parseForCrossClusterApiKeyRoleDescriptorBuilder(String access) {
try {
final XContentParser parser = jsonXContent.createParser(XContentParserConfiguration.EMPTY, access);
return CrossClusterApiKeyRoleDescriptorBuilder.PARSER.parse(parser, null);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static TimeValue randomExpiration() {
return randomFrom(TimeValue.timeValueHours(randomIntBetween(1, 999)), null);
}
Expand All @@ -136,4 +100,32 @@ private static Map<String, Object> randomMetadata() {
null
);
}

private static final List<String> ACCESS_CANDIDATES = List.of("""
{
"search": [ {"names": ["logs"]} ]
}""", """
{
"search": [ {"names": ["logs"], "query": "abc" } ]
}""", """
{
"search": [ {"names": ["logs"], "field_security": {"grant": ["*"], "except": ["private"]} } ]
}""", """
{
"search": [ {"names": ["logs"], "query": "abc", "field_security": {"grant": ["*"], "except": ["private"]} } ]
}""", """
{
"replication": [ {"names": ["archive"], "allow_restricted_indices": true } ]
}""", """
{
"replication": [ {"names": ["archive"]} ]
}""", """
{
"search": [ {"names": ["logs"]} ],
"replication": [ {"names": ["archive"]} ]
}""");

public static String randomCrossClusterApiKeyAccessField() {
return randomFrom(ACCESS_CANDIDATES);
}
}
Loading