From 30fd0c279b9409c22a19873f391b4034781178fc Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 07:45:18 +1000 Subject: [PATCH 1/8] Copy across from google docs --- .../OrganizationAbilities/README.md | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md new file mode 100644 index 000000000000..7ea2e188eef3 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md @@ -0,0 +1,123 @@ +# Organization Ability Flags + +## Overview + +Many Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features, while Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access control, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization entity that indicate whether an organization can use a specific feature. + +## The Rule + +**Never check plan types to control feature access.** Always use a dedicated ability flag on the Organization entity. + +### ❌ Don't Do This + +```csharp +// Checking plan type directly +if (organization.PlanType == PlanType.Enterprise || + organization.PlanType == PlanType.Teams || + organization.PlanType == PlanType.Family) +{ + // allow feature... +} + +### ❌ Don't Do This + +// Piggybacking off another feature's ability +if (organization.PlanType == PlanType.Enterprise && organization.UseEvents) +{ + // assume they can use some other feature... +} +``` + +### ✅ Do This Instead + +```csharp +// Check the explicit ability flag +if (organization.UseEvents) +{ + // allow UseEvents feature... +} +``` + +## Why This Pattern Matters + +Using explicit ability flags instead of plan type checks provides several benefits: + +1. **Simplicity** — A single boolean check is cleaner and less error-prone than maintaining lists of plan types. + +2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization creation/upgrade. No need to hunt through the codebase for scattered plan type checks. + +3. **Flexibility** — Abilities can be set independently of plan type, enabling: + + - Early access programs for features not yet tied to a plan + - Trial access to help customers evaluate a feature before upgrading + - Custom arrangements for specific customers + - A/B testing of features across different cohorts + +4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between tiers), we only update the ability assignment logic—not every place the feature is used. + +5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks automatically respect the new access level. + +## How It Works + +### Ability Assignment at Signup/Upgrade + +When an organization is created or changes plans, the ability flags are set based on the plan's capabilities: + +```csharp +// During organization creation or plan change +organization.UseGroups = plan.HasGroups; +organization.UseSso = plan.HasSso; +organization.UseScim = plan.HasScim; +organization.UsePolicies = plan.HasPolicies; +organization.UseEvents = plan.HasEvents; +// ... etc +``` + +### Modifying Abilities for Existing Organizations + +To change abilities for existing organizations (e.g., rolling out a feature to a new plan tier), create a database migration that updates the relevant flag: + +```sql +-- Example: Enable UseEvents for all Teams organizations +UPDATE [dbo].[Organization] +SET UseEvents = 1 +WHERE PlanType IN ('TeamsAnnually', 'TeamsMonthly') +``` + +Then update the plan-to-ability assignment code so new organizations get the correct value. + +## Adding a New Ability + +When developing a new plan-gated feature: + +1. **Add the ability to the Organization entity** — Create a `Use[FeatureName]` boolean property. + +2. **Add a database migration** — Add the new column to the Organization table. + +3. **Update plan definitions** — Add a corresponding `Has[FeatureName]` property to the Plan model and configure which plans include it. + +4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan. + +5. **Update the license model** (if applicable) — For self-hosted licensing validation. + +6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. + +## Existing Abilities + +For reference, here are some current organization ability flags: + +| Ability | Description | Plans | +| ------------------------ | ----------------------------- | ----------------- | +| `UseGroups` | Group-based collection access | Teams, Enterprise | +| `UseDirectory` | Directory Connector sync | Teams, Enterprise | +| `UseEvents` | Event logging | Teams, Enterprise | +| `UseTotp` | Authenticator (TOTP) | Teams, Enterprise | +| `UseSso` | Single Sign-On | Enterprise | +| `UseScim` | SCIM provisioning | Teams, Enterprise | +| `UsePolicies` | Enterprise policies | Enterprise | +| `UseResetPassword` | Admin password reset | Enterprise | +| `UseOrganizationDomains` | Domain verification/claiming | Enterprise | + +## Questions? + +If you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead or members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the right choice—it's easy to do and keeps our access control clean and maintainable. From 42eb3ee1be3c4980f6fefbb63d36f4d034c60d68 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 07:48:19 +1000 Subject: [PATCH 2/8] Action my feedback --- .../OrganizationFeatures/OrganizationAbilities/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md index 7ea2e188eef3..84eed084a262 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md @@ -90,7 +90,7 @@ Then update the plan-to-ability assignment code so new organizations get the cor When developing a new plan-gated feature: -1. **Add the ability to the Organization entity** — Create a `Use[FeatureName]` boolean property. +1. **Add the ability to the Organization and OrganizationAbility entities** — Create a `Use[FeatureName]` boolean property. 2. **Add a database migration** — Add the new column to the Organization table. @@ -98,9 +98,11 @@ When developing a new plan-gated feature: 4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan. -5. **Update the license model** (if applicable) — For self-hosted licensing validation. +5. **Update the organization license claims** (if applicable) - To make the feature available on self-hosted instances. 6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. + - clients: get the organization object from `OrganizationService`. + - server: use the organization object if it's already in scope, or check the OrganizationAbility via IApplicationCacheService for a high speed cache. ## Existing Abilities From 0db61da0a6778273004942bf87da22fd16a49b44 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 07:51:26 +1000 Subject: [PATCH 3/8] spelling/grammar fixes --- .../OrganizationFeatures/OrganizationAbilities/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md index 84eed084a262..1628066db892 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md @@ -18,9 +18,11 @@ if (organization.PlanType == PlanType.Enterprise || { // allow feature... } +``` ### ❌ Don't Do This +```csharp // Piggybacking off another feature's ability if (organization.PlanType == PlanType.Enterprise && organization.UseEvents) { @@ -98,11 +100,11 @@ When developing a new plan-gated feature: 4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan. -5. **Update the organization license claims** (if applicable) - To make the feature available on self-hosted instances. +5. **Update the organization license claims** (if applicable) - to make the feature available on self-hosted instances. 6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. - - clients: get the organization object from `OrganizationService`. - - server: use the organization object if it's already in scope, or check the OrganizationAbility via IApplicationCacheService for a high speed cache. + - Clients: get the organization object from `OrganizationService`. + - Server: use the organization object if it's already in scope, or check the OrganizationAbility via IApplicationCacheService for a high-speed cache. ## Existing Abilities From ba6c736db354c952bcebae9ff1cf73c34959d883 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 08:04:42 +1000 Subject: [PATCH 4/8] Move OrganizationAbility into this folder --- .../OrganizationAbility}/OrganizationAbility.cs | 0 .../{OrganizationAbilities => OrganizationAbility}/README.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Core/AdminConsole/{Models/Data/Organizations => OrganizationFeatures/OrganizationAbility}/OrganizationAbility.cs (100%) rename src/Core/AdminConsole/OrganizationFeatures/{OrganizationAbilities => OrganizationAbility}/README.md (100%) diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs similarity index 100% rename from src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md similarity index 100% rename from src/Core/AdminConsole/OrganizationFeatures/OrganizationAbilities/README.md rename to src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md From 29b335ac92c43da0f335ae672c97945492a21a6d Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 08:32:15 +1000 Subject: [PATCH 5/8] Claude! Review my code! From 27f60818fec4b75f9a40c30cf47b410232f7e66e Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 08:40:20 +1000 Subject: [PATCH 6/8] Fix sql error --- .../OrganizationFeatures/OrganizationAbility/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md index 1628066db892..2959e05a965d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md @@ -83,7 +83,7 @@ To change abilities for existing organizations (e.g., rolling out a feature to a -- Example: Enable UseEvents for all Teams organizations UPDATE [dbo].[Organization] SET UseEvents = 1 -WHERE PlanType IN ('TeamsAnnually', 'TeamsMonthly') +WHERE PlanType IN (17, 18) -- TeamsMonthly = 17, TeamsAnnually = 18 ``` Then update the plan-to-ability assignment code so new organizations get the correct value. From 2e471171e1c5ff0273ce88807b4444847c312ef8 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 13:27:20 +1000 Subject: [PATCH 7/8] feedback --- .../OrganizationFeatures/OrganizationAbility/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md index 2959e05a965d..24f1f2846233 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md @@ -104,11 +104,14 @@ When developing a new plan-gated feature: 6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. - Clients: get the organization object from `OrganizationService`. - - Server: use the organization object if it's already in scope, or check the OrganizationAbility via IApplicationCacheService for a high-speed cache. + - Server: if you already have the full `Organization` object in scope, you can use it directly. If not, use the + `IApplicationCacheService` to retrieve the `OrganizationAbility`, which is a simplified, cached representation + of the organization ability flags. Note that some older flags may be missing from `OrganizationAbility` but + can be added if needed. ## Existing Abilities -For reference, here are some current organization ability flags: +For reference, here are some current organization ability flags (not a complete list): | Ability | Description | Plans | | ------------------------ | ----------------------------- | ----------------- | From 595546442b2b33daa69509c5ab01395145d07865 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 30 Dec 2025 13:30:02 +1000 Subject: [PATCH 8/8] format --- .../OrganizationAbility/README.md | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md index 24f1f2846233..7b92ba3feff6 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/README.md @@ -2,7 +2,10 @@ ## Overview -Many Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features, while Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access control, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization entity that indicate whether an organization can use a specific feature. +Many Bitwarden features are tied to specific subscription plans. For example, SCIM and SSO are Enterprise features, +while Event Logs are available to Teams and Enterprise plans. When developing features that require plan-based access +control, we use **Organization Ability Flags** (or simply _abilities_) — explicit boolean properties on the Organization +entity that indicate whether an organization can use a specific feature. ## The Rule @@ -46,18 +49,21 @@ Using explicit ability flags instead of plan type checks provides several benefi 1. **Simplicity** — A single boolean check is cleaner and less error-prone than maintaining lists of plan types. -2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization creation/upgrade. No need to hunt through the codebase for scattered plan type checks. +2. **Centralized Control** — Feature access is managed in one place: the ability assignment during organization + creation/upgrade. No need to hunt through the codebase for scattered plan type checks. 3. **Flexibility** — Abilities can be set independently of plan type, enabling: - - Early access programs for features not yet tied to a plan - - Trial access to help customers evaluate a feature before upgrading - - Custom arrangements for specific customers - - A/B testing of features across different cohorts + - Early access programs for features not yet tied to a plan + - Trial access to help customers evaluate a feature before upgrading + - Custom arrangements for specific customers + - A/B testing of features across different cohorts -4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between tiers), we only update the ability assignment logic—not every place the feature is used. +4. **Safe Refactoring** — When plans change (e.g., adding a new plan tier, renaming plans, or moving features between + tiers), we only update the ability assignment logic—not every place the feature is used. -5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks automatically respect the new access level. +5. **Graceful Downgrades** — When an organization downgrades, we update their abilities. All feature checks + automatically respect the new access level. ## How It Works @@ -77,7 +83,8 @@ organization.UseEvents = plan.HasEvents; ### Modifying Abilities for Existing Organizations -To change abilities for existing organizations (e.g., rolling out a feature to a new plan tier), create a database migration that updates the relevant flag: +To change abilities for existing organizations (e.g., rolling out a feature to a new plan tier), create a database +migration that updates the relevant flag: ```sql -- Example: Enable UseEvents for all Teams organizations @@ -92,29 +99,31 @@ Then update the plan-to-ability assignment code so new organizations get the cor When developing a new plan-gated feature: -1. **Add the ability to the Organization and OrganizationAbility entities** — Create a `Use[FeatureName]` boolean property. +1. **Add the ability to the Organization and OrganizationAbility entities** — Create a `Use[FeatureName]` boolean + property. 2. **Add a database migration** — Add the new column to the Organization table. -3. **Update plan definitions** — Add a corresponding `Has[FeatureName]` property to the Plan model and configure which plans include it. +3. **Update plan definitions** — Add a corresponding `Has[FeatureName]` property to the Plan model and configure which + plans include it. 4. **Update organization creation/upgrade logic** — Ensure the ability is set based on the plan. 5. **Update the organization license claims** (if applicable) - to make the feature available on self-hosted instances. 6. **Implement checks throughout client and server** — Use the ability consistently everywhere the feature is accessed. - - Clients: get the organization object from `OrganizationService`. - - Server: if you already have the full `Organization` object in scope, you can use it directly. If not, use the - `IApplicationCacheService` to retrieve the `OrganizationAbility`, which is a simplified, cached representation - of the organization ability flags. Note that some older flags may be missing from `OrganizationAbility` but - can be added if needed. + - Clients: get the organization object from `OrganizationService`. + - Server: if you already have the full `Organization` object in scope, you can use it directly. If not, use the + `IApplicationCacheService` to retrieve the `OrganizationAbility`, which is a simplified, cached representation + of the organization ability flags. Note that some older flags may be missing from `OrganizationAbility` but + can be added if needed. ## Existing Abilities For reference, here are some current organization ability flags (not a complete list): | Ability | Description | Plans | -| ------------------------ | ----------------------------- | ----------------- | +|--------------------------|-------------------------------|-------------------| | `UseGroups` | Group-based collection access | Teams, Enterprise | | `UseDirectory` | Directory Connector sync | Teams, Enterprise | | `UseEvents` | Event logging | Teams, Enterprise | @@ -127,4 +136,6 @@ For reference, here are some current organization ability flags (not a complete ## Questions? -If you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead or members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the right choice—it's easy to do and keeps our access control clean and maintainable. +If you're unsure whether your feature needs a new ability or which existing ability to use, reach out to your team lead +or members of the Admin Console or Architecture teams. When in doubt, adding an explicit ability is almost always the +right choice—it's easy to do and keeps our access control clean and maintainable.