-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Migrate authentication subsystem to the new platform. #39446
Conversation
Pinging @elastic/kibana-security |
e901c8f
to
8c72cea
Compare
Hey @kobelb and @restrry, would be great if you can give any preliminary feedback on the approach I've taken here whenever you have time (PR is based on #39366, so please ignore the first commit). In #39446 (comment) I've outlined all the blockers we have right now, PR includes workarounds for all of them which I'll remove as soon as they aren't needed anymore. Right now I'm cleaning up/adding comments/fixing tests/removing leftovers. |
x-pack/plugins/security/server/authentication/providers/base.ts
Outdated
Show resolved
Hide resolved
import { schema, Type, TypeOf } from '@kbn/config-schema'; | ||
import { PluginInitializerContext } from '../../../../src/core/server'; | ||
|
||
export type ConfigType = ReturnType<typeof createConfig> extends Promise<infer P> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should core expose Unpromise
type? https://github.com/restrry/kibana/blob/8c72cead14333a583f55416c2fbf43a87f418378/src/legacy/server/kbn_server.d.ts#L89
even local declaration should simplify reading:
Unpromise<ReturnType<typeof createConfig>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JFYI: we are going to introduce type utilities library and move all type helpers there #39881
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JFYI: we are going to introduce type utilities library and move all type helpers there #39881
That'd be awesome!
} | ||
|
||
const challenges = ([] as string[]).concat( | ||
get<string | string[]>(authenticationError, 'output.headers[WWW-Authenticate]') || '' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should expose Boom errors from the core as a part of contract. even worse it is an implicit contract. @elastic/kibana-platform
https://github.com/restrry/kibana/blob/8c72cead14333a583f55416c2fbf43a87f418378/src/core/server/elasticsearch/cluster_client.ts#L100-L106
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love that we're exposing Boom but I can also see the argument that this will make migrating easier.
I think when we add the new elasticsearch-js client (#35508), we should not use Boom.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case I'd suggest to introduce ESClientError
type (which is a Boom
object atm) and document it in migration guide as a part of ES Client contract.
later we can switch all such places to a custom error and have more or less clear understanding what should be fixed.
@@ -8,9 +8,8 @@ import { get } from 'lodash'; | |||
|
|||
import { parseNext } from '../../lib/parse_next'; | |||
|
|||
export function initLoginView(server, xpackMainPlugin) { | |||
export function initLoginView({ config: { cookieName } }, server, xpackMainPlugin) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this method call isAuthenticated
instead of manually check a cookie presence? the same in logged out
view
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We definitely should somehow improve that (maybe with something like Hapi's try
auth mode? we should prevent redirects during authentication for such mode though). But the route itself opts out of authentication and hence isAuthenticated
will return false
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the route itself opts out of authentication and hence isAuthenticated will return false
yeah. that's kinda expected...because we don't run interceptor we cannot say for sure whether cookies aren't stale. The logic in legacy platform has the same problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because we don't run interceptor we cannot say for sure whether cookies aren't stale.
It has more problems than that :) Some providers (e.g. SAML) relies on intermediate cookies that used during SAML handshake, so if you start handshake you can no longer get back to login page. That's something I'm going to solve soon, but not in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
created #41959
It's already available in 'src/core/utils'. Where would you suggest to export it from?
I believe so. The only problem that most of those mocks don't exist yet. |
I believe we can change the API safely. In practice it already expects a partial config in Legacy platform where all I managed to find only monitoring plugin declaring a full elasticsearch config. Others use default elasticsearch config. |
We already inject it https://github.com/restrry/kibana/blob/8c72cead14333a583f55416c2fbf43a87f418378/src/core/server/config/config_service.ts#L145 Don't we? |
export function mockAuthenticationProviderOptions() { | ||
return { | ||
client: { callAsInternalUser: stub(), asScoped: stub(), close: stub() }, | ||
logger: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should cover cases for Logger
and BasePath
:
import { loggingServiceMock, httpServiceMock } from '../../../../../../src/core/server/mocks';
export function mockAuthenticationProviderOptions() {
const logger = loggingServiceMock.create();
const basePath = httpServiceMock.createSetupContract().basePath;
basePath.get.mockReturnValue('/base-path');
return {
client: { callAsInternalUser: stub(), asScoped: stub(), close: stub() },
logger,
basePath,
tokens: createStubInstance(Tokens),
};
}
I will add mocks for ClusterClient, ScopedClusterClient and KibanaRequest in a separate PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
Hmm, no we don't. I mean I inject it in this PR just to unblock myself, but not in master :) |
ok, I meant we inject anything added to |
That'd be great! I can do that, but ideally such changes should go through Platform review funnel :) |
Ack: reviewing now, it's taking me a bit |
created #40187 |
created #40188 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separating out "login" and "authenticate" makes it much easier to understand this providers, I really like this change.
); | ||
} | ||
} else if (!secureCookies) { | ||
secureCookies = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment: this is rather awkward, but it mirrors the behavior in the LP, so... 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please elaborate on this a bit? Do you have any suggestions on how we can improve that?
this.logger = this.initializerContext.logger.get(); | ||
} | ||
|
||
public async setup(core: CoreSetup): Promise<RecursiveReadonly<SecuritySetupContract>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Within the setup
method, we're grabbing the first instance of the config, and the clusterClient and using them within the authenticator for all subsequent http requests. Are we not concerned about these values changing or is this just temporary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as I understand an answer depends on a decision in #40188
at the current moment, it's a correct assumption.
|
||
export async function createConfig(context: PluginInitializerContext, isTLSEnabled: boolean) { | ||
const logger = context.logger.get('config'); | ||
const config = await context.config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: will this work with the various LP deprecations? For example, we've renamed xpack.security.authProviders
to authc.providers
in 7.x using the LP deprecations, will these be reflected here or do we need to duplicate these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question, I believe NP core doesn't provide any config deprecation support right now (cc @restrry). I haven't looked into this problem yet, but that will probably involve some duplicated code as a temporary workaround.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not provided yet. created #40255
this.logger = options.loggers.get('authenticator'); | ||
|
||
const providerCommonOptions = { | ||
client: this.options.clusterClient, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we perform the backport, we'll want to ensure that we're passing additional properties here:
kibana/x-pack/legacy/plugins/security/server/lib/authentication/authenticator.ts
Lines 67 to 73 in 2ceab54
protocol: server.info.protocol, | |
hostname: config.get<string>('server.host'), | |
port: config.get<number>('server.port'), | |
basePath: config.get<string>('server.basePath'), | |
tokens: new Tokens({ client, log }), | |
...config.get('xpack.security.public'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that won't be an easy backport :/
} from './providers'; | ||
import { AuthenticationResult } from './authentication_result'; | ||
import { DeauthenticationResult } from './deauthentication_result'; | ||
import { AuthenticationProviderSpecificOptions } from './providers/base'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should we move this to being exported by `./providers'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agree
|
||
if (!attempt || !attempt.username || !attempt.password) { | ||
this.logger.debug('Username and/or password not provided.'); | ||
return AuthenticationResult.notHandled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: it feels like we should be throwing an Error here as the consumer should be performing the validation on the end-user provided data before calling login.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you're right, it probably makes sense to just remove this check entirely. We shouldn't do such checks for every argument we expect, especially since they are defined as required arguments.
const user = await this.getUser(request); | ||
|
||
this.logger.debug('Request has been authenticated via header.'); | ||
return { authenticationResult: AuthenticationResult.succeeded(user) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: should we be passing the authorization headers as the second parameter? They're already on the request
, does that mean they'll automatically forward them to ES based on the elasticsearch.requestHeadersWhitelist
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're already on the request, does that mean they'll automatically forward them to ES based on the elasticsearch.requestHeadersWhitelist?
Yeah, I believe so, existing headers + authHeaders
returned from auth handler should be provided to ClusterClient that will filter them based on elasticsearch.requestHeadersWhitelist
. @restrry can you please confirm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, we merge request and authHeaders
headers, filter them and pass to ES Client only ones with names declared in elasticsearch.requestHeadersWhitelist
https://github.com/restrry/kibana/blob/0f25401fe4caaf97ff1b701d245455a0ef71ec3a/src/core/server/elasticsearch/cluster_client.ts#L206
* @param [state] Optional state to be stored and reused for the next request. | ||
*/ | ||
public static succeeded(user: AuthenticatedUser, state?: unknown) { | ||
public static succeeded( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: we almost always should be passing the authHeaders
to the succeeded method, do you think changing this method signature to the following would make this more apparent to consumers and potentially prevent us from forgetting to do so?
public static succeeded(
user: AuthenticatedUser,
authHeaders: Record<string, string> | null,
state?: unknown
) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, that's how I did it in the beginning, but eventually abandoned this idea as it became hard to read and interpret, e.g.:
return AuthenticationResult.succeeded(user, { authorization }, { authorization });
In the snippet above we pass both authHeaders
and state
and it's hard to tell what we're doing here exactly. Also TS won't complain if we decide to switch the order we pass state
and authHeaders
in since types of the args are allowing that (and I managed to spend some unreasonable amount of time looking at the code and trying to understand what was wrong when I did that 🙈 ).
So I'm leaning towards keeping it as is, but would like to hear what you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense... let's leave it how it is then.
const user = await this.getUser(request, authHeaders); | ||
|
||
this.logger.debug('Request has been authenticated via state.'); | ||
return AuthenticationResult.succeeded(user, { authHeaders }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: we're no longer mutating request.headers
for this and all other token based auth providers which is great. have we ensured that we aren't breaking reporting by doing so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll double check before I ask for review, but I believe we should be fine since we maintain BWC behavior with this https://github.com/elastic/kibana/blob/master/src/core/server/http/http_server.ts#L238
) { | ||
this.logger.debug('Trying to perform a login.'); | ||
|
||
if (!credentials || !credentials.username || !credentials.password) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: same as above, should we be throwing an error here also?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, let's just remove this check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outside of the changes that you've proposed for login's use of updateSessionStorage
, this is looking great! Just one question, but everything seems to be functioning perfectly.
headers: { authorization: 'negotiate spnego' }, | ||
}); | ||
|
||
mockScopedClusterClient(mockOptions.client) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Did we intend to add this for this test? The test passes without it, and it's not immediately obvious what it's adding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, good catch! I believe it's just a stupid copy-paste mistake 🙈 This stub should not even be called as we have one with more specific withArgs
below.
…nt hapi-auth-cookie deps from x-pack package.json, migrate to new core sessionStorage API, integrate latest Kerberos provider changes from upstream
💔 Build Failed |
💚 Build Succeeded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
💚 Build Succeeded |
Thanks everyone for review, let's see if it sticks :) |
7.x/7.4.0: e19a03b |
In this PR we migrate authentication subsystem to the new platform. Authentication source code will now live within a new platform plugin that will coexist with the legacy Security plugin until we fully migrate to the new platform.
Here is the list of blockers we need to resolve before we can merge this (PR includes all these changes as a reference of what we need, but we need a help of the @elastic/kibana-platform to merge them or get alternative proposals):
Changes in @kbn/config-schema:
schema.literal
type should supportnull
values (needed forsessionTimeout
config value) Extend @kbn/config-schema #40118schema.conditional
should support schemas within a right operand in addition to references and plain values (needed for our not-very-simple validation ofauthc.*
Extend @kbn/config-schema #40118schema.forbidden
(currently can be emulated withschema.any({ validate: () => 'error' })
, used mostly withschema.conditional
when we'd like to forbid some configuration values in case some condition is met)? Extend @kbn/config-schema #40118Changes in the Core:
dist
context variable during config validation, so that we can use it withcontextRef('dist')
(needed for automatic generation ofencryptionKey
in dev environment) New platform should inject "is distributable" flag in package info #40187http
setup contract should exposeisTLSEnabled
or something like this (needed to automatically setsecureCookies
if TLS is enabled and/or warn about insecure cookie transmission) Expose "is TLS enabled" flag for Kibana HTTP Server #40336elasticsearch
setup contract should exposecreateClient
(we need it to create our ownsecurity
client with "esShield" plugin). Another problem here is thatcreateClient
expects full Elasticsearch config, so we have to rely onsetup.elasticsearch.legacy.config$
. To be honest, I don't remember already why I didn't use default Elasticsearch config increateClient
internally when I implemented this method (it must be something related to the way legacy elasticsearch plugin works currently). This would allow consumers to just override or add some config values if needed. createCluster requires a partial elasticsearch config #40405 and expose ES createClient to plugins #40717Other changes:
xpack/plugins
into Jest config Include new platform x-pack plugin paths to Jest roots. #40422RecursivelyReadonly
-like constructions in many places, especially for the contracts we return, would it make sense to officially expose Core'sdeepFreeze
「DISCUSS」TypeScript utility types #39881?Logger
,ClusterClient
,ScopedClusterClient
andBasePath
, Mocks for CoreStart, CoreSetup and PluginInitializerContext #39351 and Provide Mocks for New Platform concepts #40186) ?Related to: #33775
Fixes: #40756
Blocked by:
#39366,#40118,#40187,#40405,#40717/cc @restrry