Skip to content

Commit

Permalink
More changes
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Jun 10, 2020
1 parent f75a899 commit d6c4454
Show file tree
Hide file tree
Showing 18 changed files with 1,000 additions and 254 deletions.
41 changes: 26 additions & 15 deletions x-pack/plugins/security/server/authentication/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,10 @@ export class Authenticator {
throw new Error('Current license does not allow access agreement acknowledgement.');
}

await this.session.set(request, { ...existingSessionValue, accessAgreementAcknowledged: true });
await this.session.update(request, {
...existingSessionValue,
accessAgreementAcknowledged: true,
});

this.options.auditLogger.accessAgreementAcknowledged(
currentUser.username,
Expand Down Expand Up @@ -478,13 +481,6 @@ export class Authenticator {
return null;
}

// If authentication succeeds or requires redirect we should automatically extend existing user session,
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
// state we should store it in the session regardless of whether it's a system API request or not.
const sessionCanBeUpdated =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || !request.isSystemRequest);

// If provider owned the session, but failed to authenticate anyway, that likely means that
// session is not valid and we should clear it. Also provider can specifically ask to clear
// session by setting it to `null` even if authentication attempt didn't fail.
Expand All @@ -496,16 +492,31 @@ export class Authenticator {
return null;
}

if (sessionCanBeUpdated) {
return await this.session.set(request, {
...(existingSessionValue || { provider, lifespanExpiration: null }),
state: authenticationResult.shouldUpdateState()
? authenticationResult.state
: existingSessionValue?.state,
// If authentication succeeds or requires redirect we should automatically extend existing user session,
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
// state we should store it in the session regardless of whether it's a system API request or not.
const sessionCanBeUpdated =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || !request.isSystemRequest);
if (!sessionCanBeUpdated) {
return existingSessionValue;
}

if (!existingSessionValue) {
return await this.session.create(request, {
provider,
state: authenticationResult.shouldUpdateState() ? authenticationResult.state : null,
});
}

if (authenticationResult.shouldUpdateState()) {
return await this.session.update(request, {
...existingSessionValue,
state: authenticationResult.state,
});
}

return existingSessionValue;
return await this.session.extend(request, existingSessionValue);
}

/**
Expand Down
32 changes: 2 additions & 30 deletions x-pack/plugins/security/server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { SessionInfo } from '../../common/types';
import { SecurityAuditLogger } from '../audit';
import { ConfigType } from '../config';
import { getErrorStatusCode } from '../errors';
Expand Down Expand Up @@ -46,6 +45,7 @@ interface SetupAuthenticationParams {
config: ConfigType;
license: SecurityLicense;
loggers: LoggerFactory;
session: Session;
}

export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>;
Expand All @@ -58,6 +58,7 @@ export async function setupAuthentication({
config,
license,
loggers,
session,
}: SetupAuthenticationParams) {
const authLogger = loggers.get('authentication');

Expand All @@ -73,34 +74,6 @@ export async function setupAuthentication({
return (http.auth.get(request).state ?? null) as AuthenticatedUser | null;
};

/**
* Returns session information for the current request.
* @param request Request instance.
*/
const getSessionInfo = async (request: KibanaRequest): Promise<SessionInfo | null> => {
const sessionValue = await session.get(request);
if (!sessionValue) {
return null;
}

// We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return
// the current server time -- that way the client can calculate the relative time to expiration.
return {
now: Date.now(),
idleTimeoutExpiration: sessionValue.idleTimeoutExpiration,
lifespanExpiration: sessionValue.lifespanExpiration,
provider: sessionValue.provider,
};
};

const session = new Session({
auditLogger,
logger: loggers.get('session'),
clusterClient,
config,
http,
});

authLogger.debug('Successfully initialized session.');

const authenticator = new Authenticator({
Expand Down Expand Up @@ -182,7 +155,6 @@ export async function setupAuthentication({
return {
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
getSessionInfo,
isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator),
acknowledgeAccessAgreement: authenticator.acknowledgeAccessAgreement.bind(authenticator),
getCurrentUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { combineLatest, BehaviorSubject, Subscription } from 'rxjs';
import { distinctUntilChanged, filter } from 'rxjs/operators';
import { Subscription, Observable } from 'rxjs';
import { UICapabilities } from 'ui/capabilities';
import {
LoggerFactory,
KibanaRequest,
IClusterClient,
ServiceStatusLevels,
Logger,
StatusServiceSetup,
HttpServiceSetup,
CapabilitiesSetup,
} from '../../../../../src/core/server';
Expand Down Expand Up @@ -44,6 +41,7 @@ import { validateReservedPrivileges } from './validate_reserved_privileges';
import { registerPrivilegesWithCluster } from './register_privileges_with_cluster';
import { APPLICATION_PREFIX } from '../../common/constants';
import { SecurityLicense } from '../../common/licensing';
import { OnlineStatusRetryScheduler } from '../elasticsearch';

export { Actions } from './actions';
export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges';
Expand All @@ -52,7 +50,6 @@ export { featurePrivilegeIterator } from './privileges';
interface AuthorizationServiceSetupParams {
packageVersion: string;
http: HttpServiceSetup;
status: StatusServiceSetup;
capabilities: CapabilitiesSetup;
clusterClient: IClusterClient;
license: SecurityLicense;
Expand All @@ -65,6 +62,7 @@ interface AuthorizationServiceSetupParams {
interface AuthorizationServiceStartParams {
features: FeaturesPluginStart;
clusterClient: IClusterClient;
online$: Observable<OnlineStatusRetryScheduler>;
}

export interface AuthorizationServiceSetup {
Expand All @@ -79,8 +77,6 @@ export interface AuthorizationServiceSetup {

export class AuthorizationService {
private logger!: Logger;
private license!: SecurityLicense;
private status!: StatusServiceSetup;
private applicationName!: string;
private privileges!: PrivilegesService;

Expand All @@ -89,7 +85,6 @@ export class AuthorizationService {
setup({
http,
capabilities,
status,
packageVersion,
clusterClient,
license,
Expand All @@ -99,8 +94,6 @@ export class AuthorizationService {
getSpacesService,
}: AuthorizationServiceSetupParams): AuthorizationServiceSetup {
this.logger = loggers.get('authorization');
this.license = license;
this.status = status;
this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`;

const mode = authorizationModeFactory(license);
Expand Down Expand Up @@ -158,12 +151,23 @@ export class AuthorizationService {
return authz;
}

start({ clusterClient, features }: AuthorizationServiceStartParams) {
start({ clusterClient, features, online$ }: AuthorizationServiceStartParams) {
const allFeatures = features.getFeatures();
validateFeaturePrivileges(allFeatures);
validateReservedPrivileges(allFeatures);

this.registerPrivileges(clusterClient);
this.statusSubscription = online$.subscribe(async ({ scheduleRetry }) => {
try {
await registerPrivilegesWithCluster(
this.logger,
this.privileges,
this.applicationName,
clusterClient
);
} catch (err) {
scheduleRetry();
}
});
}

stop() {
Expand All @@ -172,50 +176,4 @@ export class AuthorizationService {
this.statusSubscription = undefined;
}
}

private registerPrivileges(clusterClient: IClusterClient) {
const RETRY_SCALE_DURATION = 100;
const RETRY_TIMEOUT_MAX = 10000;
const retries$ = new BehaviorSubject(0);
let retryTimeout: NodeJS.Timeout;

// Register cluster privileges once Elasticsearch is available and Security plugin is enabled.
this.statusSubscription = combineLatest([
this.status.core$,
this.license.features$,
retries$.asObservable().pipe(
// We shouldn't emit new value if retry counter is reset. This comparator isn't called for
// the initial value.
distinctUntilChanged((prev, curr) => prev === curr || curr === 0)
),
])
.pipe(
filter(
([status]) =>
this.license.isEnabled() && status.elasticsearch.level === ServiceStatusLevels.available
)
)
.subscribe(async () => {
// If status or license change occurred before retry timeout we should cancel it.
if (retryTimeout) {
clearTimeout(retryTimeout);
}

try {
await registerPrivilegesWithCluster(
this.logger,
this.privileges,
this.applicationName,
clusterClient
);
retries$.next(0);
} catch (err) {
const retriesElapsed = retries$.getValue() + 1;
retryTimeout = setTimeout(
() => retries$.next(retriesElapsed),
Math.min(retriesElapsed * RETRY_SCALE_DURATION, RETRY_TIMEOUT_MAX)
);
}
});
}
}
121 changes: 121 additions & 0 deletions x-pack/plugins/security/server/elasticsearch/elasticsearch_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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.
*/

import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, shareReplay, tap } from 'rxjs/operators';
import {
IClusterClient,
ICustomClusterClient,
Logger,
ServiceStatusLevels,
StatusServiceSetup,
ElasticsearchServiceSetup as CoreElasticsearchServiceSetup,
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { elasticsearchClientPlugin } from './elasticsearch_client_plugin';

export interface ElasticsearchServiceSetupParams {
readonly elasticsearch: CoreElasticsearchServiceSetup;
readonly status: StatusServiceSetup;
readonly license: SecurityLicense;
}

export interface ElasticsearchServiceSetup {
readonly clusterClient: IClusterClient;
}

export interface ElasticsearchServiceStart {
readonly clusterClient: IClusterClient;
readonly watchOnlineStatus$: () => Observable<OnlineStatusRetryScheduler>;
}

export interface OnlineStatusRetryScheduler {
scheduleRetry: () => void;
}

export class ElasticsearchService {
private clusterClient?: ICustomClusterClient;
private coreStatus$!: Observable<boolean>;

constructor(private readonly logger: Logger) {}

setup({
elasticsearch,
status,
license,
}: ElasticsearchServiceSetupParams): ElasticsearchServiceSetup {
this.clusterClient = elasticsearch.legacy.createClient('security', {
plugins: [elasticsearchClientPlugin],
});

this.coreStatus$ = combineLatest([status.core$, license.features$]).pipe(
map(
([coreStatus]) =>
license.isEnabled() && coreStatus.elasticsearch.level === ServiceStatusLevels.available
),
shareReplay(1)
);

return { clusterClient: this.clusterClient };
}

start(): ElasticsearchServiceStart {
return {
clusterClient: this.clusterClient!,
watchOnlineStatus$: () => {
const RETRY_SCALE_DURATION = 100;
const RETRY_TIMEOUT_MAX = 10000;
const retries$ = new BehaviorSubject(0);

const retryScheduler = {
scheduleRetry: () => {
const retriesElapsed = retries$.getValue() + 1;
const nextRetryTimeout = Math.min(
retriesElapsed * RETRY_SCALE_DURATION,
RETRY_TIMEOUT_MAX
);

this.logger.debug(`Scheduling re-try in ${nextRetryTimeout} ms.`);

retryTimeout = setTimeout(() => retries$.next(retriesElapsed), nextRetryTimeout);
},
};

let retryTimeout: NodeJS.Timeout;
return combineLatest([
this.coreStatus$.pipe(
tap(() => {
// If status or license change occurred before retry timeout we should cancel
// it and reset retry counter.
if (retryTimeout) {
clearTimeout(retryTimeout);
}

if (retries$.value > 0) {
retries$.next(0);
}
})
),
retries$.asObservable().pipe(
// We shouldn't emit new value if retry counter is reset. This comparator isn't called for
// the initial value.
distinctUntilChanged((prev, curr) => prev === curr || curr === 0)
),
]).pipe(
filter(([isAvailable]) => isAvailable),
map(() => retryScheduler)
);
},
};
}

stop() {
if (this.clusterClient) {
this.clusterClient.close();
this.clusterClient = undefined;
}
}
}
Loading

0 comments on commit d6c4454

Please sign in to comment.