Skip to content

Expose API key name to the ingest pipeline #51305

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 17 commits into from
Feb 10, 2020
Merged
Show file tree
Hide file tree
Changes from 14 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
17 changes: 12 additions & 5 deletions docs/reference/ingest/processors/set-security-user.asciidoc
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
[[ingest-node-set-security-user-processor]]
=== Set Security User Processor
Sets user-related details (such as `username`, `roles`, `email`, `full_name`
and `metadata` ) from the current
Sets user-related details (such as `username`, `roles`, `email`, `full_name`,
`metadata`, `api_key`, `realm` and `authentication_type`) from the current
authenticated user to the current document by pre-processing the ingest.
The `api_key` property exists only if the user authenticates with an
API key. It is an object containing the `id` and `name` fields of the API key.
The `realm` property is also an object with two fields, `name` and `type`.
When using API key authentication, the `realm` property refers to the realm
from which the API key is created.
The `authentication_type` property is a string that can take value from
`REALM`, `API_KEY`, `TOKEN` and `ANONYMOUS`.

IMPORTANT: Requires an authenticated user for the index request.

[[set-security-user-options]]
.Set Security User Options
[options="header"]
|======
| Name | Required | Default | Description
| `field` | yes | - | The field to store the user information into.
| `properties` | no | [`username`, `roles`, `email`, `full_name`, `metadata`] | Controls what user related properties are added to the `field`.
| Name | Required | Default | Description
| `field` | yes | - | The field to store the user information into.
| `properties` | no | [`username`, `roles`, `email`, `full_name`, `metadata`, `api_key`, `realm`, `authentication_type`] | Controls what user related properties are added to the `field`.
include::common-options.asciidoc[]
|======

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public RealmRef getLookedUpBy() {
return lookedUpBy;
}

public RealmRef getSourceRealm() {
return lookedUpBy == null ? authenticatedBy : lookedUpBy;
}

public Version getVersion() {
return version;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@
},
"realm" : {
"type" : "keyword"
},
"realm_type" : {
Copy link
Member Author

Choose a reason for hiding this comment

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

Add a new field for realm type as discussed

"type" : "keyword"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
*
* * 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;

import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.xpack.core.security.user.User;

public class AuthenticationTests extends ESTestCase {

public void testWillGetLookedUpByWhenItExists() {
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef("auth_by", "auth_by_type", "node");
final Authentication.RealmRef lookedUpBy = new Authentication.RealmRef("lookup_by", "lookup_by_type", "node");
final Authentication authentication = new Authentication(
new User("user"), authenticatedBy, lookedUpBy);

assertEquals(lookedUpBy, authentication.getSourceRealm());
}

public void testWillGetAuthenticateByWhenLookupIsNull() {
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef("auth_by", "auth_by_type", "node");
final Authentication authentication = new Authentication(
new User("user"), authenticatedBy, null);

assertEquals(authenticatedBy, authentication.getSourceRealm());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ public class ApiKeyService {
private static final Logger logger = LogManager.getLogger(ApiKeyService.class);
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(logger);
public static final String API_KEY_ID_KEY = "_security_api_key_id";
public static final String API_KEY_NAME_KEY = "_security_api_key_name";
public static final String API_KEY_REALM_NAME = "_es_api_key";
public static final String API_KEY_REALM_TYPE = "_es_api_key";
public static final String API_KEY_CREATOR_REALM = "_security_api_key_creator_realm";
public static final String API_KEY_CREATOR_REALM_NAME = "_security_api_key_creator_realm_name";
public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type";
static final String API_KEY_ROLE_DESCRIPTORS_KEY = "_security_api_key_role_descriptors";
static final String API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY = "_security_api_key_limited_by_role_descriptors";

Expand Down Expand Up @@ -271,8 +273,8 @@ XContentBuilder newDocument(SecureString apiKey, String name, Authentication aut
.startObject("creator")
.field("principal", authentication.getUser().principal())
.field("metadata", authentication.getUser().metadata())
.field("realm", authentication.getLookedUpBy() == null ?
authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName())
.field("realm", authentication.getSourceRealm().getName())
.field("realm_type", authentication.getSourceRealm().getType())
.endObject()
.endObject();

Expand Down Expand Up @@ -501,10 +503,12 @@ private void validateApiKeyExpiration(Map<String, Object> source, ApiKeyCredenti
: limitedByRoleDescriptors.keySet().toArray(Strings.EMPTY_ARRAY);
final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true);
final Map<String, Object> authResultMetadata = new HashMap<>();
authResultMetadata.put(API_KEY_CREATOR_REALM, creator.get("realm"));
authResultMetadata.put(API_KEY_CREATOR_REALM_NAME, creator.get("realm"));
authResultMetadata.put(API_KEY_CREATOR_REALM_TYPE, creator.get("realm_type"));
authResultMetadata.put(API_KEY_ROLE_DESCRIPTORS_KEY, roleDescriptors);
authResultMetadata.put(API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, limitedByRoleDescriptors);
authResultMetadata.put(API_KEY_ID_KEY, credentials.getId());
authResultMetadata.put(API_KEY_NAME_KEY, source.get("name"));
listener.onResponse(AuthenticationResult.success(apiKeyUser, authResultMetadata));
} else {
listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null));
Expand Down Expand Up @@ -878,12 +882,27 @@ public void getApiKeys(String realmName, String username, String apiKeyName, Str
*/
public static String getCreatorRealmName(final Authentication authentication) {
if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) {
return (String) authentication.getMetadata().get(API_KEY_CREATOR_REALM);
return (String) authentication.getMetadata().get(API_KEY_CREATOR_REALM_NAME);
} else {
return authentication.getAuthenticatedBy().getName();
}
}

/**
* Returns realm type for the authenticated user.
* If the user is authenticated by realm type {@value API_KEY_REALM_TYPE}
* then it will return the realm type of user who created this API key.
* @param authentication {@link Authentication}
* @return realm type
*/
public static String getCreatorRealmType(final Authentication authentication) {
if (authentication.getAuthenticatedBy().getType().equals(API_KEY_REALM_TYPE)) {
return (String) authentication.getMetadata().get(API_KEY_CREATOR_REALM_TYPE);
} else {
return authentication.getAuthenticatedBy().getType();
}
}

final class CachedApiKeyHashResult {
final boolean success;
final char[] hash;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.elasticsearch.ingest.Processor;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.security.authc.ApiKeyService;

import java.util.Arrays;
import java.util.EnumSet;
Expand Down Expand Up @@ -85,6 +86,47 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
userObject.put("metadata", user.metadata());
}
break;
case API_KEY:
final String apiKey = "api_key";
final Object existingApiKeyField = userObject.get(apiKey);
@SuppressWarnings("unchecked")
final Map<String, Object> apiKeyField =
existingApiKeyField instanceof Map ? (Map<String, Object>) existingApiKeyField : new HashMap<>();
Object apiKeyName = authentication.getMetadata().get(ApiKeyService.API_KEY_NAME_KEY);
if (apiKeyName != null) {
apiKeyField.put("name", apiKeyName);
}
Object apiKeyId = authentication.getMetadata().get(ApiKeyService.API_KEY_ID_KEY);
if (apiKeyId != null) {
apiKeyField.put("id", apiKeyId);
}
if (false == apiKeyField.isEmpty()) {
userObject.put(apiKey, apiKeyField);
}
break;
case REALM:
final String realmKey = "realm";
final Object existingRealmField = userObject.get(realmKey);
@SuppressWarnings("unchecked")
final Map<String, Object> realmField =
existingRealmField instanceof Map ? (Map<String, Object>) existingRealmField : new HashMap<>();
final Object realmName = ApiKeyService.getCreatorRealmName(authentication);
Copy link
Contributor

@albertzaharovits albertzaharovits Feb 6, 2020

Choose a reason for hiding this comment

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

In case you authenticate without an API Key, this gets you the authentication realm, although it should get the lookup realm, otherwise the set security user processor won't work as expected for run-as users.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a good point. No need to let the existing questionable logic propagate to here. I'll implement the logic locally and this also helps get rid of the other newly added getCreatorRealmType method. Thanks!

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated Thanks again for catching this.

if (realmName != null) {
realmField.put("name", realmName);
}
final Object realmType = ApiKeyService.getCreatorRealmType(authentication);
if (realmType != null) {
realmField.put("type", realmType);
}
if (false == realmField.isEmpty()) {
userObject.put(realmKey, realmField);
}
break;
case AUTHENTICATION_TYPE:
if (authentication.getAuthenticationType() != null) {
userObject.put("authentication_type", authentication.getAuthenticationType().toString());
}
Copy link
Contributor

Choose a reason for hiding this comment

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

See my other comment about wanting the id etc as well.
I think this should be a nested object inside the user object.

user: {
   username: "...",
   api_key: {
       name: "...", id: "...", "realm": "..."
   }
}   

break;
default:
throw new UnsupportedOperationException("unsupported property [" + property + "]");
}
Expand Down Expand Up @@ -138,7 +180,10 @@ public enum Property {
FULL_NAME,
EMAIL,
ROLES,
METADATA;
METADATA,
API_KEY,
REALM,
AUTHENTICATION_TYPE;

static Property parse(String tag, String value) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ public void testAuthenticateWithApiKey() throws Exception {
assertThat(auth.getStatus(), is(AuthenticationResult.Status.SUCCESS));
assertThat(auth.getUser(), notNullValue());
assertThat(auth.getUser().principal(), is("hulk"));
assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM_NAME), is("realm1"));
assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM_TYPE), is("native"));
assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_ID_KEY), is(id));
assertThat(auth.getMetadata().get(ApiKeyService.API_KEY_NAME_KEY), is("test"));
}

public void testAuthenticationIsSkippedIfLicenseDoesNotAllowIt() throws Exception {
Expand Down Expand Up @@ -284,6 +288,7 @@ public void testValidateApiKey() throws Exception {
Map<String, Object> creatorMap = new HashMap<>();
creatorMap.put("principal", "test_user");
creatorMap.put("realm", "realm1");
creatorMap.put("realm_type", "realm_type1");
creatorMap.put("metadata", Collections.emptyMap());
sourceMap.put("creator", creatorMap);
sourceMap.put("api_key_invalidated", false);
Expand All @@ -302,7 +307,7 @@ public void testValidateApiKey() throws Exception {
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors")));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY),
equalTo(sourceMap.get("limited_by_role_descriptors")));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM), is("realm1"));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM_NAME), is("realm1"));

sourceMap.put("expiration_time", Clock.systemUTC().instant().plus(1L, ChronoUnit.HOURS).toEpochMilli());
future = new PlainActionFuture<>();
Expand All @@ -316,7 +321,7 @@ public void testValidateApiKey() throws Exception {
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors")));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY),
equalTo(sourceMap.get("limited_by_role_descriptors")));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM), is("realm1"));
assertThat(result.getMetadata().get(ApiKeyService.API_KEY_CREATOR_REALM_NAME), is("realm1"));

sourceMap.put("expiration_time", Clock.systemUTC().instant().minus(1L, ChronoUnit.HOURS).toEpochMilli());
future = new PlainActionFuture<>();
Expand Down Expand Up @@ -561,6 +566,22 @@ public void testApiKeyCacheDisabled() {
assertNull(cachedApiKeyHashResult);
}

public void testWillAlwaysGetAuthenticationRealmName() {
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef("auth_by", "auth_by_type", "node");
final Authentication.RealmRef lookedUpBy = new Authentication.RealmRef("lookup_by", "lookup_by_type", "node");
final Authentication authentication = new Authentication(
new User("user"), authenticatedBy, lookedUpBy);
assertEquals("auth_by", ApiKeyService.getCreatorRealmName(authentication));
}

public void testWillAlwaysGetAuthenticationRealmType() {
final Authentication.RealmRef authenticatedBy = new Authentication.RealmRef("auth_by", "auth_by_type", "node");
final Authentication.RealmRef lookedUpBy = new Authentication.RealmRef("lookup_by", "lookup_by_type", "node");
final Authentication authentication = new Authentication(
new User("user"), authenticatedBy, lookedUpBy);
assertEquals("auth_by_type", ApiKeyService.getCreatorRealmType(authentication));
}

private ApiKeyService createApiKeyService(Settings baseSettings) {
final Settings settings = Settings.builder()
.put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true)
Expand Down
Loading