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 AnonymousAccess service through Security OSS plugin. #87091

Merged
merged 15 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/plugins/security_oss/common/app_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Defines Security OSS application state.
*/
export interface AppState {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: couldn't find a better name for that service, data and endpoint, kind of analogy with ASP.NET view state, but happy to take any other name if you have suggestions.

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with AppState here. That makes sense to me

insecureClusterAlert: { displayAlert: boolean };
anonymousAccess: {
isEnabled: boolean;
accessURLParameters: Record<string, string> | null;
};
}
20 changes: 20 additions & 0 deletions src/plugins/security_oss/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export type { AppState } from './app_state';
47 changes: 47 additions & 0 deletions src/plugins/security_oss/public/app_state/app_state_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { CoreStart } from 'kibana/public';
import { AppState } from '../../common';

const DEFAULT_APP_STATE = Object.freeze({
insecureClusterAlert: { displayAlert: false },
anonymousAccess: { isEnabled: false, accessURLParameters: null },
});

interface StartDeps {
core: Pick<CoreStart, 'http'>;
}

export interface AppStateServiceStart {
getState: () => Promise<AppState>;
}

/**
* Service that allows to retrieve application state.
*/
export class AppStateService {
start({ core }: StartDeps): AppStateServiceStart {
const appStatePromise = core.http.anonymousPaths.isAnonymous(window.location.pathname)
? Promise.resolve(DEFAULT_APP_STATE)
: core.http.get<AppState>('/internal/security_oss/app_state').catch(() => DEFAULT_APP_STATE);
Copy link
Member

Choose a reason for hiding this comment

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

🏅 thanks for falling back with .catch()


return { getState: () => appStatePromise };
}
}
20 changes: 20 additions & 0 deletions src/plugins/security_oss/public/app_state/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export { AppStateService, AppStateServiceStart } from './app_state_service';
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CoreSetup, CoreStart, MountPoint, Toast } from 'kibana/public';
import { BehaviorSubject, combineLatest, from } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { ConfigType } from '../config';
import { AppStateServiceStart } from '../app_state';
import { defaultAlertText, defaultAlertTitle } from './components';

interface SetupDeps {
Expand All @@ -30,6 +31,7 @@ interface SetupDeps {

interface StartDeps {
core: Pick<CoreStart, 'notifications' | 'http' | 'application'>;
appState: AppStateServiceStart;
}

export interface InsecureClusterServiceSetup {
Expand Down Expand Up @@ -84,37 +86,30 @@ export class InsecureClusterService {
};
}

public start({ core }: StartDeps): InsecureClusterServiceStart {
const shouldInitialize =
Copy link
Member Author

Choose a reason for hiding this comment

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

note: We can keep this check here if you wish, but we have it in the AppState service now and the overhead we'll get here should be negligible in theory (unnecessary Observable subscription in initializeAlert)

this.enabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname);

if (shouldInitialize) {
this.initializeAlert(core);
public start({ core, appState }: StartDeps): InsecureClusterServiceStart {
if (this.enabled) {
this.initializeAlert(core, appState);
}

return {
hideAlert: (persist: boolean) => this.setAlertVisibility(false, persist),
};
}

private initializeAlert(core: StartDeps['core']) {
const displayAlert$ = from(
core.http
.get<{ displayAlert: boolean }>('/internal/security_oss/display_insecure_cluster_alert')
.catch((e) => {
// in the event we can't make this call, assume we shouldn't display this alert.
return { displayAlert: false };
})
);
private initializeAlert(core: StartDeps['core'], appState: AppStateServiceStart) {
const appState$ = from(appState.getState());

// 10 days is reasonably long enough to call "forever" for a page load.
// Can't go too much longer than this. See https://github.com/elastic/kibana/issues/64264#issuecomment-618400354
const oneMinute = 60000;
const tenDays = oneMinute * 60 * 24 * 10;

combineLatest([displayAlert$, this.alertVisibility$])
combineLatest([appState$, this.alertVisibility$])
.pipe(
map(([{ displayAlert }, isAlertVisible]) => displayAlert && isAlertVisible),
map(
([{ insecureClusterAlert }, isAlertVisible]) =>
insecureClusterAlert.displayAlert && isAlertVisible
),
distinctUntilChanged()
)
.subscribe((showAlert) => {
Expand Down
42 changes: 39 additions & 3 deletions src/plugins/security_oss/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,51 @@
* under the License.
*/

import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import {
Capabilities,
CoreSetup,
CoreStart,
Plugin,
PluginInitializerContext,
} from 'src/core/public';
import { ConfigType } from './config';
import {
InsecureClusterService,
InsecureClusterServiceSetup,
InsecureClusterServiceStart,
} from './insecure_cluster_service';
import { AppStateService } from './app_state';

export interface SavedObjectTypeAnonymousAccess {
/**
* Indicates whether anonymous user can access particular Saved Object type (e.g. dashboard, map etc.).
*/
canAccess: boolean;
/**
* A map of query string parameters that should be specified in URL so that anonymous user can use
* to automatically log in to Kibana and access particular Saved Object type.
*/
accessURLParameters: Record<string, string> | null;
}

export interface SecurityOssPluginSetup {
insecureCluster: InsecureClusterServiceSetup;
}

export interface SecurityOssPluginStart {
insecureCluster: InsecureClusterServiceStart;
anonymousAccess: {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: I'll discuss the final API shape with AppArch, but I don't expect it to change too much. It feels the best option would be to give Share plugin consumers entire capabilities object so that they can the check on their own.

Copy link
Member

Choose a reason for hiding this comment

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

I agree, I don't see a need to filter out the capabilities object on our side.

getAccessURLParameters: () => Promise<Record<string, string> | null>;
getCapabilities: () => Promise<Capabilities>;
};
}

export class SecurityOssPlugin
implements Plugin<SecurityOssPluginSetup, SecurityOssPluginStart, {}, {}> {
private readonly config: ConfigType;

private insecureClusterService: InsecureClusterService;
private readonly insecureClusterService: InsecureClusterService;
private readonly appStateService = new AppStateService();

constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<ConfigType>();
Expand All @@ -51,8 +75,20 @@ export class SecurityOssPlugin
}

public start(core: CoreStart) {
const appState = this.appStateService.start({ core });
return {
insecureCluster: this.insecureClusterService.start({ core }),
insecureCluster: this.insecureClusterService.start({ core, appState }),
anonymousAccess: {
async getAccessURLParameters() {
const { anonymousAccess } = await appState.getState();
return anonymousAccess.accessURLParameters;
},
async getCapabilities() {
return await core.http.get<Capabilities>(
Copy link
Member

Choose a reason for hiding this comment

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

optional nit: unnecessary await

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 think there was a verbal agreement in a Platform team long ago to do this since we don't define return type explicitly here and it's not always obvious that function returns Promise. I couldn't find it recorded anywhere, so will remove it.

'/internal/security_oss/anonymous_access/capabilities'
);
},
},
};
}
}
60 changes: 57 additions & 3 deletions src/plugins/security_oss/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,64 @@
* under the License.
*/

import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import {
Capabilities,
CoreSetup,
KibanaRequest,
Logger,
Plugin,
PluginInitializerContext,
} from 'kibana/server';
import { BehaviorSubject, Observable } from 'rxjs';
import { createClusterDataCheck } from './check_cluster_data';
import { ConfigType } from './config';
import { setupDisplayInsecureClusterAlertRoute } from './routes';
import { setupAppStateRoute, setupAnonymousAccessCapabilitiesRoute } from './routes';

export interface SecurityOssPluginSetup {
/**
* Allows consumers to show/hide the insecure cluster warning.
*/
showInsecureClusterWarning$: BehaviorSubject<boolean>;

/**
* Set the provider function that returns a service that can deal with various aspects of the
* anonymous access.
* @param provider
*/
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => void;
}

export interface AnonymousAccessService {
/**
* Indicates whether anonymous access is enabled.
*/
readonly isAnonymousAccessEnabled: boolean;

/**
* A map of query string parameters that should be specified in URL so that anonymous user can use
* to automatically log in to Kibana.
*/
readonly accessURLParameters: Readonly<Map<string, string>> | null;

/**
* Gets capabilities of the anonymous service account.
* @param request Kibana request instance.
*/
getCapabilities: (request: KibanaRequest) => Promise<Capabilities>;
}

export class SecurityOssPlugin implements Plugin<SecurityOssPluginSetup, void, {}, {}> {
private readonly config$: Observable<ConfigType>;
private readonly logger: Logger;

private anonymousAccessServiceProvider?: () => AnonymousAccessService;
Copy link
Member Author

Choose a reason for hiding this comment

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

note: kind of clumsy, but I cannot think of a better approach to pass service that is only available at start.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed. It's at least consistent with the way we've done this elsewhere 🤷‍♂️

private readonly getAnonymousAccessService = () => {
if (!this.anonymousAccessServiceProvider) {
throw new Error('Anonymous Access service provider is not set.');
}
return this.anonymousAccessServiceProvider();
};

constructor(initializerContext: PluginInitializerContext<ConfigType>) {
this.config$ = initializerContext.config.create();
this.logger = initializerContext.logger.get();
Expand All @@ -43,16 +84,29 @@ export class SecurityOssPlugin implements Plugin<SecurityOssPluginSetup, void, {
const router = core.http.createRouter();
const showInsecureClusterWarning$ = new BehaviorSubject<boolean>(true);

setupDisplayInsecureClusterAlertRoute({
setupAppStateRoute({
router,
log: this.logger,
config$: this.config$,
displayModifier$: showInsecureClusterWarning$,
doesClusterHaveUserData: createClusterDataCheck(),
getAnonymousAccessService: this.getAnonymousAccessService,
});

setupAnonymousAccessCapabilitiesRoute({
router,
getAnonymousAccessService: this.getAnonymousAccessService,
});

return {
showInsecureClusterWarning$,
setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => {
if (this.anonymousAccessServiceProvider) {
throw new Error('Anonymous Access service provider is already set.');
}

this.anonymousAccessServiceProvider = provider;
},
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { IRouter } from 'kibana/server';
import { AnonymousAccessService } from '../plugin';

interface Deps {
router: IRouter;
getAnonymousAccessService: () => AnonymousAccessService;
}

/**
* Defines route that returns capabilities of the anonymous service account.
*/
export function setupAnonymousAccessCapabilitiesRoute({ router, getAnonymousAccessService }: Deps) {
router.get(
{ path: '/internal/security_oss/anonymous_access/capabilities', validate: false },
async (_context, request, response) => {
return response.ok({
body: await getAnonymousAccessService().getCapabilities(request),
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this route is always expecting the anonymous access service to be defined, but since it's only registered by x-pack security (as far as I can see), I think this will throw an exception for the OSS distribution whenever its called.

Copy link
Member Author

@azasypkin azasypkin Jan 7, 2021

Choose a reason for hiding this comment

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

Yeah, it will, and user will get 500 error. Would you like it to have some default 200 behavior (then we'll have to have default capabilities in two places or return null/empty object) or just more appropriate error code (e.g. 501 Not Implemented or 403 Forbidden)?

Copy link
Member

Choose a reason for hiding this comment

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

I like 501 Not Implemented for this case. I assume that this endpoint won't normally be called if anonymous access is disabled (and by extension, when called in the OSS distribution)? In other words, we'll have enough information client-side so that we know not to even attempt the call

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 assume that this endpoint won't normally be called if anonymous access is disabled (and by extension, when called in the OSS distribution)?

Correct, there shouldn't be any case when Kibana would call it with disabled anonymous access.

});
}
);
}
Loading