Skip to content
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

Enhance Invalidate Token API #35388

Merged
merged 50 commits into from
Dec 18, 2018
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
71447cc
wip
jkakavas Nov 2, 2018
c9b1c45
Adds support for invalidate by realm or username
jkakavas Nov 6, 2018
dee9fce
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Nov 6, 2018
f1cbe27
Update TransportSamlInvalidateSessionActionTests
jkakavas Nov 6, 2018
118ed35
Handle cases where there are no tokens to invalidate
jkakavas Nov 7, 2018
a6e1f22
Remove * import
jkakavas Nov 7, 2018
485cff9
Add a couple more integ tests
jkakavas Nov 7, 2018
0647a28
Implement toXContent and revelant tests
jkakavas Nov 8, 2018
94abc3f
add rest tests
jkakavas Nov 8, 2018
a2227ea
Don't wrap result
jkakavas Nov 8, 2018
19c75c4
remove another unnecessary layer of XContent wrapping
jkakavas Nov 8, 2018
c3e67f6
Refactor and add invalidateAllTokens method
jkakavas Nov 8, 2018
8e4dec5
Change some hasText <-> isNullOrEmpty for readability and remove unne…
jkakavas Nov 8, 2018
1005e15
Correct parsing of invalidation request
jkakavas Nov 8, 2018
2a37cfc
Remove unused imports
jkakavas Nov 8, 2018
d6cf150
Don't handle VersionConflictEngineExceptions as we don't do version u…
jkakavas Nov 8, 2018
40feb13
duplicate traceLog for readability
jkakavas Nov 8, 2018
75c1f37
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Nov 28, 2018
4a76b77
address initial feedback
jkakavas Nov 29, 2018
43127b0
Finalize change
jkakavas Dec 2, 2018
0055ba1
Add empty new line
jkakavas Dec 3, 2018
d7c1556
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 3, 2018
541bf64
Fix rest tests
jkakavas Dec 3, 2018
46fca25
Fix TransportSamlLogoutActionTests
jkakavas Dec 3, 2018
ac3dd86
Remove unused import
jkakavas Dec 3, 2018
be3cad3
fix tests so that arrays are not empty when required
jkakavas Dec 3, 2018
a2d8078
Address feedback
jkakavas Dec 5, 2018
8342818
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 5, 2018
5c28353
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 7, 2018
fd5ea5f
Reverts commits regarding HLRC
jkakavas Dec 7, 2018
e402d88
Address feedback
jkakavas Dec 7, 2018
dac7823
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 7, 2018
9c8e90c
Reference HLRC PR in assumeFalse
jkakavas Dec 7, 2018
a8c9410
fix merge woes
jkakavas Dec 7, 2018
0f2f201
Address feedback
jkakavas Dec 10, 2018
6209deb
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 10, 2018
2fb24d6
Fix serialization tests now that we do not implement equals
jkakavas Dec 11, 2018
2e0cf31
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 14, 2018
e91e555
address feedback
jkakavas Dec 14, 2018
6ad0850
Missing previousResult param
jkakavas Dec 14, 2018
bbbd900
Restore rest tests
jkakavas Dec 14, 2018
bcac7f1
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 14, 2018
bf9043a
restore previous invalidated token HLRC docs
jkakavas Dec 17, 2018
162ea66
Add created field so that it's included in the backport
jkakavas Dec 17, 2018
7356dd2
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 17, 2018
75e62d6
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 17, 2018
1c7c761
address final round of feedback
jkakavas Dec 17, 2018
858935e
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 17, 2018
e0546b3
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 17, 2018
ff775e6
Merge remote-tracking branch 'origin/master' into enhance-invalidate-api
jkakavas Dec 18, 2018
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 @@ -8,7 +8,7 @@
import org.elasticsearch.action.Action;

/**
* Action for invalidating a given token
* Action for invalidating one or more tokens
*/
public final class InvalidateTokenAction extends Action<InvalidateTokenResponse> {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
Expand All @@ -22,31 +23,88 @@
public final class InvalidateTokenRequest extends ActionRequest {

public enum Type {
ACCESS_TOKEN,
REFRESH_TOKEN
ACCESS_TOKEN("token"),
REFRESH_TOKEN("refresh_token");

private final String value;

Type(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static Type fromString(String tokenType) {
if (tokenType != null) {
for (Type type : values()) {
if (type.getValue().equals(tokenType)) {
return type;
}
}
}
return null;
}
}

private String tokenString;
private Type tokenType;
private String realmName;
private String userName;

public InvalidateTokenRequest() {}

/**
* @param tokenString the string representation of the token
* @param tokenString the string representation of the token to be invalidated
* @param tokenType the type of the token to be invalidated
* @param realmName the name of the realm for which all tokens will be invalidated
* @param userName the principal of the user for which all tokens will be invalidated
*/
public InvalidateTokenRequest(String tokenString, Type type) {
public InvalidateTokenRequest(@Nullable String tokenString, @Nullable String tokenType, @Nullable String realmName, @Nullable String userName) {
this.tokenString = tokenString;
this.tokenType = type;
this.tokenType = Type.fromString(tokenType);
this.realmName = realmName;
this.userName = userName;
}

/**
* @param tokenString the string representation of the token to be invalidated
* @param tokenType the type of the token to be invalidated
*/
public InvalidateTokenRequest(String tokenString, String tokenType) {
this.tokenString = tokenString;
this.tokenType = Type.fromString(tokenType);
this.realmName = null;
this.userName = null;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(tokenString)) {
validationException = addValidationError("token string must be provided", null);
}
if (tokenType == null) {
validationException = addValidationError("token type must be provided", validationException);
if (Strings.hasText(realmName)) {
if (Strings.hasText(tokenString)) {
validationException = addValidationError("token string must not be provided when realm name is specified", null);
}
if (tokenType != null) {
validationException = addValidationError("token type must not be provided when realm name is specified", null);
}
} else if (Strings.hasText(userName)) {
if (Strings.hasText(tokenString)) {
validationException = addValidationError("token string must not be provided when username is specified", null);
}
if (tokenType != null) {
validationException = addValidationError("token type must not be provided when username is specified", null);
}
} else {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
if (Strings.isNullOrEmpty(tokenString)) {
validationException =
addValidationError("token string must be provided when not specifying a realm name or a username", null);
}
if (tokenType == null) {
validationException =
addValidationError("token type must be provided when a token string is specified", null);
}
}
return validationException;
}
Expand All @@ -67,26 +125,52 @@ void setTokenType(Type tokenType) {
this.tokenType = tokenType;
}

public String getRealmName() {
return realmName;
}

public void setRealmName(String realmName) {
this.realmName = realmName;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(tokenString);
out.writeOptionalString(tokenString);
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
out.writeVInt(tokenType.ordinal());
out.writeOptionalVInt(tokenType == null ? null : tokenType.ordinal());
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
} else if (tokenType == Type.REFRESH_TOKEN) {
throw new IllegalArgumentException("refresh token invalidation cannot be serialized with version [" + out.getVersion() +
"]");
throw new IllegalArgumentException("refresh token invalidation cannot be serialized with version [" + out.getVersion() + "]");
}
if (out.getVersion().onOrAfter(Version.V_6_6_0)) {
out.writeOptionalString(realmName);
out.writeOptionalString(userName);
} else {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("realm token invalidation cannot be serialized with version [" + out.getVersion() + "]");
}
}

@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
tokenString = in.readString();
tokenString = in.readOptionalString();
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
tokenType = Type.values()[in.readVInt()];
Integer type = in.readOptionalVInt();
tokenType = type == null ? null : Type.values()[type];
} else {
tokenType = Type.ACCESS_TOKEN;
}
if (in.getVersion().onOrAfter(Version.V_6_6_0)) {
realmName = in.readOptionalString();
userName = in.readOptionalString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,20 @@ public InvalidateTokenRequestBuilder setType(InvalidateTokenRequest.Type type) {
request.setTokenType(type);
return this;
}

/**
* Sets the name of the realm for which all tokens should be invalidated
*/
public InvalidateTokenRequestBuilder setRealmName(String realmName) {
request.setRealmName(realmName);
return this;
}

/**
* Sets the username for which all tokens should be invalidated
*/
public InvalidateTokenRequestBuilder setUserName(String username) {
request.setUserName(username);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,43 @@
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationResult;

import java.io.IOException;

/**
* Response for a invalidation of a token.
* Response for a invalidation of one or multiple tokens.
*/
public final class InvalidateTokenResponse extends ActionResponse {
public final class InvalidateTokenResponse extends ActionResponse implements ToXContent {

private boolean created;
private TokensInvalidationResult result;
Copy link
Contributor

Choose a reason for hiding this comment

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

Per my earlier comment I don't understand the use of a Result object inside a Response object.
It's more strange in the Rest client, but I don't follow it here either.

Copy link
Member Author

Choose a reason for hiding this comment

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

The Result object was created so that I can efficiently pass status between invalidation attempts in TokenService. Then, since I had that encapsulation in the result object, it made sense to use that object in the Response. This didn't feel strange (neither does it now), I thought it follows the paradigm we use with i.e. User objects in GetUsers or RoleDescriptor objects in GetRoles (granted we have multiple objects there but one result here ).

That said I don't have any objection to not use it in the response if you feel strongly about it.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK - I get it now.
I'll come back when I've finished reading the token service changes.


public InvalidateTokenResponse() {}

public InvalidateTokenResponse(boolean created) {
this.created = created;
public InvalidateTokenResponse(TokensInvalidationResult result) {
this.result = result;
}

/**
* If the token is already invalidated then created will be <code>false</code>
*/
public boolean isCreated() {
return created;
public TokensInvalidationResult getResult() {
return result;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeBoolean(created);
TokensInvalidationResult.writeTo(result, out);
}

@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
created = in.readBoolean();
result = TokensInvalidationResult.readFrom(in);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
return result.toXContent(builder, params);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.core.security.authc.support;

import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

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

/**
* The result of attempting to invalidate one or multiple tokens. The result contains information about:
* <ul>
* <li>how many of the tokens were actually invalidated</li>
* <li>how many tokens are not invalidated in this request because they were already invalidated</li>
* <li>how many tokens were not invalidated because of an error and what the error was</li>
* </ul>
*/
public class TokensInvalidationResult implements ToXContentObject {

private final String[] invalidatedTokens;
private final String[] prevInvalidatedTokens;
private final String[] errors;
private final int attemptCounter;
Copy link
Member

Choose a reason for hiding this comment

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

attemptCount maybe since its final


jkakavas marked this conversation as resolved.
Show resolved Hide resolved

public TokensInvalidationResult(String[] invalidatedTokens, String[] notInvalidatedTokens,
@Nullable String[] errors, int attemptCounter) {
Objects.requireNonNull(invalidatedTokens, "invalidated_tokens must be provided");
this.invalidatedTokens = invalidatedTokens;
Objects.requireNonNull(notInvalidatedTokens, "not_invalidated_must be provided");
this.prevInvalidatedTokens = notInvalidatedTokens;
if (null != errors) {
this.errors = errors;
} else {
this.errors = new String[0];
}
this.attemptCounter = attemptCounter;
}

public static TokensInvalidationResult emptyResult(){
Copy link
Member

Choose a reason for hiding this comment

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

nit: space between ( and {

return new TokensInvalidationResult(new String[0], new String[0], new String[0], 0);
}

public String[] getInvalidatedTokens() {
return invalidatedTokens;
}

public String[] getPrevInvalidatedTokens() {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
return prevInvalidatedTokens;
}

public String[] getErrors() {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
return errors;
}

public int getAttemptCounter() {
return attemptCounter;
}

public static void writeTo(TokensInvalidationResult result, StreamOutput out) throws IOException {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
out.writeVInt(result.getInvalidatedTokens().length);
out.writeStringArray(result.getInvalidatedTokens());
out.writeVInt(result.getPrevInvalidatedTokens().length);
out.writeStringArray(result.getPrevInvalidatedTokens());
out.writeVInt(result.getErrors().length);
out.writeStringArray(result.getErrors());
out.writeVInt(result.getAttemptCounter());
}

public static TokensInvalidationResult readFrom(StreamInput in) throws IOException {
int invalidatedTokensSize = in.readVInt();
String[] invalidatedTokens = in.readStringArray();
int prevInvalidatedTokensSize = in.readVInt();
String[] prevUnvalidatedTokens = in.readStringArray();
int errorsSize = in.readVInt();
String[] errors = in.readStringArray();
int attemptCounter = in.readVInt();
return new TokensInvalidationResult(invalidatedTokens, prevUnvalidatedTokens, errors, attemptCounter);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject()
.field("invalidated_tokens", invalidatedTokens.length)
.field("prev_invalidated_tokens", prevInvalidatedTokens.length)
.startObject("errors")
.field("size", errors.length);
if (errors.length > 0) {
builder.field("error_messages");
builder.startArray();
for (String error : errors) {
builder.value(error);
}
builder.endArray();
}
builder.endObject()
.endObject();
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,10 @@ public InvalidateTokenRequestBuilder prepareInvalidateToken(String token) {
return new InvalidateTokenRequestBuilder(client).setTokenString(token);
}

public InvalidateTokenRequestBuilder prepareInvalidateToken() {
return new InvalidateTokenRequestBuilder(client);
}

public void invalidateToken(InvalidateTokenRequest request, ActionListener<InvalidateTokenResponse> listener) {
client.execute(InvalidateTokenAction.INSTANCE, request, listener);
}
Expand Down
Loading