Skip to content

Commit

Permalink
Return API KEY name in _authentication response
Browse files Browse the repository at this point in the history
Responses to POST /_security/api_key includes id, name, metadata,
api_key (shared secret), and encoded (base64 of id:api_key).

Requests to GET /_security/_authenticate returns data about the user,
but not the API KEY.

When authenticating using an API KEY, return API KEY info map in the
response. The initial feature request asked for 'name'. However, the
request's Authentication header contains 'encoded', so the decoded
'id' will be returned for convenience too.

When authenticating using any other method, API KEY info map is
omitted.

Closes elastic#70306
  • Loading branch information
justincr-elastic committed Oct 11, 2021
1 parent 5aa200c commit 2442918
Show file tree
Hide file tree
Showing 34 changed files with 396 additions and 186 deletions.
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Debug_Elasticsearch.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,29 @@ public final class AuthenticateResponse implements ToXContentObject {
static final ParseField REALM_TYPE = new ParseField("type");
static final ParseField AUTHENTICATION_TYPE = new ParseField("authentication_type");
static final ParseField TOKEN = new ParseField("token");
static final ParseField API_KEY_INFO = new ParseField("api_key"); // authentication.api_key={"id":"abc123","name":"my-api-key"}
static final ParseField API_KEY_INFO_ID = new ParseField("id");
static final ParseField API_KEY_INFO_NAME = new ParseField("name");

@SuppressWarnings("unchecked")
private static final ConstructingObjectParser<AuthenticateResponse, Void> PARSER = new ConstructingObjectParser<>(
"client_security_authenticate_response", true,
a -> new AuthenticateResponse(
new User((String) a[0], ((List<String>) a[1]), (Map<String, Object>) a[2],
"client_security_authenticate_response", true,
a -> new AuthenticateResponse(
new User((String) a[0], ((List<String>) a[1]), (Map<String, Object>) a[2],
(String) a[3], (String) a[4]), (Boolean) a[5], (RealmInfo) a[6], (RealmInfo) a[7], (String) a[8],
(Map<String, Object>) a[9]));
(Map<String, Object>) a[9], (ApiKeyInfo) a[10]));

static {
final ConstructingObjectParser<RealmInfo, Void> realmInfoParser = new ConstructingObjectParser<>("realm_info", true,
a -> new RealmInfo((String) a[0], (String) a[1]));
realmInfoParser.declareString(constructorArg(), REALM_NAME);
realmInfoParser.declareString(constructorArg(), REALM_TYPE);

final ConstructingObjectParser<ApiKeyInfo, Void> apiKeyInfoParser = new ConstructingObjectParser<>("api_key", true,
a -> new ApiKeyInfo((String) a[0], (String) a[1]));
apiKeyInfoParser.declareString(constructorArg(), API_KEY_INFO_ID);
apiKeyInfoParser.declareString(constructorArg(), API_KEY_INFO_NAME);

PARSER.declareString(constructorArg(), USERNAME);
PARSER.declareStringArray(constructorArg(), ROLES);
PARSER.<Map<String, Object>>declareObject(constructorArg(), (parser, c) -> parser.map(), METADATA);
Expand All @@ -67,6 +77,7 @@ public final class AuthenticateResponse implements ToXContentObject {
PARSER.declareObject(constructorArg(), realmInfoParser, LOOKUP_REALM);
PARSER.declareString(constructorArg(), AUTHENTICATION_TYPE);
PARSER.declareObjectOrNull(optionalConstructorArg(), (p, c) -> p.map(), null, TOKEN);
PARSER.declareObjectOrNull(optionalConstructorArg(), apiKeyInfoParser, null, API_KEY_INFO);
}

private final User user;
Expand All @@ -76,20 +87,29 @@ public final class AuthenticateResponse implements ToXContentObject {
private final String authenticationType;
@Nullable
private final Map<String, Object> token;
@Nullable
private final ApiKeyInfo apikeyinfo; // authentication.api_key={"id":"abc123","name":"my-api-key"}

public AuthenticateResponse(User user, boolean enabled, RealmInfo authenticationRealm,
RealmInfo lookupRealm, String authenticationType) {
this(user, enabled, authenticationRealm, lookupRealm, authenticationType, null);
this(user, enabled, authenticationRealm, lookupRealm, authenticationType, null, null);
}

public AuthenticateResponse(User user, boolean enabled, RealmInfo authenticationRealm,
RealmInfo lookupRealm, String authenticationType, @Nullable Map<String, Object> token) {
this(user, enabled, authenticationRealm, lookupRealm, authenticationType, token, null);
}

public AuthenticateResponse(User user, boolean enabled, RealmInfo authenticationRealm,
RealmInfo lookupRealm, String authenticationType, @Nullable Map<String, Object> token,
@Nullable ApiKeyInfo apikeyinfo) {
this.user = user;
this.enabled = enabled;
this.authenticationRealm = authenticationRealm;
this.lookupRealm = lookupRealm;
this.authenticationType = authenticationType;
this.token = token;
this.apikeyinfo = apikeyinfo;
}

/**
Expand Down Expand Up @@ -130,6 +150,10 @@ public Map<String, Object> getToken() {
return token;
}

public ApiKeyInfo getApiKeyInfo() {
return apikeyinfo;
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
Expand All @@ -155,6 +179,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
if (token != null) {
builder.field(AuthenticateResponse.TOKEN.getPreferredName(), token);
}
if (apikeyinfo != null) {
builder.startObject(AuthenticateResponse.API_KEY_INFO.getPreferredName());
builder.field(AuthenticateResponse.API_KEY_INFO_ID.getPreferredName(), apikeyinfo.getId());
builder.field(AuthenticateResponse.API_KEY_INFO_NAME.getPreferredName(), apikeyinfo.getName());
builder.endObject();
}
return builder.endObject();
}

Expand All @@ -168,12 +198,13 @@ public boolean equals(Object o) {
Objects.equals(authenticationRealm, that.authenticationRealm) &&
Objects.equals(lookupRealm, that.lookupRealm) &&
Objects.equals(authenticationType, that.authenticationType) &&
Objects.equals(token, that.token);
Objects.equals(token, that.token) &&
Objects.equals(apikeyinfo, that.apikeyinfo);
}

@Override
public int hashCode() {
return Objects.hash(user, enabled, authenticationRealm, lookupRealm, authenticationType, token);
return Objects.hash(user, enabled, authenticationRealm, lookupRealm, authenticationType, token, apikeyinfo);
}

public static AuthenticateResponse fromXContent(XContentParser parser) throws IOException {
Expand Down Expand Up @@ -211,4 +242,36 @@ public int hashCode() {
return Objects.hash(name, type);
}
}

public static class ApiKeyInfo { // authentication.api_key={"id":"abc123","name":"my-api-key"}
private String id;
private String name;

ApiKeyInfo(String id, String name) {
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ApiKeyInfo that = (ApiKeyInfo) o;
return Objects.equals(this.id, that.id) &&
Objects.equals(this.name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,10 @@ protected AuthenticateResponse createTestInstance() {
final String fullName = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
final String email = randomFrom(random(), null, randomAlphaOfLengthBetween(0, 4));
final boolean enabled = randomBoolean();
final String authenticationRealmName = randomAlphaOfLength(5);
final String authenticationRealmType = randomFrom(
"service_account");

final String authenticationRealmType = randomFrom("service_account");
final AuthenticateResponse.RealmInfo authenticationRealm =
new AuthenticateResponse.RealmInfo(authenticationRealmName, authenticationRealmType);
new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), authenticationRealmType);

final AuthenticateResponse.RealmInfo lookupRealm;
final Map<String, Object> tokenInfo;
Expand All @@ -81,9 +80,16 @@ protected AuthenticateResponse createTestInstance() {

final String authenticationType = randomFrom("realm", "api_key", "token", "anonymous", "internal");

final AuthenticateResponse.ApiKeyInfo apiKeyInfo;
if ("api_key".equals(authenticationType)) {
apiKeyInfo = new AuthenticateResponse.ApiKeyInfo(randomAlphaOfLength(16), randomAlphaOfLength(20));
} else {
apiKeyInfo = null;
}

return new AuthenticateResponse(
new User(username, roles, metadata, fullName, email), enabled, authenticationRealm,
lookupRealm, authenticationType, tokenInfo);
lookupRealm, authenticationType, tokenInfo, apiKeyInfo);
}

private void toXContent(AuthenticateResponse response, XContentBuilder builder) throws IOException {
Expand All @@ -95,57 +101,67 @@ private AuthenticateResponse copy(AuthenticateResponse response) {
final User copyUser = new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail());
return new AuthenticateResponse(copyUser, response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(), response.getAuthenticationType(), Map.copyOf(response.getToken()));
response.getLookupRealm(), response.getAuthenticationType(), Map.copyOf(response.getToken()), response.getApiKeyInfo());
}

private AuthenticateResponse mutate(AuthenticateResponse response) {
final User originalUser = response.getUser();
switch (randomIntBetween(1, 10)) {
int randomSwitchCase = randomIntBetween(1, 11); // range is inclusive
switch (randomSwitchCase) {
case 1:
return new AuthenticateResponse(new User(originalUser.getUsername() + "wrong", originalUser.getRoles(),
originalUser.getMetadata(), originalUser.getFullName(), originalUser.getEmail()), response.enabled(),
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType(), response.getToken());
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 2:
final List<String> wrongRoles = new ArrayList<>(originalUser.getRoles());
wrongRoles.add(randomAlphaOfLengthBetween(1, 4));
return new AuthenticateResponse(new User(originalUser.getUsername(), wrongRoles, originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(), response.getAuthenticationType(), response.getToken());
response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 3:
final Map<String, Object> wrongMetadata = new HashMap<>(originalUser.getMetadata());
wrongMetadata.put("wrong_string", randomAlphaOfLengthBetween(0, 4));
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), wrongMetadata,
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(), response.getAuthenticationType(), response.getToken());
response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 4:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName() + "wrong", originalUser.getEmail()), response.enabled(),
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType());
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 5:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail() + "wrong"), response.enabled(),
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType(), response.getToken());
response.getAuthenticationRealm(), response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 6:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled() == false, response.getAuthenticationRealm(),
response.getLookupRealm(), response.getAuthenticationType(), response.getToken());
response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 7:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5)),
response.getAuthenticationType(), response.getToken());
response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 8:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(),
new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5)), response.getLookupRealm(),
response.getAuthenticationType(), response.getToken());
response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo());
case 9:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(),
randomValueOtherThan(response.getAuthenticationType(),
() -> randomFrom("realm", "api_key", "token", "anonymous", "internal")), response.getToken());
default:
() -> randomFrom("realm", "api_key", "token", "anonymous", "internal")), response.getToken(),
response.getApiKeyInfo());
case 10:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(),
Expand All @@ -155,8 +171,18 @@ private AuthenticateResponse mutate(AuthenticateResponse response) {
randomFrom(Map.of(
"name", randomValueOtherThan(response.getToken().get("name"), () -> randomAlphaOfLengthBetween(3, 8)),
"type", randomValueOtherThan(response.getToken().get("type"), () -> randomAlphaOfLengthBetween(3, 8))
), null));

), null),
response.getApiKeyInfo());
case 11:
return new AuthenticateResponse(new User(originalUser.getUsername(), originalUser.getRoles(), originalUser.getMetadata(),
originalUser.getFullName(), originalUser.getEmail()), response.enabled(), response.getAuthenticationRealm(),
response.getLookupRealm(), response.getAuthenticationType(), response.getToken(),
response.getApiKeyInfo() == null
? new AuthenticateResponse.ApiKeyInfo(randomAlphaOfLength(16), randomAlphaOfLength(20)) : null
);
default:
fail("Random number " + randomSwitchCase + " did not match any switch cases");
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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

import static org.hamcrest.Matchers.equalTo;

Expand All @@ -30,9 +31,11 @@ public void testFromXContent() throws IOException {
final String type = randomAlphaOfLength(6);
final String kerberosAuthenticationResponseToken = randomBoolean() ? null : randomAlphaOfLength(7);
final AuthenticateResponse authentication = new AuthenticateResponse(new User(randomAlphaOfLength(7),
Arrays.asList( randomAlphaOfLength(9) )),
true, new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(7) ),
new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5) ), "realm");
Arrays.asList(randomAlphaOfLength(9))),
true, new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(7)),
new AuthenticateResponse.RealmInfo(randomAlphaOfLength(5), randomAlphaOfLength(5)), "realm",
Map.of("servicetoken1", randomAlphaOfLengthBetween(3, 8)),
new AuthenticateResponse.ApiKeyInfo(randomAlphaOfLength(16), randomAlphaOfLength(20)));

final XContentType xContentType = randomFrom(XContentType.values());
final XContentBuilder builder = XContentFactory.contentBuilder(xContentType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ AuthenticateResponse createServerAuthenticationResponse(Authentication authentic
authentication.getLookedUpBy() == null?
authentication.getAuthenticatedBy().getType(): authentication.getLookedUpBy().getType());
return new AuthenticateResponse(cUser, user.enabled(), authenticatedBy, lookedUpBy,
authentication.getAuthenticationType().toString().toLowerCase(Locale.ROOT));
authentication.getAuthenticationType().toString().toLowerCase(Locale.ROOT), null, null);
}
}
Loading

0 comments on commit 2442918

Please sign in to comment.