Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Organization Ability Flags
# Organization ability flags

## Overview

Expand All @@ -7,11 +7,11 @@ while Event Logs are available to Teams and Enterprise plans. When developing fe
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
## The rule

**Never check plan types to control feature access.** Always use a dedicated ability flag on the Organization entity.

### ❌ Don't Do This
### ❌ Don't do this

```csharp
// Checking plan type directly
Expand All @@ -23,7 +23,7 @@ if (organization.PlanType == PlanType.Enterprise ||
}
```

### ❌ Don't Do This
### ❌ Don't do this

```csharp
// Piggybacking off another feature's ability
Expand All @@ -33,17 +33,18 @@ if (organization.PlanType == PlanType.Enterprise && organization.UseEvents)
}
```

### βœ… Do This Instead
### βœ… Do this instead

```csharp
// Check the explicit ability flag
if (organization.UseEvents)
if (!organization.UseEvents)
{
// allow UseEvents feature...
throw new BadRequestException("Your organization does not have access to this feature.");
}
// proceed with feature logic...
```

## Why This Pattern Matters
## Why this pattern matters

Using explicit ability flags instead of plan type checks provides several benefits:

Expand All @@ -53,21 +54,46 @@ Using explicit ability flags instead of plan type checks provides several benefi
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
- Custom arrangements for specific customers (can be manually toggled in Bitwarden Portal)
- A/B testing of features across different cohorts
- Gating high-risk features behind internal support teams (e.g., Key Connector)

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
6. **Semantic Code** β€” The code clearly expresses what capability is being checked, making it more maintainable.

## Organization abilities and other features

Organization abilities work alongside other access control mechanisms. Understanding the differences helps you choose the right tool:

| | **Organization abilities** (this document) | **Feature flags** | **Enterprise policies** |
|-------------------|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------|-------------------------------------------------------------------------|
| **Purpose** | Control whether an organization has **access** to a feature | Control feature **rollout** and act as a killswitch if necessary | Control **behavior** of features the organization already has access to |
| **Set by** | Subscription plan (automatically) or internal support teams (manual override via Bitwarden Portal) | Engineering teams | Organization admins and owners |
| **Lifecycle** | Permanent - part of the core product | Temporary - removed once feature is stable | Permanent - part of the core product |
| **Scope** | Per organization | Global or targeted | Per organization |
| **Toggle method** | Bitwarden Portal (single) or data migration (bulk) | LaunchDarkly | In-product via Admin Console |
| **Examples** | Can the org use SSO? Can they use SCIM? Can they use Events? | Is the new API available? Is the redesigned UI enabled? | Require 2FA for all users, enforce password complexity |

### When to use which?

**Use an organization ability** when the feature will be permanently gated behind a subscription tier or our support teams.

**Use a feature flag** when you need to control the release of a new feature.

**Use a policy** when you're adding configurable rules to a feature the organization can already access.

**Use multiple together** when appropriate. For example, a new enterprise feature might use all three: a feature flag to control initial rollout, an organization ability to restrict it to Enterprise plans, and a policy to let admins configure enforcement rules.

## How it works

### Ability Assignment at Signup/Upgrade
### Ability assignment at signup/upgrade

When an organization is created or changes plans, the ability flags are set based on the plan's capabilities:

Expand All @@ -81,48 +107,225 @@ organization.UseEvents = plan.HasEvents;
// ... etc
```

### Modifying Abilities for Existing Organizations
### Accessing abilities in code

**Server-side:**

- If you already have the full `Organization` object in scope, use it directly: `organization.UseMyFeature`
- If not, use the in-memory cache to avoid hitting the database:
`IApplicationCacheService.GetOrganizationAbilityAsync(orgId)`
- This returns an `OrganizationAbility` object - a simplified, cached representation of the ability flags
- Note: some older flags may be missing from `OrganizationAbility` but can be added if needed

**Client-side:**

- Get the organization object from `OrganizationService`, then use it directly: `organization.useMyFeature`

### Manual override via Bitwarden Portal

Organization abilities can be manually toggled for specific customers via the Bitwarden Portal β†’ Organizations page.
This is useful for custom arrangements, early access, or internal testing.

## Adding a new ability

When developing a new plan-gated feature, follow these steps. We use `MyFeature` as a placeholder for your feature name
(e.g., `UseEvents`).

### 1. Update core entities

- `src/Core/AdminConsole/Entities/Organization.cs` β€” Add `UseMyFeature` boolean property
- `src/Core/AdminConsole/OrganizationFeatures/OrganizationAbility/OrganizationAbility.cs` β€” Add to ability object

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:
### 2. Database changes (MSSQL)

Add a new `UseMyFeature` column to the Organization table:

**Files to modify:**

- `src/Sql/dbo/Tables/Organization.sql` β€” Add column with `NOT NULL` constraint and default of `0` (false) for EDD
backward compatibility

**Stored procedures to update:**

- `src/Sql/dbo/Stored Procedures/Organization_Create.sql`
- `src/Sql/dbo/Stored Procedures/Organization_Update.sql`
- `src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql`

**Views to update (add the new column):**

- `src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql`
- `src/Sql/dbo/Views/ProviderUserProviderOrganizationDetailsView.sql`
- `src/Sql/dbo/Views/OrganizationView.sql`

**Views to refresh (use `sp_refreshview`):**

After schema changes, the following views may need to be refreshed even though they don't explicitly include the new
column:

- `src/Sql/dbo/Views/OrganizationCipherDetailsCollectionsView.sql`
- `src/Sql/dbo/Views/ProviderOrganizationOrganizationDetailsView.sql`

**Create a migration script** for these database changes.

### 3. Entity Framework changes

EF is primarily used for self-host. Implementations must be kept consistent.

**Generate EF migrations** for the new column.

**Update queries and initialization code:**

- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs`
- Update `GetManyAbilitiesAsync()` to initialize the new property
- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs`
- Update the integration test:
`test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs`
- `src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/ProviderUserOrganizationDetailsViewQuery.cs`

### 4. Data migrations for existing organizations

If your feature should be enabled for existing organizations on certain plan types, create data migrations to set the
ability flag:

**MSSQL migration:**

```sql
-- Example: Enable UseEvents for all Teams organizations
-- Example: Enable UseMyFeature for all Enterprise organizations
-- Check src/Core/Billing/Enums/PlanType.cs for current values
UPDATE [dbo].[Organization]
SET UseEvents = 1
WHERE PlanType IN (17, 18) -- TeamsMonthly = 17, TeamsAnnually = 18
SET UseMyFeature = 1
WHERE PlanType IN (4, 5, 10, 11, 14, 15, 19, 20) -- All Enterprise plan types (2019, 2020, 2023, current)
```

Then update the plan-to-ability assignment code so new organizations get the correct value.
**EF migration:**

Create a corresponding data migration for EF databases used by self-hosted instances.

## Adding a New Ability
### 5. Server code changes

When developing a new plan-gated feature:
Update related models and mapping code so models receive the new value.

1. **Add the ability to the Organization and OrganizationAbility entities** β€” Create a `Use[FeatureName]` boolean
property.
**Response models:**

2. **Add a database migration** β€” Add the new column to the Organization table.
- `src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs`
- `src/Api/AdminConsole/Models/Response/BaseProfileOrganizationResponseModel.cs`

3. **Update plan definitions** β€” Add a corresponding `Has[FeatureName]` property to the Plan model and configure which
plans include it.
**Data models:**

4. **Update organization creation/upgrade logic** β€” Ensure the ability is set based on the plan.
- `src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs`
- `src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs`
- `src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs`
- `src/Core/AdminConsole/Models/Data/IProfileOrganizationDetails.cs`

5. **Update the organization license claims** (if applicable) - to make the feature available on self-hosted instances.
**Plan definition and signup logic:**

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.
If your feature should be automatically enabled based on plan type at signup (e.g., SSO for Enterprise plans), you'll
need to:

1. Work with the Billing Team to add a `HasMyFeature` property to the Plan model and configure which plans include it
2. Update `src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs` to map
`plan.HasMyFeature` to `organization.UseMyFeature`

**Note:** This step is not required if your feature is enabled manually via the Admin Portal.

### 6. Client changes

**TypeScript models to update:**

- `libs/common/src/admin-console/models/response/profile-organization.response.ts`
- `libs/common/src/admin-console/models/response/organization.response.ts`
- `libs/common/src/admin-console/models/domain/organization.ts`
- `libs/common/src/admin-console/models/data/organization.data.ts`
- Update tests: `libs/common/src/admin-console/models/data/organization.data.spec.ts`

### 7. Bitwarden Portal changes

For manual override capability in the admin portal:

- `src/Admin/AdminConsole/Models/OrganizationEditModel.cs` β€” Map the ability from the organization entity
- `src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml` β€” Add checkbox for the new ability
- `src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml` β€” Add the new ability to the
`togglePlanFeatures()` function so it's automatically set when a plan type is selected
- `src/Admin/AdminConsole/Controllers/OrganizationsController.cs` β€” Update `UpdateOrganization()` method mapping

### 8. Self-host licensing

> ⚠️ **WARNING:** Mistakes in organization license changes can disable the entire organization for self-hosted
> customers!
> Double-check your work and ask for help if unsure.
>
> **Note:** New properties must be added to both the `OrganizationLicense` class and the claims-based system.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is also required to add the new property to the UpdateFromLicense() method in src/Core/AdminConsole/Entities/Organization.cs

**Update OrganizationLicense:**

- `src/Core/Billing/Organizations/Models/OrganizationLicense.cs`
- Add the new property to the class
- `VerifyData()` β€” Add claims validation
- `GetDataBytes()` β€” Add the new property to the ignored fields section (below the comment
`// any new fields added need to be added here so that they're ignored`)

**Add property to Organization entity mapper:**

- `src/Core/AdminConsole/Entities/Organization.cs` β€” Add the new property to the `UpdateFromLicense()` method

**Add claims for the new feature:**

- `src/Core/Billing/Licenses/LicenseConstants.cs` β€” Add constant for the new ability in `OrganizationLicenseConstants`
- `src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs`

**Update license command:**

Map your feature property from the claim to the organization when creating or updating from the license file:

- `src/Core/AdminConsole/Services/OrganizationFactory.cs`
- `src/Core/Billing/Organizations/Commands/UpdateOrganizationLicenseCommand.cs`

**Update tests:**

- `test/Core.Test/Billing/Organizations/Commands/UpdateOrganizationLicenseCommandTests.cs` - add the new property to
`UpdateLicenseAsync_WithClaimsPrincipal_ExtractsAllPropertiesFromClaims` test

> **Tip:** Running tests in `UpdateOrganizationLicenseCommandTests.cs` will help identify any missing changes.
> Test failures will guide you to all areas that need updates.

### 9. Implement business logic checks

In your feature's business logic, check the ability flag:

```csharp
// Retrieve the organization ability (uses cache, avoids DB hit)
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);

if (!orgAbility.UseMyFeature)
{
throw new BadRequestException("Your organization's plan does not support this feature.");
}

// Proceed with feature logic...
```

As explained above, organization abilities work alongside feature flags β€” they don't replace them.
For new features, you'll typically want both:

```csharp
// Check feature flag first (controls rollout)
if (!_featureService.IsEnabled(FeatureFlagKeys.MyFeature))
{
throw new BadRequestException("This feature is not available.");
}

// Then check organization ability (controls plan-based access)
if (!orgAbility.UseMyFeature)
{
throw new BadRequestException("Your organization's plan does not support this feature.");
}
```

## Existing Abilities
## Existing abilities

For reference, here are some current organization ability flags (not a complete list):

| Ability | Description | Plans |
| Ability | Description | Typical Plans |
|--------------------------|-------------------------------|-------------------|
| `UseGroups` | Group-based collection access | Teams, Enterprise |
| `UseDirectory` | Directory Connector sync | Teams, Enterprise |
Expand Down
Loading