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

Implement new extension points in IdentityPlugin and add ContextProvidingPluginSubject #4665

Open
wants to merge 57 commits into
base: main
Choose a base branch
from

Conversation

cwperks
Copy link
Member

@cwperks cwperks commented Aug 19, 2024

Description

Companion PR in core: opensearch-project/OpenSearch#14630

This PR by itself does not add additional functionality, it simply implements the new extension points in core and introduces a new class called ContextProvidingPluginSubject which populates a header in the ThreadContext with the canonical class name of the plugin that is executing code using pluginSystemSubject.runAs(() -> { ... }). See ./gradlew integrationTest --tests SystemIndexTests for an example that verifies that a block of code run with pluginSystemSubject.runAs(() -> { ... }) has contextual info in the thread context.

  • Category (Enhancement, New feature, Bug fix, Test fix, Refactoring, Maintenance, Documentation)

Enhancement

Issues Resolved

Related to #4439

Check List

  • New functionality includes testing
  • New functionality has been documented
  • New Roles/Permissions have a corresponding security dashboards plugin PR
  • API changes companion pull request created
  • Commits are signed per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Signed-off-by: Craig Perkins <[email protected]>
Signed-off-by: Craig Perkins <[email protected]>
Signed-off-by: Craig Perkins <[email protected]>
Copy link
Member

@peternied peternied left a comment

Choose a reason for hiding this comment

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

@cwperks Thanks for working on this - great forward movement to granularize the permissions available to plugins.

I'm having a little trouble grokking the scope of these changes - it seems like there should be a new component that serves as a source of truth for plugin permissions that is missing - I've called this out.

Could you create a diagram showing the new parts of the system and how they interplay with with the existing security architecture?

Finally, can you speak to where this design change is in the security review process?

@@ -97,5 +98,7 @@ Set<String> getAllPermittedIndicesForDashboards(

SecurityRoles filter(Set<String> roles);

SecurityRoles createSecurityRole(String roleName, Set<String> clusterPerms, Map<String, Set<String>> indexPatternToAllowedActions);
Copy link
Member

Choose a reason for hiding this comment

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

createSecurityRole doesn't align with how these configuration has been used before, its meant to be a representation of the security configuration from the security index, not a way to update/generate/create security objects.

Rather than overload SecurityRole with this responsibility, lets make a new component that is initialized in its own way and separately from our existing configuration. Its strange to have the logic to create these roles inside of configuration model specific code and for its state to be held inside of the privilege evaluator.

/**
* @return true if this instance is of the type PluginUser
*/
public boolean isPluginUser() {
Copy link
Member

Choose a reason for hiding this comment

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

This goes against the pattern of polymorphism, the base class shouldn't be updated to know if it is a subclass. This forces callers of User to determine what subclasses are supported.

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed to using instanceof. I'll look into other ways to do this as well, but I'm reticent to user the same user attributes approach that we used with service accounts.

Copy link
Member

Choose a reason for hiding this comment

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

Can you help me understand why not? To take this to an extreme, if a user object representing plugin could have bad interactions with existing scenarios, why re-use the User object at all?

@cwperks
Copy link
Member Author

cwperks commented Sep 20, 2024

@peternied Thanks for the review! I'll get back to the comments ASAP.

I think you may be interested in this PR I made in core that allows a plugin dev to specify what transport actions a plugin would need to perform outside the authenticated user context. The security plugin is a good example because there's a use-case where security needs to be able to write to the audit log index (if using an opensearch index) regardless of the authenticated user's permissions.

Does that help answer your first question?

I'm having a little trouble grokking the scope of these changes - it seems like there should be a new component that serves as a source of truth for plugin permissions that is missing - I've called this out.

In the PR in core, the cluster admin will be prompted at installation time what a plugin is requesting to be able to perform.

That PR was made in response to what you called out earlier and hearing back from plugin devs on opensearch-project/opensearch-plugins#238

Edit: Plugins will also inherently have system index access to the system indices that they have registered with SystemIndexPlugin.getSystemIndexDescriptors

@cwperks
Copy link
Member Author

cwperks commented Sep 20, 2024

createSecurityRole doesn't align with how these configuration has been used before, its meant to be a representation of the security configuration from the security index, not a way to update/generate/create security objects.

Rather than overload SecurityRole with this responsibility, lets make a new component that is initialized in its own way and separately from our existing configuration. Its strange to have the logic to create these roles inside of configuration model specific code and for its state to be held inside of the privilege evaluator.

That's a good point. I pushed a commit to abstract this out into a separate class called InMemorySecurityRolesV7 and made it distinct from any of the ConfigV7/ConfigV6 classes which are backed by the security index.

@cwperks
Copy link
Member Author

cwperks commented Sep 20, 2024

@peternied Here's some mermaid diagrams that I made with the help of ChatGPT-4o

Here's the prompt:

In the plugin installation process:

  1. OpenSearch will read both plugin-security.policy (JSM Permissions) + plugin-permissions.yml (requested transport actions) and prompt the administrator about the requested permissions. Administrator can choose to accept the risks or abort the plugin installation process.

In the OpenSearch Bootstrap process:

  1. The IdentityService is initialized using a singular IdentityPlugin
  2. For each IdentityAwarePlugin, the IdentityPlugin will assign a PluginSubject
  3. The plugin uses the pluginSubject to execute transport actions using the identity associated with the plugin

Low-Level Detail

The permissions defined in plugin-permissions.yml are used to create an In Memory role that allows the security plugin to re-use all existing authz logic

pluginSubject.runAs(() -> { ... }) is a direct replacement for try (ThreadContext.StoredContext ctx = threadContext.stashContext()) { ... }


Plugin Installation Sequence

sequenceDiagram
    participant Admin as Administrator
    participant OpenSearch as OpenSearch
    participant Plugin as Plugin
    
    OpenSearch ->> Plugin: Reads `plugin-security.policy` (JSM Permissions)
    OpenSearch ->> Plugin: Reads `plugin-permissions.yml` (Transport Actions)
    OpenSearch ->> Admin: Prompt for requested permissions
    
    alt Admin accepts risks
        Admin ->> OpenSearch: Accept permissions
        OpenSearch ->> Plugin: Continue with installation
    else Admin aborts
        Admin ->> OpenSearch: Abort installation
    end
Loading

OpenSearch Bootstrap Sequence:

sequenceDiagram
    participant Bootstrap as OpenSearch Bootstrap
    participant IdentityService as IdentityService
    participant IdentityPlugin as IdentityPlugin
    participant Plugin as IdentityAwarePlugin
    
    Bootstrap ->> IdentityService: Initialize IdentityService
    IdentityService ->> IdentityPlugin: Use singular IdentityPlugin
    
    loop For each IdentityAwarePlugin
        IdentityPlugin ->> Plugin: Assign PluginSubject
        Plugin ->> IdentityPlugin: PluginSubject executes transport actions
    end
    
    Plugin ->> Plugin: Executes actions with associated identity
Loading

@nibix
Copy link
Collaborator

nibix commented Sep 20, 2024

Just one heads up that one goal of #4380 is to remove SecurityRoles alltogether.

Signed-off-by: Craig Perkins <[email protected]>
Copy link
Member

@peternied peternied left a comment

Choose a reason for hiding this comment

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

Thanks for iterating on this @cwperks we are getting closer.

@nibix if you wouldn't mind could you make sure the outstanding comment threads I've created are resolved as appropriate - when this is done please dismiss my review if you think the sprint of the feedback has been addressed. Thanks!

presponse.markComplete();
return;
}
}

if (user instanceof PluginUser) {
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem necessary, if the user principle that represents a plugin is trustworthy the lookup for allowed system indices should return an empty set for a normal user. Lets treat all users the same in code, but allow there configuration data to be populated in a way that distinguishes them well.

By special casing for a plugin user vs normal user create a huge potential for bugs down the line where these permissions are not accounted correctly that would result in a CVE. Note; If there are performance concerns then we need to revisit how these data structures are build to be O(1) lookup.

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes sense, I just pushed a change gets rid of the need for the subclass.

I plan to use a username convention for these special "plugin users". The name will be plugin:<pluginCanonicalClassName>. I'm choosing this because : is restricted from usernames, so this will provide assurance that this is a special kind of system username and not a user that could have been created through any other means.

@cwperks
Copy link
Member Author

cwperks commented Oct 2, 2024

Sync'ed with the latest from main

@cwperks
Copy link
Member Author

cwperks commented Oct 4, 2024

Resolved conflicts after merge of latest PRs

@cwperks
Copy link
Member Author

cwperks commented Oct 4, 2024

Just one heads up that one goal of #4380 is to remove SecurityRoles alltogether.

Thank you @nibix! I will check to see what changes are necessary to incorporate this change with Optimized Privileges Evaluation. I'm introducing the "in-memory" role for a plugin in this PR with the goal of eventually incorporating the changes from this PR in core so that we can re-use the authz checks when plugins want to operate outside the authenticated user context.

Copy link
Member

@peternied peternied left a comment

Choose a reason for hiding this comment

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

Thanks for making progress here, I've created some new comment threads and followed up on existing ones.


import org.opensearch.identity.PluginSubject;

public class PluginContextSwitcher {
Copy link
Member

Choose a reason for hiding this comment

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

All User's support runAs(...), not sure what the case for this class is

Copy link
Member Author

@cwperks cwperks Oct 14, 2024

Choose a reason for hiding this comment

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

Its a vehicle for carrying the subject that's been assigned to a plugin to the transport layer. Take a look at SystemIndexPlugin1 where it instantiates class and returns it from createComponents so that its injectable to transport actions.

Comment on lines 298 to 302
if (user.isPluginUser()) {
securityRoles = getSecurityRoleForPlugin(user.getName());
} else {
securityRoles = getSecurityRoles(mappedRoles);
}
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't special case user vs non-user, seems like they could both be checked against either registry.

Copy link
Member Author

Choose a reason for hiding this comment

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

Abstracted this logic to a separate method. The problem with the latter in this block is that its actually a filtering operation and it filters based on the roles in the security index.

For these plugin users, the permissions are not tied to the security index. If opensearch-project/OpenSearch#15778 gets merged, then the permissions given to a plugin are defined in a yaml file that is read on bootstrap and kept in the memory of the process. There is no plan to support dynamic updates to these permissions as they are static and prompted to a cluster administrator on plugin install to accept.

@@ -47,6 +47,10 @@ public Set<String> getMissingPrivileges() {
return new HashSet<String>(missingPrivileges);
}

public boolean addMissingPrivileges(String action) {
Copy link
Member

Choose a reason for hiding this comment

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

Good idea!

Copy link
Member Author

@cwperks cwperks Oct 14, 2024

Choose a reason for hiding this comment

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

IMO This should always have been exposed through methods instead of directly accessing fields of this class.

presponse.markComplete();
return;
}
}

if (user.isPluginUser()) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove the distinction between different kinds of users, it adds complexity in these downstream systems that will make the code harder to maintain over time.

Copy link
Member

Choose a reason for hiding this comment

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

Note; It seems reasonable that no matter the source so long as the expected permissions grants are on the user (if it represents a person or a plugin) they can access system indices even if practically they aren't available at this point in time. What do you think?

Copy link
Member Author

@cwperks cwperks Oct 14, 2024

Choose a reason for hiding this comment

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

I think this distinction should be kept here since this is an "ownership" model. Authorization is done based on identity here and not a role. i.e. PluginA registers SystemIndexA and gets full access to it by default.

We have a separate mechanism for provisioning system index access to regular user's but it relies on a feature being enabled. The feature flag is plugins.security.system_indices.permission.enabled and its off by default.

Copy link
Member

Choose a reason for hiding this comment

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

Authorization is done based on identity here and not a role. i.e. PluginA registers SystemIndexA and gets full access to it by default.

Why add an alternative authorization model for this case? Seems like this makes the system more complex to understand and verify vs role checks.

Copy link
Member

Choose a reason for hiding this comment

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

@nibix @DarshitChanpura I've got a preference for consistent treatment Users objects no matter if they represent a person or a plugin, I'd be curious for your opinions one way or the other.

Copy link
Member Author

@cwperks cwperks Oct 15, 2024

Choose a reason for hiding this comment

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

@peternied For what its worth, there is service account specific logic in the same file. Service account is analogous to plugin user, but the difference is that its meant for extensions. i.e. If there are extensions in a cluster that register system indices, then on bootstrap the security plugin issues a token to the extension that it can use to access its registered system indices if it wants to use OpenSearch as a datastore.

Edit: IMO I'd be in favor of unifying the service account logic and plugin user logic longer-term. I think there would need to be some additional changes in the extension registration process to allow configuration of security settings like reserving (system) indices.

Copy link
Member

Choose a reason for hiding this comment

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

From what I understand, we are trying to introduce a concept of Plugin Awareness. This requires Identification of the plugin. To enforce this identity we are leveraging already existing User authz flow. Each plugin should have access to its own resource by default, but no other indices. We already have "permission-types" in place, so IMO introducing a plugin user is manageable. We can mark plugin specific users and roles as reserved and static (probly hidden?) to prevent them from being unintentionally modified.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can mark plugin specific users and roles as reserved and static (probly hidden?) to prevent them from being unintentionally modified.

These aren't tied to an index. They are purely in-memory and cannot be altered.

/**
* @return true if this instance is of the type PluginUser
*/
public boolean isPluginUser() {
Copy link
Member

Choose a reason for hiding this comment

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

Can you help me understand why not? To take this to an extreme, if a user object representing plugin could have bad interactions with existing scenarios, why re-use the User object at all?

@cwperks
Copy link
Member Author

cwperks commented Oct 14, 2024

Can you help me understand why not? To take this to an extreme, if a user object representing plugin could have bad interactions with existing scenarios, why re-use the User object at all?

The only real distinction is in that logic to get the "roles". For a regular user, it would filter from the roles that are in the cache, but for a plugin user it would get or create an instance of a role that's only kept in the memory of the process and not backed by the security index. With this PR, pluginSubject's are limited to interactions only with their own system indices. I plan to do a follow-up to this PR once another corresponding PR in core is merged that let's plugins request additional permissions at installation time.

Again, the distinction is in how to compute the role or roles that are used to evaluate permissions. I will abstract the logic into a single method.

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.

4 participants