Expose AnonymousAccess service through Security OSS plugin.#87091
Expose AnonymousAccess service through Security OSS plugin.#87091azasypkin merged 15 commits intoelastic:masterfrom
Conversation
| } | ||
|
|
||
| public start(core: CoreStart) { | ||
| const isAnonymousAccessEnabled = !!core.application.capabilities.security?.anonymousAccess; |
There was a problem hiding this comment.
note: My understanding and main assumption is that using endpoints and capabilities exposed by the X-Pack counterpart in the OSS version of the plugin is acceptable as long as it works fine with OSS distribution. What do you think?
There was a problem hiding this comment.
I think capabilities are more OK than endpoints, but I don't know if we've codified this anywhere.
I don't love the idea of using x-pack endpoints in OSS, because that could lead to 404 errors, which is sloppy. I see you've guarded against that here, but I worry about the precedent this sets.
For the insecure cluster warning, we defined the endpoint in OSS and allowed x-pack security to augment the logic. This allows the endpoint to work in all distributions, while still allowing for distribution-specific behavior.
There was a problem hiding this comment.
I don't love the idea of using x-pack endpoints in OSS, because that could lead to 404 errors, which is sloppy. I see you've guarded against that here, but I worry about the precedent this sets.
For the insecure cluster warning, we defined the endpoint in OSS and allowed x-pack security to augment the logic. This allows the endpoint to work in all distributions, while still allowing for distribution-specific behavior.
Yeah, from the technical side I agree with your sentiment. It just feels to me that we're building all these abstractions only to pretend that there is no explicit dependency between OSS and X-Pack plugin parts when in reality the only purpose of the the X-Pack_Something_OSS plugins is to support their main plugins across distributions while being useless on its own. The insecure cluster warning is just a great exception that does its job even without X-Pack part though.
Let me see if I can find a better way to preserve a status quo here 😛
| // We should use credentials of the anonymous service account instead of credentials of the | ||
| // current user to figure out if the specified Saved Object type can be accessed anonymously. | ||
| const anonymousAuthorizationHeader = this.createAnonymousAuthorizationHeader(); | ||
| const fakeAnonymousRequest = KibanaRequest.from(({ |
There was a problem hiding this comment.
question: do we have any better option to do what I do right now?
There was a problem hiding this comment.
Not really 🙁. Core support for something better is being discussed in #39430, but for the time being this is the generally accepted short-term solution.
There was a problem hiding this comment.
Got it, thanks for confirming!
| }: PluginSetupDependencies | ||
| ) { | ||
| const [config, legacyConfig] = await combineLatest([ | ||
| this.configSubscription = combineLatest([ |
There was a problem hiding this comment.
note: finally making our setup sync.
| }, | ||
| }); | ||
|
|
||
| core.capabilities.registerProvider(() => ({ |
There was a problem hiding this comment.
note: I was hesitating about this one since it has nothing to do with the feature controls (FC) and UI capabilities are mainly used with FC right now. But if I understand the idea behind Core's/OSS UI capabilities then it's not necessarily related to the FC or Security in general, so I thought that anonymous access can be treated as a capability that may affect the way we render UI.
Am I just looking for excuse or you think it's a valid use case for UI capabilities as well? The main motivator is to avoid unnecessary call to the Kibana server on every page refresh just to know that anonymous access isn't enabled (yeah, I'm missing good old injected vars 🙈 )
|
@legrego would you mind giving a preliminary feedback (tests are not ready yet) on the approach I picked in this PR, whenever you have time? I left a couple of questions for you. Thanks! |
legrego
left a comment
There was a problem hiding this comment.
Here are my preliminary thoughts - happy to discuss further! Thanks for putting this together ❤️
| }, | ||
| }); | ||
|
|
||
| core.capabilities.registerProvider(() => ({ |
There was a problem hiding this comment.
I'm also hesitant about this. UI Capabilities were designed to support FC, but that doesn't mean they can't be used for other purposes. That said, I'm not convinced this is the right tool for the job.
The main motivator is to avoid unnecessary call to the Kibana server on every page refresh just to know that anonymous access isn't enabled (yeah, I'm missing good old injected vars 🙈 )
I miss injected vars too! If the network call is the primary motivation, then perhaps we can tweak the security_oss plugin a bit. The OSS plugin makes a call to determine if the "insecure cluster warning" should be shown.
If we wanted to, we could introduce an /internal/security/setup (or similar) call, which can return information for both the "insecure cluster warning" and the state of anonymous access. That would keep the number of network calls the same as it is today, without changing the way that UI Capabilities are used. What do you think?
There was a problem hiding this comment.
If we wanted to, we could introduce an /internal/security/setup (or similar) call, which can return information for both the "insecure cluster warning" and the state of anonymous access. That would keep the number of network calls the same as it is today, without changing the way that UI Capabilities are used. What do you think?
Yeah, I like that! I'll try to do that.
| // We should use credentials of the anonymous service account instead of credentials of the | ||
| // current user to figure out if the specified Saved Object type can be accessed anonymously. | ||
| const anonymousAuthorizationHeader = this.createAnonymousAuthorizationHeader(); | ||
| const fakeAnonymousRequest = KibanaRequest.from(({ |
There was a problem hiding this comment.
Not really 🙁. Core support for something better is being discussed in #39430, but for the time being this is the generally accepted short-term solution.
| ); | ||
|
|
||
| if (hasAllRequested) { | ||
| this.logger.debug( |
There was a problem hiding this comment.
nit: While I'm generally a fan of logging all the things, these might be unnecessary. The return value here (which currently translates to the API route return value) isn't really transformed or fed into another server-side process.
There was a problem hiding this comment.
Was mostly using it to distinguish canAccess: false for the case when user cannot access something and when anonymous is not enabled. But yeah, it makes more sense to log it in the route instead (or wherever this API is used) 👍
| return { | ||
| isAnonymousAccessEnabled: this.isAnonymousAccessEnabled(), | ||
| accessURLParameters: | ||
| anonymousProvider && !anonymousIsDefault |
There was a problem hiding this comment.
question is there harm in supplying the access url parameters if anonymous is the default provider? If an administrator changes the auth provider config, then they'd potentially invalidate any previously generated URLs if the anonymous provider suddenly isn't the default anymore.
There was a problem hiding this comment.
question is there harm in supplying the access url parameters if anonymous is the default provider?
Well, the base assumption here (we'll need to verify) that anonymous access will be the only one or default one in the majority of the setups. And the idea is that Share plugin shouldn't show Public URL option during sharing/embedding if there is nothing special to be added to the URL that would make UI less loaded and confusing.
If an administrator changes the auth provider config, then they'd potentially invalidate any previously generated URLs if the anonymous provider suddenly isn't the default anymore.
Actually it feels reasonable to me to show login selector if the URL was shared without Public URL (was hidden when anonymous was default, so user didn't indicate that this URL should use anonymous access specifically) in case anonymous becomes non-default.
Do you see any issues here?
There was a problem hiding this comment.
Well, the base assumption here (we'll need to verify) that anonymous access will be the only one or default one in the majority of the setups. And the idea is that Share plugin shouldn't show Public URL option during sharing/embedding if there is nothing special to be added to the URL that would make UI less loaded and confusing.
I'd personally be surprised if anonymous access was commonly the only provider, but I take your point here.
Actually it feels reasonable to me to show login selector if the URL was shared without Public URL (was hidden when anonymous was default, so user didn't indicate that this URL should use anonymous access specifically) in case anonymous becomes non-default.
I'm torn on this. From a security perspective, it makes sense to fail closed if the auth config changes in a way that alters the default provider. On the other hand, the administrator has to take explicit action to make the anonymous provider the default/only provider, so you could argue that generating these share links is intentionally making them available anonymously.
Let's keep it the way you have it for now. This is something we can easily change if we decide to take a different approach
| ); | ||
| } | ||
|
|
||
| this.httpAuthorizationHeader = AnonymousAuthenticationProvider.createHTTPAuthorizationHeader( |
| } catch (err) { | ||
| const errorMessage = getDetailedErrorMessage(err); | ||
| if (getErrorStatusCode(err) === 401) { | ||
| logger.error(`Anonymous access may not be properly configured yet: ${errorMessage}`); |
There was a problem hiding this comment.
nit: warn might be a better severity here.
| } | ||
|
|
||
| public start(core: CoreStart) { | ||
| const isAnonymousAccessEnabled = !!core.application.capabilities.security?.anonymousAccess; |
There was a problem hiding this comment.
I think capabilities are more OK than endpoints, but I don't know if we've codified this anywhere.
I don't love the idea of using x-pack endpoints in OSS, because that could lead to 404 errors, which is sloppy. I see you've guarded against that here, but I worry about the precedent this sets.
For the insecure cluster warning, we defined the endpoint in OSS and allowed x-pack security to augment the logic. This allows the endpoint to work in all distributions, while still allowing for distribution-specific behavior.
…has enough privileges.
| /** | ||
| * Defines Security OSS application state. | ||
| */ | ||
| export interface AppState { |
There was a problem hiding this comment.
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.
I'm fine with AppState here. That makes sense to me
|
Thanks for the feedback @legrego ! I took a stub at implementing changes we discussed yesterday, turned out a bit more complex than I expected, but it seems to do the job. Would you mind taking a look whenever you have time and let me know if it corresponds to what you had in mind? |
| } | ||
|
|
||
| public start({ core }: StartDeps): InsecureClusterServiceStart { | ||
| const shouldInitialize = |
There was a problem hiding this comment.
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)
|
|
||
| export interface SecurityOssPluginStart { | ||
| insecureCluster: InsecureClusterServiceStart; | ||
| anonymousAccess: { |
There was a problem hiding this comment.
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.
I agree, I don't see a need to filter out the capabilities object on our side.
| private readonly config$: Observable<ConfigType>; | ||
| private readonly logger: Logger; | ||
|
|
||
| private anonymousAccessServiceProvider?: () => AnonymousAccessService; |
There was a problem hiding this comment.
note: kind of clumsy, but I cannot think of a better approach to pass service that is only available at start.
There was a problem hiding this comment.
Agreed. It's at least consistent with the way we've done this elsewhere 🤷♂️
legrego
left a comment
There was a problem hiding this comment.
Thanks for the edits! The high-level approach here looks good to me, and I think this will work really well for the sharing use case
| 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.
🏅 thanks for falling back with .catch()
|
|
||
| if (!this.isAnonymousAccessEnabled) { | ||
| this.logger.warn('Anonymous access is not enabled'); | ||
| return DEFAULT_CAPABILITIES; |
There was a problem hiding this comment.
We recently introduced the concept of "default capabilities" into core, so I think we should instead use that mechanism here. This will allow us to return the full set of UI capabilities, so consumers won't have to worry as much about undefined properties on the capabilities object.
We'll need to update the resolveCapabilities signature to allow for an optional useDefaultCapabilities boolean, which we can pass through to the underlying function which currently defaults to false.
So the call here would instead look something like:
| return DEFAULT_CAPABILITIES; | |
| return capabilities.resolveCapabilities(fakeRequest, true); |
There was a problem hiding this comment.
Oh, interesting, didn't know that. It sounds like a way to go then, but it can be a bit tricky for the case when anonymous service account isn't properly configured and we'll get 401. If I understand everything correctly we have 3 different cases here:
- Anonymous access is not enabled:
const fakeAnonymousRequest = KibanaRequest.from({ auth: { isAuthenticated: false } });
const caps = await capabilities.resolveCapabilities(fakeAnonymousRequest, { useDefaultCaps: true });- Anonymous access is enabled and properly configured:
const fakeAnonymousRequest = KibanaRequest.from({ auth: { isAuthenticated: true } });
const caps = await capabilities.resolveCapabilities(fakeAnonymousRequest);- Anonymous access is enabled but misconfigured (we don't know that in advance):
const fakeAnonymousRequest = KibanaRequest.from({ auth: { isAuthenticated: true } });
const caps = await capabilities.resolveCapabilities(fakeAnonymousRequest);
// if we get 401 then we'll try again this:
const fakeAnonymousRequest = KibanaRequest.from({ auth: { isAuthenticated: false } });
const caps = await capabilities.resolveCapabilities(fakeAnonymousRequest, { useDefaultCaps: true });For the last case we could also make an additional "pre-flight" request to _authenticate and depending on the result switch useDefaultCaps and isAuthenticated on or off. It may be more correct solution, but I'm not sure if it makes sense to optimize for the case that should be very rare at the cost of an additional request for all cases.
What do you think? I'll go ahead without pre-flight request to _authenticate for now, but can quickly adjust.
There was a problem hiding this comment.
I think you summarized the scenarios correctly here. For the misconfigured case, I don't have a strong opinion either way - _authenticate does feel more correct, so I'm +1 as long as it's not too difficult to implement.
| { 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.
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.
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| private readonly config$: Observable<ConfigType>; | ||
| private readonly logger: Logger; | ||
|
|
||
| private anonymousAccessServiceProvider?: () => AnonymousAccessService; |
There was a problem hiding this comment.
Agreed. It's at least consistent with the way we've done this elsewhere 🤷♂️
| /** | ||
| * Defines Security OSS application state. | ||
| */ | ||
| export interface AppState { |
There was a problem hiding this comment.
I'm fine with AppState here. That makes sense to me
|
|
||
| export interface SecurityOssPluginStart { | ||
| insecureCluster: InsecureClusterServiceStart; | ||
| anonymousAccess: { |
There was a problem hiding this comment.
I agree, I don't see a need to filter out the capabilities object on our side.
|
Pinging @elastic/kibana-security (Team:Security) |
pgayvallet
left a comment
There was a problem hiding this comment.
LGTM for core changes
| resolveCapabilities: (request) => this.resolveCapabilities(request, [], false), | ||
| resolveCapabilities: (request, options) => | ||
| this.resolveCapabilities(request, [], !!options?.useDefaultCapabilities), |
There was a problem hiding this comment.
NIT: imho options?.useDefaultCapabilities ?? false is slightly more explicit.
There was a problem hiding this comment.
Yep, agree. I'll change, thanks!
…e ES native anonymous user case.
|
@elasticmachine merge upstream |
|
I'll try to finish reviewing this tomorrow 👍 |
legrego
left a comment
There was a problem hiding this comment.
LGTM with some optional nits - tested locally, and all seems to be working great!
| return anonymousAccess.accessURLParameters; | ||
| }, | ||
| async getCapabilities() { | ||
| return await core.http.get<Capabilities>( |
There was a problem hiding this comment.
optional nit: unnecessary await
There was a problem hiding this comment.
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.
| const appState: AppState = { | ||
| insecureClusterAlert: { displayAlert }, | ||
| anonymousAccess: { | ||
| isEnabled: !!anonymousAccessService?.isAnonymousAccessEnabled, |
There was a problem hiding this comment.
optional nit: similar to #87091 (comment):
| isEnabled: !!anonymousAccessService?.isAnonymousAccessEnabled, | |
| isEnabled: anonymousAccessService?.isAnonymousAccessEnabled ?? false, |
There was a problem hiding this comment.
Thanks, I need to get used to this better/more readable alternative!
| } | ||
|
|
||
| // We should use credentials of the anonymous service account instead of credentials of the | ||
| // current user to figure out if the specified Saved Object type can be accessed anonymously. |
There was a problem hiding this comment.
nit: the comment about "specified Saved Object type" isn't relevant anymore, now that we're performing a capabilities check instead of an explicit Saved Object Type check
| authenticateRequest && this.httpAuthorizationHeader | ||
| ? { authorization: this.httpAuthorizationHeader.toString() } | ||
| : {}, | ||
| // This flag is essential for the security capability switcher that relies on it to decide if |
💚 Build SucceededMetrics [docs]Module Count
Page load bundle
History
To update your PR or re-run it, just comment with: |
|
7.x/7.12.0: 93db245 |
In this PR I'm introducing a new
Anonymous Accessdomain that spans both authentication and authorization pieces and will serve as a base for any future improvements related to the anonymous access.There are four main changes in this PR:
AnonymousAccessServicethat can figure out if anonymous service account can successfully authenticate and what capabilities it has. It's registered with Security OSS plugin./internal/security_oss/anonymous_access/capabilitiesSpaces aware endpoint that can be used to check what capabilities are available to anonymous service account using the registeredAnonymousAccessService.startcontract:/internal/security_oss/app_stateendpoint instartto fetch the application state.Prerequisite for: #86965 and #83650