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

Expose API key name to the ingest pipeline #51305

Merged
merged 17 commits into from
Feb 10, 2020

Conversation

ywangd
Copy link
Member

@ywangd ywangd commented Jan 22, 2020

API key name is an important piece of information for traceability when
data are being ingested from multiple agents.
This becomes more relevant with the incoming support of required
pipelines (#46847)

Resolves: #49106

Working code, pending tests and check
@ywangd ywangd added >enhancement :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) v8.0.0 v7.7.0 labels Jan 22, 2020
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-security (:Security/Authentication)

@ywangd
Copy link
Member Author

ywangd commented Jan 22, 2020

This PR is WIP. I'd like to have an early PR to increase visibility and easier discussion.

@jrodewig jrodewig added the WIP label Jan 22, 2020
Object apiKeyName = authentication.getMetadata().get(ApiKeyService.API_KEY_NAME_KEY);
if (apiKeyName != null) {
userObject.put("api_key_name", apiKeyName);
}
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": "..."
   }
}   

@tvernum
Copy link
Contributor

tvernum commented Jan 24, 2020

@ywangd for WIP PRs, you can create them as a Draft PR when you open it in GitHub, which makes it more obvious to other observers, and also prevents them from being accidentally merged.

@ywangd
Copy link
Member Author

ywangd commented Jan 29, 2020

@elasticmachine run elasticsearch-ci/default-distro

@ywangd ywangd removed the WIP label Jan 29, 2020
@ywangd ywangd requested a review from tvernum January 29, 2020 12:32
@@ -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

@ywangd
Copy link
Member Author

ywangd commented Jan 31, 2020

This is now a formal PR ready for reivew. I removed the WIP label two days ago but somehow left the announcement in another PR ... Anyway, it's ready and please bring it on :)

@@ -85,6 +86,39 @@ public IngestDocument execute(IngestDocument ingestDocument) throws Exception {
userObject.put("metadata", user.metadata());
}
break;
case API_KEY:
final HashMap<String, Object> apiKey = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Per #51454, if userObject has an api_key field already, and it's an object (Map) then we should add new fields to that object, not overwrite it.
Likewise for realm.

Copy link
Member Author

@ywangd ywangd Feb 3, 2020

Choose a reason for hiding this comment

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

Updated for api_key and realm. Also added test to make sure they are not overwritten.

through API key. It is an object containing two fields, `id` and `name`.
When using API key authentication, the `realm` property refers to the realm
from which the API key is created. It is also an object with two fields,
`name` and `type`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think describing the realm property in the space of API Keys would lead some users to read it as only applying to API Keys. It would be better to say something like:

The `api_key` property is only applicable when the authentication is
through API key. It is an object containing two fields, `id` and `name`.

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. 

That said, I think we ought to actually expand this whole section and give examples, etc.
I'd be happy to see that in a followup PR though.

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. I would also prefer to have the example as a separate PR. The example code may need to be added as part of doc tests which require some work. Should I create an issue for it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Should I create an issue for it?

Yes please.

Copy link
Member Author

Choose a reason for hiding this comment

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

Issue created: #51907

break;
case REALM:
final Object realmName = ApiKeyService.getCreatorRealmName(authentication);
final Object realmType = ApiKeyService.getCreatorRealmType(authentication);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's a mismatch here.

The API Key service populates the API key creator realm based on the looked-up-by.
But the getCreatorRealm* methods return the authenticating realm, not the looked up realm.

So, if I do curl -u elastic -H "es-security-runas-user: foo" PUT /index/_doc/1 the pipeline will record the realm as reserved

But if I do curl -u elastic -H "es-security-runas-user: foo" PUT /_security/api_key ... and then curl -H "Authorization: ApiKey .... PUT /index/_doc/1 it will have a realm of native.

It's not a big deal, but it's inconsistent. I think we want to always use the looked-up-by in the ingest processor, maybe with a flag of lookup: true or something like that if the authenticated realm was different.

Copy link
Member Author

Choose a reason for hiding this comment

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

You are right. I think the first case should report realm as native as well. This should be easily achieved once we augument the Authentication class with a new method to resolve the actual realm.

As for the lookup: true flag, do you mean add it as part of the SetSecurityUserProcessor configuration?

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 for both name and type.

@tvernum
Copy link
Contributor

tvernum commented Feb 3, 2020

What would people say about renaming existing fields, to better align to the ECS specification?

I think it would be good to introduce an ECS mode to this processor like we did in the user-agent processor and gradually move to ECS being the default and, eventually, the only mode.

@ywangd, would you open a new issue for that, and tag it both :Security and :Core/Features/Ingest so we can get some discussion on that?

Edit: I just read a bit further and realised that it might only be a single field that we need to change. I still think it's worth doing - the username can be a pretty important field in ECS - but it might mean that we don't prioritise it straight away.

@ywangd
Copy link
Member Author

ywangd commented Feb 3, 2020

would you open a new issue for that, and tag it both :Security and :Core/Features/Ingest so we can get some discussion on that?

Issue created: #51799

@ywangd ywangd requested a review from tvernum February 3, 2020 13:22
@ywangd
Copy link
Member Author

ywangd commented Feb 3, 2020

@elasticmachine run elasticsearch-ci/2

@@ -76,6 +76,10 @@ public RealmRef getLookedUpBy() {
return lookedUpBy;
}

public RealmRef getNominalRealm() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public RealmRef getNominalRealm() {
public RealmRef getRealm() {

Copy link
Contributor

@tvernum tvernum Feb 5, 2020

Choose a reason for hiding this comment

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

I think getRealm is a bit too general - I worry about someone just deciding to call getRealm() without thinking about the fact that there's 2 possible realms that might be in play and they need to be intentional about which one to use.

Maybe getSourceRealm()?

The better long term fix would actually be to make getLookedUpBy() always return a realm (that is, is uses this implementation) and then have a isUserLookup() if you need to know whether there was a specific (separate) lookup realm.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree getRealm is too general. I am OK with getSourceRealm and will go with it.

Is User#isRunAs functionally equivelent to the isUserLookup method you propose?

Copy link
Contributor

@tvernum tvernum Feb 10, 2020

Choose a reason for hiding this comment

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

Logically equivalent yes, but it's implemented by looking at different fields, so I'd rather have both implementations so that the one in Authentication is specifically about whether lookupRealm is populated.

Copy link
Contributor

@albertzaharovits albertzaharovits left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@@ -886,7 +884,7 @@ 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_NAME);
} else {
return authentication.getAuthenticatedBy().getName();
return authentication.getNominalRealm().getName();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need to really consider whether this is the change we want.
It's a fairly fundamental change to the ApiKey service, and it affects how the manage_own_api_key privilege would work.
It might be right, I'd need to think about it, but if it is, then we might consider it a breaking change.

Copy link
Member Author

Choose a reason for hiding this comment

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

Here is my understanding:

In generaly, I feel the changed behaviour is correct, though you are right about it is a breaking change.

The above code is only used in TransportInvalidateApiKeyAction and TransportGetApiKeyAction. The else part will be called when the authentication is not of ApiKey auth type. Since ApiKey auth cannot perform runAs, this means theelse part will only makes a difference when userA runs as userB. In this case, the existing code will return userA realm, while the changes will return userB realm.

I can think of two reasons to support the changed behaviour:

  • When apikey is created, it records the runas user (if there is one) and runas/lookedUpBy realm. It feels correct that the same runas/lookedUpBy info should be checked when retrieve/invalidate the apikey.
  • When you run as someone else, all permissions and stuff should be checked/recorded as someone else.

I don't quite understand how manage_own_api_key work, the document says

All security-related operations on Elasticsearch API keys that are 
owned by the current authenticated user. The operations include 
creating new API keys, retrieving information about API keys, 
and invalidating API keys.

Does this mean you will not be able to create an API key for yourself unless you have this priviledge? If this is the case, I don't see how this is related to the change. Could you please elaborate on it?

Copy link
Contributor

Choose a reason for hiding this comment

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

Tim saved the day, I completely missed this 😞 .

@yang I think what you're describing is mostly correct.

Does this mean you will not be able to create an API key for yourself unless you have this priviledge?

Yes

If this is the case, I don't see how this is related to the change. Could you please elaborate on it?

Let's say you run-as and create an API key. The user that you run as only has the manage_own_api_key privilege. The creation would work. But then invalidation (and get), also using the run-as, won't work because the API Key Service would search the APi Key as incorrectly belonging to a user in the realm that authenticated the caller, whereas the API Key is created under the run-as user. (Note, I haven't actually tested this scenario).

In the interest of this PR, I would revert to the previous behavior in the TransportInvalidateApiKeyAction and TransportGetApiKeyAction , and, assuming you agree this is a bug, you should open a bug issue with your finding.

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to simulate the scenario you described with two users foo and bar, where foo is a superuser and bar only has manage_own_api_key for cluster and read for all indices. ES version 7.5.2

It behaves pretty much as you described when requests are made with owner=true. Given foo creates an ApiKey while runas bar, the request
curl -u foo:password -H 'es-security-runas-user: bar' 'localhost:9200/_security/api_key?id=keyId&owner=true' returns empty results.

While I do believe the correct behaviour of above request should be returning the api key, I also think it is better to have a separate PR for this as it is not related to the current issue.

PS: I also found if bar has only manage_own_api_key priviledge, it is not possible to get or delete the api key. I got error says action [cluster:admin/xpack/security/api_key/invalidate] is unauthorized for user [bar]. Once I add manage_api_key to bar, everything works.

Copy link
Member Author

Choose a reason for hiding this comment

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

I reverted the logic and created #51975 to track it. Thanks

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you Yang! But currently I don't think we want to set the authenticating realm in the case of the set security user processor, see my other comment: https://github.com/elastic/elasticsearch/pull/51305/files?file-filters%5B%5D=.java#r375923902

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. Please see my other comment.

@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.

@ywangd
Copy link
Member Author

ywangd commented Feb 6, 2020

@elasticmachine run elasticsearch-ci/bwc run elasticsearch-ci/default-distro run elasticsearch-ci/packaging-sample-matrix-windows

@ywangd ywangd requested a review from tvernum February 8, 2020 14:19
Copy link
Contributor

@tvernum tvernum left a comment

Choose a reason for hiding this comment

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

LGTM.

@ywangd ywangd merged commit 5c9f795 into elastic:master Feb 10, 2020
ywangd added a commit to ywangd/elasticsearch that referenced this pull request Feb 10, 2020
The changes add more granularity for identiying the data ingestion user.
The ingest pipeline can now be configure to record authentication realm and
type. It can also record API key name and ID when one is in use. 
This improves traceability when data are being ingested from multiple agents
and will become more relevant with the incoming support of required
pipelines (elastic#46847)

Resolves: elastic#49106
ywangd added a commit that referenced this pull request Feb 11, 2020
The changes add more granularity for identiying the data ingestion user.
The ingest pipeline can now be configure to record authentication realm and
type. It can also record API key name and ID when one is in use. 
This improves traceability when data are being ingested from multiple agents
and will become more relevant with the incoming support of required
pipelines (#46847)

Resolves: #49106
albertzaharovits added a commit that referenced this pull request Jul 14, 2020
The `Authentication` object that gets built following an API Key authentication
contains the realm name of the owner user that created the key (which is audited),
but the specific field used for storing it changed in #51305 .

This PR makes it so that auditing tolerates an "unfound" realm name, so it doesn't
throw an NPE, because the owner realm name is not found in the expected field.

Closes #59425
albertzaharovits added a commit that referenced this pull request Jul 14, 2020
The `Authentication` object that gets built following an API Key authentication
contains the realm name of the owner user that created the key (which is audited),
but the specific field used for storing it changed in #51305 .

This PR makes it so that auditing tolerates an "unfound" realm name, so it doesn't
throw an NPE, because the owner realm name is not found in the expected field.

Closes #59425
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>enhancement :Security/Authentication Logging in, Usernames/passwords, Realms (Native/LDAP/AD/SAML/PKI/etc) v7.7.0 v8.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expose API key name to the ingest pipeline
6 participants