-
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
Expose AnonymousAccess service through Security OSS plugin. #87091
Changes from 5 commits
ad82f4c
27ee6d1
4a77cfb
1d4b394
0fad3e6
f77a4f5
2277675
bc7f9b1
b0e2624
e877c5b
bf71e3c
ea31566
db91b20
e166855
35d89ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
insecureClusterAlert: { displayAlert: boolean }; | ||
anonymousAccess: { | ||
isEnabled: boolean; | ||
accessURLParameters: Record<string, string> | null; | ||
}; | ||
} |
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'; |
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🏅 thanks for falling back with |
||
|
||
return { getState: () => appStatePromise }; | ||
} | ||
} |
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 |
---|---|---|
|
@@ -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 { | ||
|
@@ -30,6 +31,7 @@ interface SetupDeps { | |
|
||
interface StartDeps { | ||
core: Pick<CoreStart, 'notifications' | 'http' | 'application'>; | ||
appState: AppStateServiceStart; | ||
} | ||
|
||
export interface InsecureClusterServiceSetup { | ||
|
@@ -84,37 +86,30 @@ export class InsecureClusterService { | |
}; | ||
} | ||
|
||
public start({ core }: StartDeps): InsecureClusterServiceStart { | ||
const shouldInitialize = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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>(); | ||
|
@@ -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>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional nit: unnecessary There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
'/internal/security_oss/anonymous_access/capabilities' | ||
); | ||
}, | ||
}, | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
@@ -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; | ||
}, | ||
}; | ||
} | ||
|
||
|
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), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like this route is always expecting the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it will, and user will get There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct, there shouldn't be any case when Kibana would call it with disabled anonymous access. |
||
}); | ||
} | ||
); | ||
} |
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.
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.
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 fine with
AppState
here. That makes sense to me