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

[PM-14439] Add PolicyRequirements for enforcement logic #5336

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

eliykat
Copy link
Member

@eliykat eliykat commented Jan 29, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/jira/dashboards/10012

📔 Objective

Background

Enterprise policies are organization-wide settings that limit app behavior for members. For example, if the Disable Send policy is enabled, members of that organization cannot use the Send feature.

Policies are complex because they're cross-domain and cross-team, affecting any area of the application. They can also have different enforcement logic. For example:

  • most policies exempt Owners and Admins from their effect, but some don't
  • the SSO Policy sometimes exempts Owners, but this depends on an IGlobalSettings override
  • most policies are only enforced if the user is in an Accepted state, but some need to be checked before then
  • different organizations can have conflicting policies which must be reconciled

Today, this is mostly handled by PolicyService. However, this has to know about every type of policy, which is not very maintainable or extendible. It also has a poor interface:

  • AnyPoliciesApplicableToUserAsync returns true if any policy of a given type is Enabled - which is handy for very simple boolean policies, but only covers a subset of policies.
  • GetPoliciesApplicableToUserAsync returns all policies of a given type that should be enforced against a user, but then leaves the caller to evaluate and reconcile the Policy objects (including deserializing the Data json blob).

Additionally, many devs don't know about this, and access policies directly using the repository. This bypasses the business logic entirely and leads to repeated bugs.

Proposed solution

  1. We need to stop dealing with raw Policy objects in our business logic. This PR introduces the idea of a "policy requirement", which is the result of filtering and combining policies to create a single object. This object represents business requirements rather than data. In other words, it's a bridge between the policy domain and the caller's domain. Callers no longer have to know about the underlying policies.
  2. A policy requirement is defined for each policy type. It can be anything - whatever is most convenient for representing a policy type's business requirements.
  3. Policy requirements define their own factory function, which takes (at a minimum) all policies that could potentially affect the user. This function defines the policy type's specific rules, e.g. who it applies to.
  4. Policy requirements are registered with the PolicyRequirementQuery.
  5. Callers can call the PolicyRequirementQuery to get the requirement they want to check.

This PR implements a draft version of this for feedback.

TODO

  • enable nullable
  • review requirements to make sure their business logic is correct
  • update all calling code to use the new query
  • use feature flags
  • SQL migration script
  • EF implementation
  • (in a later PR) consider adding a response model for each requirement and including it in sync data (so the clients don't have to reimplement this logic)

📸 Screenshots

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Member Author

@eliykat eliykat Jan 29, 2025

Choose a reason for hiding this comment

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

Point of interest: this sproc only returns policies where they're enabled and the organization's plan supports them. This is a fundamental requirement of the policy domain so it's not left up to the policy requirements to decide. (Of course if this changed in the future we could lift it up.)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is an intentional split of responsibilities here: all business logic is in the policy requirements, which are written in a functional style. The query is agnostic about what policies are being handled - its only job is to connect policy requirements to dependencies.

Copy link
Member Author

Choose a reason for hiding this comment

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

Point of interest: we have a separate policy type for "Disable Send" and "Send Options". In practice we always need to check both, so we can encapsulate them in the same requirement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Point of interest: an example of a more complex requirement that can encapsulate additional logic (beyond enabled/disabled) that is currently in the caller (in this case, OrganizationService).

Copy link
Member Author

@eliykat eliykat Jan 29, 2025

Choose a reason for hiding this comment

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

Simple example usage by an outside caller.

@eliykat eliykat changed the title Policy requirements refactor [Server] Add PolicyRequirements for enforcement logic Jan 29, 2025
Comment on lines +19 to +21
_policyRequirements.Add(SendPolicyRequirement.Create);
_policyRequirements.Add(up
=> SsoPolicyRequirement.Create(up, globalSettings.Sso));
Copy link
Member Author

@eliykat eliykat Jan 29, 2025

Choose a reason for hiding this comment

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

Example of how you can just register the factory function directly for simple requirements (which will be most of them), or inject additional values if needed (settings, feature flags, etc).

Copy link
Contributor

Choose a reason for hiding this comment

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

Just an idea but maybe we could use Reflection here to find all classes that inherit IPolicyRequirement and automatically register them. Maybe it adds some unnecessary complexity though.

@eliykat eliykat changed the title [Server] Add PolicyRequirements for enforcement logic [PM-14439] Add PolicyRequirements for enforcement logic Jan 29, 2025
Copy link
Contributor

github-actions bot commented Jan 29, 2025

Logo
Checkmarx One – Scan Summary & Details0c734754-7fb5-43a9-9a6b-de64550906e5

Great job, no security vulnerabilities found in this Pull Request

Copy link
Contributor

@r-tome r-tome left a comment

Choose a reason for hiding this comment

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

This is looking very good! Please add some xmldoc to the interfaces


public class SendPolicyRequirement : IPolicyRequirement
{
public bool DisableSend { get; init; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public bool DisableSend { get; init; }
public bool DisableSend { get; private init; }

Copy link
Contributor

Choose a reason for hiding this comment

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

This is great!


public static IEnumerable<OrganizationUserPolicyDetails> ExcludeOwnersAndAdmins(
this IEnumerable<OrganizationUserPolicyDetails> userPolicyDetails) =>
userPolicyDetails.Where(x => x.OrganizationUserType != OrganizationUserType.Owner);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
userPolicyDetails.Where(x => x.OrganizationUserType != OrganizationUserType.Owner);
userPolicyDetails.Where(x => x.OrganizationUserType is not OrganizationUserType.Owner and not OrganizationUserType.Admin);

Comment on lines +19 to +21
_policyRequirements.Add(SendPolicyRequirement.Create);
_policyRequirements.Add(up
=> SsoPolicyRequirement.Create(up, globalSettings.Sso));
Copy link
Contributor

Choose a reason for hiding this comment

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

Just an idea but maybe we could use Reflection here to find all classes that inherit IPolicyRequirement and automatically register them. Maybe it adds some unnecessary complexity though.

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.

2 participants