Adding authc.grantAPIKeyAsInternalUser #60423
Conversation
| const [scheme] = authorizationHeaderValue.split(/\s+/); | ||
| const credentials = authorizationHeaderValue.substring(scheme.length + 1); | ||
|
|
||
| return new HTTPAuthorizationHeader(scheme, credentials); |
There was a problem hiding this comment.
With the old implementation of getHTTPAuthenticationScheme, .toLowerCase() was being called when initially parsing the scheme from the request's headers. This felt inconsistent when I began to use the HTTPAuthorizationHeader class and .toString() to create the Authorization headers themselves. It has the effect of requiring consumers to call .toLowerCase(), or instead use .localeCompare, to treat this as being case insensitive.
@azasypkin I'm interested in how you feel about this.
There was a problem hiding this comment.
It's fine by me as long as we mention this in the property JS docs. I'm curious if there is any reason we cannot do toLowerCase() for both scheme and credentials in HTTPAuthorizationHeader constructor though?
There was a problem hiding this comment.
We definitely can do this in the constructor. When we were previously building the Authorization header manually within the auth providers, we were using a capitalized version, for example: Bearer ${accessToken}. Changing this behavior for the sake of making matching the scheme easier felt wrong. However, I don't want to be introducing future bugs by being pedantic... I wish there was CaseInsensitiveString in JavaScript which overrode equals, etc. but AFAIK there is no such thing, or no ability to create one ourselves which would work with things like a Map and a Set.
There was a problem hiding this comment.
Changing this behavior for the sake of making matching the scheme easier felt wrong. However, I don't want to be introducing future bugs by being pedantic...
Yeah, I agree. We use and compare schemes just in a couple of places, I think the risk of accidentally breaking something in this area may be negligible.
x-pack/plugins/security/server/authentication/http_authentication/http_authorization_header.ts
Outdated
Show resolved
Hide resolved
…ion/http_authorization_header.ts Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com>
|
@tvernum I've been able to make the current ES Grant API key endpoint work, but it requires that Kibana parse the |
mikecote
left a comment
There was a problem hiding this comment.
Tested this locally. I changed some alerting code to make the alerting APIs use this new API and everything worked based on @kobelb temporary workaround. I tested with a user who didn't have any access to API keys and Kibana allowed the creation of alerts. Tested the API key the alert created to ensure it's usable, etc and it worked! LGTMike 👍
| * Tries to grant an API key for the current user. | ||
| * @param request Request instance. | ||
| */ | ||
| async grant(request: KibanaRequest): Promise<GrantAPIKeyResult | null> { |
There was a problem hiding this comment.
We chatted a bit about the function naming. Did you decide to keep as is or go with something like grantAsInternalUser?
|
ACK: will review today |
|
@elasticmachine merge upstream |
authc.grantAPIKey authc.grantAPIKeyAsInternalUser
azasypkin
left a comment
There was a problem hiding this comment.
Looks great, thanks! Tested locally with both SAML and Basic.
...plugins/security/server/authentication/http_authentication/http_authorization_header.test.ts
Outdated
Show resolved
Hide resolved
...ity/server/authentication/http_authentication/basic_http_authorization_header_credentials.ts
Outdated
Show resolved
Hide resolved
|
|
||
| const authHeaders = { | ||
| authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, | ||
| authorization: new HTTPAuthorizationHeader( |
There was a problem hiding this comment.
note: would you mind also updating https://github.com/elastic/kibana/blob/master/x-pack/plugins/security/server/routes/users/change_password.ts#L46 to use HTTPAuthorizationHeader?
| interface GrantAPIKeyParams { | ||
| grant_type: 'password' | 'access_token'; | ||
| username?: string; | ||
| password?: string; | ||
| access_token?: string; | ||
| } |
There was a problem hiding this comment.
optional nit: how do you feel about something like this instead?
| interface GrantAPIKeyParams { | |
| grant_type: 'password' | 'access_token'; | |
| username?: string; | |
| password?: string; | |
| access_token?: string; | |
| } | |
| type GrantAPIKeyParams = | |
| | { grant_type: 'password'; username: string; password: string } | |
| | { grant_type: 'access_token'; access_token: string }; |
There was a problem hiding this comment.
question: hmm I'm surprised that Elasticsearch doesn't allow client to specify Api Key name like it does in create API key API. Do you know why? Just out of curiosity.
There was a problem hiding this comment.
I don't, unfortunately. It also doesn't allow you to specify a role descriptor. It's not blocking Alerting's usage, so I :ostrich_head_in_sand:'ed it.
| result = (await this.clusterClient | ||
| .asScoped(request) | ||
| .callAsInternalUser('shield.grantAPIKey', { body: params })) as GrantAPIKeyResult; |
There was a problem hiding this comment.
question: I guess we don't need asScoped here (as well as as GrantAPIKeyResult)?
| result = (await this.clusterClient | |
| .asScoped(request) | |
| .callAsInternalUser('shield.grantAPIKey', { body: params })) as GrantAPIKeyResult; | |
| result = await this.clusterClient.callAsInternalUser('shield.grantAPIKey', { body: params }); |
There was a problem hiding this comment.
The conceptual equivalent to an asScoped "grant" is just createAPIKey. It's safe for us to always allow end-users to use the create API Key API in Elasticsearch and ES will enforce all of the necessary authorization for this to be safe. The ability to grant an API Key in this manner is a new "two credentials API" where the Kibana server should only be able to perform the operation when it has the user's credentials, and the result of the operation should be safe-guarded...
There was a problem hiding this comment.
Yeah, but I what I meant is that you're using callAsInternalUser and callAsInternalUser on ScopedClusterClient is exactly the same as on non-scoped ClusterClient and doesn't use request headers you scoped client with. So unless I'm missing something clusterClient.asScoped(request).callAsInternalUser is a bit less performant equivalent of just clusterClient.callAsInternalUser.
There was a problem hiding this comment.
I'm with you now, apologies for being dense. Fix forthcoming.
| } | ||
|
|
||
| this.logger.debug('Trying to grant an API key'); | ||
| const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request); |
There was a problem hiding this comment.
note: I'm not super happy that we should do that, but I can't think of a better solution for the time being as well. So +1.
Just for the records: my main concern is that the fact that we have Authorization header within request.headers is just "temporary" backward compatibility escape hatch for the places that still rely on legacy requests. The idea was to store authHeaders returned from auth hook internally in the core so that it can add them automatically whenever authenticated requested is relayed to ES. And nothing else would have access to them.
However we (Security) register and own this hook, provide auth headers and hence should have an exclusive right to access these headers whenever we need them (e.g. via Core "auth headers accessor" returned as part of the auth hook registration). So even if Core decides to no longer populate request.headers with Authorization we'll still have access to them via some API. Migrating to this API will be an implementation detail that won't change public API we expose unless I'm missing something.
There was a problem hiding this comment.
What you've said makes sense. I'm also open to alternatives which require some changes to the authentication providers. I began exploring what it'd look like to allow the auth providers to store the parameters we need to grant an API key in a WeakMap<KibanaRequest, GrantAPIKeyParams> that only the security plugin had access to, but then came on the necessity to parse the Authorization header within the http authentication provider and decided to just do the header parsing logic here.
There was a problem hiding this comment.
Yeah, I think what you have now is the best option for the time being. Parsing headers because of http is where I ended up as well when we had initial discussion on that requirement.
It feels we're still missing some pieces either in Security plugin or in the Core that would have allowed us to come with a more future-proof approach, but I'm sure we'll get there and reuse the work you've done here anyway.
Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-Authored-By: Mike Côté <mikecote@users.noreply.github.com>
💚 Build SucceededHistory
To update your PR or re-run it, just comment with: |
* master: [Uptime] Skip failing location test temporarily (elastic#60938) [ML] Disabling datafeed editing when job is running (elastic#60751) Adding `authc.invalidateAPIKeyAsInternalUser` (elastic#60717) [SIEM] Add license check to ML Rule form (elastic#60691) Adding `authc.grantAPIKeyAsInternalUser` (elastic#60423) Support Histogram Data Type (elastic#59387) [Upgrade Assistant] Fix edge case where reindex op can falsely be seen as stale (elastic#60770) [SIEM] [Cases] Update case icons (elastic#60812) [TSVB] Fix percentiles band mode (elastic#60741)
* Parsing the Authorization HTTP header to grant API keys * Using HTTPAuthorizationHeader and BasicHTTPAuthorizationHeaderCredentials * Adding tests for grantAPIKey * Adding http_authentication/ folder * Removing test route * Using new classes to create the headers we pass to ES * No longer .toLowerCase() when parsing the scheme from the request * Updating snapshots * Update x-pack/plugins/security/server/authentication/http_authentication/http_authorization_header.ts Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Updating another inline snapshot * Adding JSDoc * Renaming `grant` to `grantAsInternalUser` * Adding forgotten test. Fixing snapshot * Fixing mock * Apply suggestions from code review Co-Authored-By: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-Authored-By: Mike Côté <mikecote@users.noreply.github.com> * Using new classes for changing password * Removing unneeded asScoped call Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Mike Côté <mikecote@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Mike Côté <mikecote@users.noreply.github.com>
* master: (26 commits) [Alerting] Fixes flaky test in Alert Instances Details page (elastic#60893) cleanup visualizations api (elastic#59958) Inline timezoneProvider function, remove ui/vis/lib/timezone (elastic#60475) [SIEM] Adds 'Open one signal' Cypress test (elastic#60484) [UA] Upgrade assistant migration meta data can become stale (elastic#60789) [Metrics Alerts] Remove metric field from doc count on backend (elastic#60679) [Uptime] Skip failing location test temporarily (elastic#60938) [ML] Disabling datafeed editing when job is running (elastic#60751) Adding `authc.invalidateAPIKeyAsInternalUser` (elastic#60717) [SIEM] Add license check to ML Rule form (elastic#60691) Adding `authc.grantAPIKeyAsInternalUser` (elastic#60423) Support Histogram Data Type (elastic#59387) [Upgrade Assistant] Fix edge case where reindex op can falsely be seen as stale (elastic#60770) [SIEM] [Cases] Update case icons (elastic#60812) [TSVB] Fix percentiles band mode (elastic#60741) Fix formatter on range aggregation (elastic#58651) Goodbye, legacy data plugin 👋 (elastic#60449) [Metrics UI] Alerting for metrics explorer and inventory (elastic#58779) [Remote clustersadopt changes to remote info API (elastic#60795) Only run xpack siem cypress in PRs when there are siem changes (elastic#60661) ...
I tried to find a way to prevent having to parse
KibanaRequest::headersto use Elasticsearch's grant API Key API, but I was unable to find a way without breaking support for clients like cURL. So instead, I've attempted to parse theAuthorizationheader in the "nicest way possible".The
kibana_systemrole doesn't have the recently addedgrant_apicluster privilege , so for the time being I've been creating a new role and creating a new user with thekibana_systemand the new role:And then changing my
elasticsearch.usernametokibana_system.