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

[Cloud Security] CSP Integrations PLI auth block #182110

Closed
wants to merge 15 commits into from

Conversation

JordanSh
Copy link
Contributor

@JordanSh JordanSh commented Apr 30, 2024

Summary

Resolves #179412

Video showing that the PLI auth block is only showing when missing the correct PLI, and that it does not effect other integrations

Screen.Recording.2024-05-27.at.19.00.03.mov

Problem

During development, we encountered a challenge with retrieving upsellingServices in our integration forms. This issue was caused by the inability to declare securitySolution as a dependency due to cyclic dependencies. Additionally, wrapping our forms in the security solution context was not feasible since they are registered immediately on plugin startup without being wrapped by our context wrappers.

Solution

There were a few optional solutions, but each had its ups and downs. I've settled on the following solution:

Registering upselling component

{
id: 'cloud_security_posture_integration_installation',
pli: ProductFeatureKey.cloudSecurityPosture,
component: CloudSecurityPosturePLIBlockLazy,
},

In security_solution_serverless plugin, we register our upselling component according to this guide. The registration includes the component itself, a registration id, and the required PLI.

Declaring securitySolution as a runtime dependency

"runtimePluginDependencies": ["securitySolution"],

In cloud_security_posture plugin we declare securitySolution as a run time dependency, this allows us to use core.plugins.onStart('securitySolution'), which is an async method to fetch a plugin contract. Please refer to the PluginsServiceStart interface for more information.

Note that I was not able to use this hook from our useKibana services even though it does return plugins.onStart. I suspect this is also caused due to missing context at the point of registering the integration form, since I was able to use the hook from useKibana on other pages which are rendered inside our context wrappers.

Passing securitySolution contract into our integration form

public start(core: CoreStart, plugins: CspClientPluginStartDeps): CspClientPluginStart {
plugins.fleet.registerExtension({
package: CLOUD_SECURITY_POSTURE_PACKAGE_NAME,
view: 'package-policy-replace-define-step',
Component: (props) => {
return <LazyCspPolicyTemplateForm {...props} pluginsOnStart={core.plugins.onStart} />;
},
});

We pass the method into our integration form and call for the registered security solution runtime dependency.
The expected response is the security solution contract, part of this contract is the upselling services which includes a map for registered components (referred to as sections by this service).

We can get our component from the sections map manually. This component will only return in case the current PLI is lower than the required PLI we registered the component into.

Adding PLI auth block state in fleet

if (!!pliAuthBlockComponent && React.isValidElement(pliAuthBlockComponent)) {
return (
<CreatePackagePolicySinglePageLayout
{...layoutProps}
data-test-subj="pliAuthBlockPackagePolicy"
>
<Suspense fallback={null}>{pliAuthBlockComponent}</Suspense>
</CreatePackagePolicySinglePageLayout>
);
}

In fleet, I've added a new state dedicated to rendering the form wrapper without the actual form or bottom bar, which included interaction buttons, but only the component which will be set into the state. That is to to comply with our design and product demands. I named the state as pliAuthBlockComponent but it can be any component. for the Fleet reviewer, let me know thats ok or would you prefer a more generic naming convention. The set state method is being passed into the replaceDefineStepView component, which we use in order to render our custom forms.

Handling the PLI block state

const { upsellingService, isServerless } = useServerlessServices(pluginsOnStart);
const CspIntegrationInstallationUpsellingPliBlock = upsellingService?.sections.get(
'cloud_security_posture_integration_installation'
);
useEffect(() => {
if (
isServerless &&
!!CspIntegrationInstallationUpsellingPliBlock &&
setPliAuthBlockComponent
) {
setPliAuthBlockComponent(CspIntegrationInstallationUpsellingPliBlock);
}
}, [isServerless, CspIntegrationInstallationUpsellingPliBlock, setPliAuthBlockComponent]);

Back in cloud_seucity_posture integration form, in case we receive our registered component from the upselling service, the set state method will be called, setting our PLI block component and preventing the user from interacting with the page without upgrading its project. Since the execution of this set state method is being controlled by the individual component it was passed into the fleet page itself is oblivious. It is the responsibility of the executioner to make sure this state is only being passed a component, based on the matching PLI needs, and only on serverless environment.

Downsides

  1. Importing the types from security solution is not possible due to cyclic dependencies. I've managed to import the logic but had to copy paste some of the types.
  2. Had to disable a type error over our registered component to fleet which i could not find a way to both keep lazy and pass the needed props into.
  3. Overall optimal solution would have been to be able to use security_solution infra since we are technically a part of it. but because of early decisions, we are rendered on a different scope and their internal infra is unavailable to us unless we expose it by manually inserting it into our security solution context wrapper, which as mentioned, does not wrap the registered forms.

Alternative Solutions

Using Fleet's UI Extension infrastructure

Initially, we explored leveraging Fleet's existing infrastructure to register a new view for pli-auth-block, allowing all packages to utilize this view to render their PLI blocks. The concept was to register the upselling component as usual and then register it into the UI Extension service, which wraps Fleet. Since Fleet's UI is affected, it seemed like a logical approach. This would mean we don't have to add any new dependencies, and not copy any code into our plugin. all logic would be controlled from Security Solution Serverless upselling and Fleet's UI Extension.

However, this direction presented a few challenges. Firstly, it introduced code complexity with double registrations of lazy components. Secondly, it was difficult to reliably detect whether the upselling component returned as a component or not. Since the return value of the registered view was consistently a lazy component (even if it would eventually result to null), making conditional rendering unreliable.

Additionally, This approach centralized the rendering decision in a higher hierarchy. Meaning other integrations were exposed to being blocked by PLI requirements of unrelated integrations if a mistake occurred for one integration.

While leveraging Fleet's UI Extension infrastructure initially seemed promising, we found that its complexity and potential impact on other integrations' PLI handling outweighed its benefits.

Copy pasting upselling service into Cloud Security Posture plugin

Copying the upselling service code into the Cloud Security Posture plugin would spare us from having to work around cyclic dependencies with async calls for the plugin's contract on run time. However, it introduced maintenance overhead due to managing a large portion of copied code, which could cause divergence from updates made to the original upselling service and potential bloated and redundant code.

Exposing upselling service to a different plugin in order to import it without cyclic dependencies

The idea was to import the upselling service from security solution to another plugin, exposing it from that shared plugin, and importing the service to our cloud security solution plugin, without having cyclic dependencies.

This option was not actually tested, I fear it might be unconventional and could lead to confusion or unexpected behavior. I also have some concerns with the added complexity in managing dependencies between plugins. As well as feasibility concerns due to limitations in plugin architecture: we export the plugin's API from its index.ts file, but have access to imported plugins APIs from the setup and start functions in the plugin.ts file.

@JordanSh JordanSh changed the title WIP [Cloud Security] CSP Integrations PLI auth block May 27, 2024
@JordanSh
Copy link
Contributor Author

JordanSh commented Jun 2, 2024

/ci

@kibana-ci
Copy link
Collaborator

kibana-ci commented Jun 2, 2024

💔 Build Failed

Failed CI Steps

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@JordanSh
Copy link
Contributor Author

I'm closing this in favor of a preferred method which can be seen here:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Cloud Security] Serverless authorization block Phase 1 - CSPM/KSPM/CNVM
2 participants