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

Migrate authentication subsystem to the new platform. #39446

Merged
merged 22 commits into from
Jul 19, 2019

Conversation

azasypkin
Copy link
Member

@azasypkin azasypkin commented Jun 21, 2019

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:

  • The schema.literal type should support null values (needed for sessionTimeout config value) Extend @kbn/config-schema #40118
  • The schema.conditional should support schemas within a right operand in addition to references and plain values (needed for our not-very-simple validation of authc.* Extend @kbn/config-schema #40118
  • [Optional] Maybe introduce schema.forbidden (currently can be emulated with schema.any({ validate: () => 'error' }), used mostly with schema.conditional when we'd like to forbid some configuration values in case some condition is met)? Extend @kbn/config-schema #40118

Changes in the Core:

  • Config validation code should inject dist context variable during config validation, so that we can use it with contextRef('dist') (needed for automatic generation of encryptionKey in dev environment) New platform should inject "is distributable" flag in package info #40187
  • The http setup contract should expose isTLSEnabled or something like this (needed to automatically set secureCookies if TLS is enabled and/or warn about insecure cookie transmission) Expose "is TLS enabled" flag for Kibana HTTP Server #40336
  • The elasticsearch setup contract should expose createClient (we need it to create our own security client with "esShield" plugin). Another problem here is that createClient expects full Elasticsearch config, so we have to rely on setup.elasticsearch.legacy.config$. To be honest, I don't remember already why I didn't use default Elasticsearch config in createClient 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 #40717

Other changes:

Related to: #33775
Fixes: #40756
Blocked by: #39366, #40118, #40187, #40405, #40717

/cc @restrry

@azasypkin azasypkin added Feature:New Platform Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! Feature:Security/Authentication Platform Security - Authentication release_note:skip Skip the PR/issue when compiling release notes v7.3.0 labels Jun 21, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-security

@azasypkin
Copy link
Member Author

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.

src/core/server/config/env.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>
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Member Author

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!

x-pack/plugins/security/server/config.ts Outdated Show resolved Hide resolved
}

const challenges = ([] as string[]).concat(
get<string | string[]>(authenticationError, 'output.headers[WWW-Authenticate]') || ''
Copy link
Contributor

@mshustov mshustov Jun 27, 2019

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

Copy link
Contributor

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.

Copy link
Contributor

@mshustov mshustov Jun 28, 2019

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) {
Copy link
Contributor

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

Copy link
Member Author

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

Copy link
Contributor

@mshustov mshustov Jul 2, 2019

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.

Copy link
Member Author

@azasypkin azasypkin Jul 2, 2019

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

created #41959

x-pack/plugins/security/server/__fixtures__/request.ts Outdated Show resolved Hide resolved
@mshustov
Copy link
Contributor

We use RecursivelyReadonly-like constructions in many places, especially for the contracts we return, would it make sense to officially expose Core's deepFreeze?

It's already available in 'src/core/utils'. Where would you suggest to export it from?

Would it make sense for Core to expose mocks for commonly used stuff (e.g. mock for Logger, ClusterClient, ScopedClusterClient and BasePath

I believe so. The only problem that most of those mocks don't exist yet.

@mshustov
Copy link
Contributor

mshustov commented Jun 28, 2019

The elasticsearch setup contract should expose createClient (we need it to create our own security client with "esShield" plugin). Another problem here is that createClient expects full Elasticsearch config, so we have to rely on setup.elasticsearch.legacy.config$. To be honest, I don't remember already why I didn't use default Elasticsearch config in createClient 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.

I believe we can change the API safely. In practice it already expects a partial config in Legacy platform where all createClient external consumers are.
https://github.com/restrry/kibana/blob/06dbcfbe96ef768ac73700ec888e4b1165682ee3/src/legacy/core_plugins/elasticsearch/index.js#L84-L85

I managed to find only monitoring plugin declaring a full elasticsearch config. Others use default elasticsearch config.

@mshustov
Copy link
Contributor

mshustov commented Jul 1, 2019

Config validation code should inject dist context variable during config validation, so that we can use it with contextRef('dist') (needed for automatic generation of encryptionKey in dev environment)

We already inject it https://github.com/restrry/kibana/blob/8c72cead14333a583f55416c2fbf43a87f418378/src/core/server/config/config_service.ts#L145 Don't we?

@kobelb kobelb self-requested a review July 1, 2019 21:32
@mshustov mshustov mentioned this pull request Jul 2, 2019
3 tasks
export function mockAuthenticationProviderOptions() {
return {
client: { callAsInternalUser: stub(), asScoped: stub(), close: stub() },
logger: {
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Awesome!

@azasypkin
Copy link
Member Author

We already inject it https://github.com/restrry/kibana/blob/8c72cead14333a583f55416c2fbf43a87f418378/src/core/server/config/config_service.ts#L145 Don't we?

Hmm, no we don't. I mean I inject it in this PR just to unblock myself, but not in master :)

@mshustov
Copy link
Contributor

mshustov commented Jul 2, 2019

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 packageInfo. do you want me to add dist in a separate PR?

@azasypkin
Copy link
Member Author

ok, I meant we inject anything added to packageInfo. do you want me to add dist in a separate PR?

That'd be great! I can do that, but ideally such changes should go through Platform review funnel :)

@kobelb
Copy link
Contributor

kobelb commented Jul 2, 2019

Ack: reviewing now, it's taking me a bit

@mshustov
Copy link
Contributor

mshustov commented Jul 2, 2019

That'd be great! I can do that, but ideally such changes should go through Platform review funnel :)

created #40187

@mshustov
Copy link
Contributor

mshustov commented Jul 2, 2019

I believe we can change the API safely. In practice it already expects a partial config in Legacy platform where all createClient external consumers are.

created #40188

Copy link
Contributor

@kobelb kobelb left a 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;
Copy link
Contributor

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

Copy link
Member Author

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>> {
Copy link
Contributor

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?

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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,
Copy link
Contributor

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:

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'),

Copy link
Member Author

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';
Copy link
Contributor

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'?

Copy link
Member Author

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();
Copy link
Contributor

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.

Copy link
Member Author

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) };
Copy link
Contributor

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?

Copy link
Member Author

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?

Copy link
Contributor

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(
Copy link
Contributor

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
  ) {

Copy link
Member Author

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?

Copy link
Contributor

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 });
Copy link
Contributor

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?

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'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) {
Copy link
Contributor

@kobelb kobelb Jul 2, 2019

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?

Copy link
Member Author

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.

@spalger spalger added the v7.4.0 label Jul 3, 2019
Copy link
Contributor

@kobelb kobelb left a 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)
Copy link
Contributor

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.

Copy link
Member Author

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
@elasticmachine
Copy link
Contributor

💔 Build Failed

@azasypkin
Copy link
Member Author

azasypkin commented Jul 16, 2019

CI will be failing until #41263 is fixed. But apart from that PR should be ready for review @kobelb .

@azasypkin azasypkin requested a review from kobelb July 16, 2019 17:08
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@azasypkin azasypkin requested a review from mshustov July 17, 2019 06:37
Copy link
Contributor

@kobelb kobelb left a comment

Choose a reason for hiding this comment

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

Nice work!

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@azasypkin
Copy link
Member Author

Thanks everyone for review, let's see if it sticks :)

@azasypkin
Copy link
Member Author

7.x/7.4.0: e19a03b

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backported Feature:NP Migration Feature:Security/Authentication Platform Security - Authentication release_note:skip Skip the PR/issue when compiling release notes Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v7.4.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

replaceInjectedVars uiExport mocha test logging UnhandledPromiseRejectionWarning
6 participants