Skip to content

Commit

Permalink
[Transform] Expose authorization failure as transform health issue (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
przemekwitek committed Apr 5, 2023
1 parent 0e1e4ce commit 8a43667
Show file tree
Hide file tree
Showing 42 changed files with 1,455 additions and 241 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/94724.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 94724
summary: Expose authorization failure as transform health issue
area: Transform
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.core.transform.action;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.support.tasks.BaseTasksRequest;
Expand All @@ -19,6 +20,7 @@
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xpack.core.common.validation.SourceDestValidator;
import org.elasticsearch.xpack.core.transform.TransformField;
import org.elasticsearch.xpack.core.transform.transforms.AuthorizationState;
import org.elasticsearch.xpack.core.transform.transforms.TransformConfig;
import org.elasticsearch.xpack.core.transform.transforms.TransformConfigUpdate;

Expand Down Expand Up @@ -46,6 +48,7 @@ public static class Request extends BaseTasksRequest<Request> {
private final String id;
private final boolean deferValidation;
private TransformConfig config;
private AuthorizationState authState;

public Request(TransformConfigUpdate update, String id, boolean deferValidation, TimeValue timeout) {
this.update = update;
Expand All @@ -62,6 +65,11 @@ public Request(StreamInput in) throws IOException {
if (in.readBoolean()) {
this.config = new TransformConfig(in);
}
if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
if (in.readBoolean()) {
this.authState = new AuthorizationState(in);
}
}
}

public static Request fromXContent(
Expand Down Expand Up @@ -124,6 +132,14 @@ public void setConfig(TransformConfig config) {
this.config = config;
}

public AuthorizationState getAuthState() {
return authState;
}

public void setAuthState(AuthorizationState authState) {
this.authState = authState;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
Expand All @@ -136,12 +152,20 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(true);
config.writeTo(out);
}
if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
if (authState == null) {
out.writeBoolean(false);
} else {
out.writeBoolean(true);
authState.writeTo(out);
}
}
}

@Override
public int hashCode() {
// the base class does not implement hashCode, therefore we need to hash timeout ourselves
return Objects.hash(getTimeout(), update, id, deferValidation, config);
return Objects.hash(getTimeout(), update, id, deferValidation, config, authState);
}

@Override
Expand All @@ -159,6 +183,7 @@ public boolean equals(Object obj) {
&& this.deferValidation == other.deferValidation
&& this.id.equals(other.id)
&& Objects.equals(config, other.config)
&& Objects.equals(authState, other.authState)
&& getTimeout().equals(other.getTimeout());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* 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.transform.transforms;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.health.HealthStatus;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContent;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentParser;

import java.io.IOException;
import java.time.Instant;
import java.util.Locale;
import java.util.Objects;

/**
* {@link AuthorizationState} holds the state of the authorization performed in the past.
* By examining the instance of this class the caller can learn whether or not the user was authorized to access the source/dest indices
* present in the {@link TransformConfig}.
*
* This class is immutable.
*/
public class AuthorizationState implements Writeable, ToXContentObject {

public static AuthorizationState green() {
return new AuthorizationState(System.currentTimeMillis(), HealthStatus.GREEN, null);
}

public static boolean isNullOrGreen(AuthorizationState authState) {
return authState == null || HealthStatus.GREEN.equals(authState.getStatus());
}

public static AuthorizationState red(Exception e) {
return new AuthorizationState(System.currentTimeMillis(), HealthStatus.RED, e != null ? e.getMessage() : "unknown exception");
}

public static final ParseField TIMESTAMP = new ParseField("timestamp");
public static final ParseField STATUS = new ParseField("status");
public static final ParseField LAST_AUTH_ERROR = new ParseField("last_auth_error");

public static final ConstructingObjectParser<AuthorizationState, Void> PARSER = new ConstructingObjectParser<>(
"transform_authorization_state",
true,
a -> new AuthorizationState((Long) a[0], (HealthStatus) a[1], (String) a[2])
);

static {
PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIMESTAMP);
PARSER.declareField(ConstructingObjectParser.constructorArg(), p -> {
if (p.currentToken() == XContentParser.Token.VALUE_STRING) {
return HealthStatus.valueOf(p.text().toUpperCase(Locale.ROOT));
}
throw new IllegalArgumentException("Unsupported token [" + p.currentToken() + "]");
}, STATUS, ObjectParser.ValueType.STRING);
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), LAST_AUTH_ERROR);
}

private final long timestampMillis;
private final HealthStatus status;
@Nullable
private final String lastAuthError;

public AuthorizationState(Long timestamp, HealthStatus status, @Nullable String lastAuthError) {
this.timestampMillis = timestamp;
this.status = status;
this.lastAuthError = lastAuthError;
}

public AuthorizationState(StreamInput in) throws IOException {
this.timestampMillis = in.readLong();
this.status = in.readEnum(HealthStatus.class);
this.lastAuthError = in.readOptionalString();
}

public Instant getTimestamp() {
return Instant.ofEpochMilli(timestampMillis);
}

public HealthStatus getStatus() {
return status;
}

public String getLastAuthError() {
return lastAuthError;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
builder.startObject();
builder.field(TIMESTAMP.getPreferredName(), timestampMillis);
builder.field(STATUS.getPreferredName(), status.xContentValue());
if (lastAuthError != null) {
builder.field(LAST_AUTH_ERROR.getPreferredName(), lastAuthError);
}
builder.endObject();
return builder;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeLong(timestampMillis);
status.writeTo(out);
out.writeOptionalString(lastAuthError);
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}

if (other == null || getClass() != other.getClass()) {
return false;
}

AuthorizationState that = (AuthorizationState) other;

return this.timestampMillis == that.timestampMillis
&& this.status.value() == that.status.value()
&& Objects.equals(this.lastAuthError, that.lastAuthError);
}

@Override
public int hashCode() {
return Objects.hash(timestampMillis, status, lastAuthError);
}

@Override
public String toString() {
return Strings.toString(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public static TransformCheckpoint fromXContent(final XContentParser parser, bool

public static String documentId(String transformId, long checkpoint) {
if (checkpoint < 0) {
throw new IllegalArgumentException("checkpoint must be a positive number");
throw new IllegalArgumentException("checkpoint must be a non-negative number");
}

return NAME + "-" + transformId + "-" + checkpoint;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class TransformConfigUpdate implements Writeable {

public static final String NAME = "data_frame_transform_config_update";

public static TransformConfigUpdate EMPTY = new TransformConfigUpdate(null, null, null, null, null, null, null, null);
public static final TransformConfigUpdate EMPTY = new TransformConfigUpdate(null, null, null, null, null, null, null, null);

private static final ConstructingObjectParser<TransformConfigUpdate, String> PARSER = new ConstructingObjectParser<>(
NAME,
Expand Down Expand Up @@ -235,6 +235,10 @@ public boolean changesSettings(TransformConfig config) {
return isNullOrEqual(settings, config.getSettings()) == false;
}

public boolean changesHeaders(TransformConfig config) {
return isNullOrEqual(headers, config.getHeaders()) == false;
}

private boolean isNullOrEqual(Object lft, Object rgt) {
return lft == null || lft.equals(rgt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

package org.elasticsearch.xpack.core.transform.transforms;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
Expand All @@ -21,18 +22,23 @@

public class TransformHealthIssue implements Writeable, ToXContentObject {

private static final String TYPE = "type";
private static final String ISSUE = "issue";
private static final String DETAILS = "details";
private static final String COUNT = "count";
private static final String FIRST_OCCURRENCE = "first_occurrence";
private static final String FIRST_OCCURRENCE_HUMAN_READABLE = FIRST_OCCURRENCE + "_string";

private static final String DEFAULT_TYPE_PRE_8_8 = "unknown";

private final String type;
private final String issue;
private final String details;
private final int count;
private final Instant firstOccurrence;

public TransformHealthIssue(String issue, String details, int count, Instant firstOccurrence) {
public TransformHealthIssue(String type, String issue, String details, int count, Instant firstOccurrence) {
this.type = Objects.requireNonNull(type);
this.issue = Objects.requireNonNull(issue);
this.details = details;
if (count < 1) {
Expand All @@ -43,12 +49,21 @@ public TransformHealthIssue(String issue, String details, int count, Instant fir
}

public TransformHealthIssue(StreamInput in) throws IOException {
if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
this.type = in.readString();
} else {
this.type = DEFAULT_TYPE_PRE_8_8;
}
this.issue = in.readString();
this.details = in.readOptionalString();
this.count = in.readVInt();
this.firstOccurrence = in.readOptionalInstant();
}

public String getType() {
return type;
}

public String getIssue() {
return issue;
}
Expand All @@ -68,6 +83,7 @@ public Instant getFirstOccurrence() {
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(TYPE, type);
builder.field(ISSUE, issue);
if (Strings.isNullOrEmpty(details) == false) {
builder.field(DETAILS, details);
Expand All @@ -81,6 +97,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws

@Override
public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_8_0)) {
out.writeString(type);
}
out.writeString(issue);
out.writeOptionalString(details);
out.writeVInt(count);
Expand All @@ -100,14 +119,15 @@ public boolean equals(Object other) {
TransformHealthIssue that = (TransformHealthIssue) other;

return this.count == that.count
&& Objects.equals(this.type, that.type)
&& Objects.equals(this.issue, that.issue)
&& Objects.equals(this.details, that.details)
&& Objects.equals(this.firstOccurrence, that.firstOccurrence);
}

@Override
public int hashCode() {
return Objects.hash(issue, details, count, firstOccurrence);
return Objects.hash(type, issue, details, count, firstOccurrence);
}

@Override
Expand Down

0 comments on commit 8a43667

Please sign in to comment.