diff --git a/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs
new file mode 100644
index 0000000000..e904080043
--- /dev/null
+++ b/src/Api/AdminConsole/Authorization/Requirements/BasePermissionRequirement.cs
@@ -0,0 +1,24 @@
+using Bit.Core.Context;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data;
+
+namespace Bit.Api.AdminConsole.Authorization.Requirements;
+
+///
+/// A base implementation of which will authorize Owners, Admins, Providers,
+/// and custom users with the permission specified by the permissionPicker constructor parameter. This is suitable
+/// for most requirements related to a custom permission.
+///
+/// A function that returns a custom permission which will authorize the action.
+public abstract class BasePermissionRequirement(Func permissionPicker) : IOrganizationRequirement
+{
+ public async Task AuthorizeAsync(CurrentContextOrganization? organizationClaims,
+ Func> isProviderUserForOrg)
+ => organizationClaims switch
+ {
+ { Type: OrganizationUserType.Owner } => true,
+ { Type: OrganizationUserType.Admin } => true,
+ { Type: OrganizationUserType.Custom } when permissionPicker(organizationClaims.Permissions) => true,
+ _ => await isProviderUserForOrg()
+ };
+}
diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs
deleted file mode 100644
index 268fee5d95..0000000000
--- a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#nullable enable
-
-using Bit.Core.Context;
-using Bit.Core.Enums;
-
-namespace Bit.Api.AdminConsole.Authorization.Requirements;
-
-public class ManageAccountRecoveryRequirement : IOrganizationRequirement
-{
- public async Task AuthorizeAsync(
- CurrentContextOrganization? organizationClaims,
- Func> isProviderUserForOrg)
- => organizationClaims switch
- {
- { Type: OrganizationUserType.Owner } => true,
- { Type: OrganizationUserType.Admin } => true,
- { Permissions.ManageResetPassword: true } => true,
- _ => await isProviderUserForOrg()
- };
-}
diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs
deleted file mode 100644
index 84f38e36c2..0000000000
--- a/src/Api/AdminConsole/Authorization/Requirements/ManageUsersRequirement.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-#nullable enable
-
-using Bit.Core.Context;
-using Bit.Core.Enums;
-
-namespace Bit.Api.AdminConsole.Authorization.Requirements;
-
-public class ManageUsersRequirement : IOrganizationRequirement
-{
- public async Task AuthorizeAsync(
- CurrentContextOrganization? organizationClaims,
- Func> isProviderUserForOrg)
- => organizationClaims switch
- {
- { Type: OrganizationUserType.Owner } => true,
- { Type: OrganizationUserType.Admin } => true,
- { Permissions.ManageUsers: true } => true,
- _ => await isProviderUserForOrg()
- };
-}
diff --git a/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs b/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs
new file mode 100644
index 0000000000..e3100aff11
--- /dev/null
+++ b/src/Api/AdminConsole/Authorization/Requirements/PermissionRequirements.cs
@@ -0,0 +1,11 @@
+namespace Bit.Api.AdminConsole.Authorization.Requirements;
+
+public class AccessEventLogsRequirement() : BasePermissionRequirement(p => p.AccessEventLogs);
+public class AccessImportExportRequirement() : BasePermissionRequirement(p => p.AccessImportExport);
+public class AccessReportsRequirement() : BasePermissionRequirement(p => p.AccessReports);
+public class ManageAccountRecoveryRequirement() : BasePermissionRequirement(p => p.ManageResetPassword);
+public class ManageGroupsRequirement() : BasePermissionRequirement(p => p.ManageGroups);
+public class ManagePoliciesRequirement() : BasePermissionRequirement(p => p.ManagePolicies);
+public class ManageScimRequirement() : BasePermissionRequirement(p => p.ManageScim);
+public class ManageSsoRequirement() : BasePermissionRequirement(p => p.ManageSso);
+public class ManageUsersRequirement() : BasePermissionRequirement(p => p.ManageUsers);
diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs b/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs
new file mode 100644
index 0000000000..07d263b263
--- /dev/null
+++ b/test/Api.Test/AdminConsole/Authorization/Requirements/BasePermissionRequirementTests.cs
@@ -0,0 +1,66 @@
+using Bit.Api.AdminConsole.Authorization.Requirements;
+using Bit.Core.Context;
+using Bit.Core.Enums;
+using Bit.Core.Test.AdminConsole.AutoFixture;
+using Bit.Core.Test.AdminConsole.Helpers;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Xunit;
+
+namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
+
+public class BasePermissionRequirementTests
+{
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)]
+ public async Task Authorizes_Owners(CurrentContextOrganization organizationClaims)
+ {
+ var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)]
+ public async Task Authorizes_Admins(CurrentContextOrganization organizationClaims)
+ {
+ var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
+ public async Task Authorizes_Providers(CurrentContextOrganization organizationClaims)
+ {
+ var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(true));
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
+ public async Task Authorizes_CustomPermission(CurrentContextOrganization organizationClaims)
+ {
+ organizationClaims.Permissions.ManageGroups = true;
+ var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)]
+ public async Task DoesNotAuthorize_Users(CurrentContextOrganization organizationClaims)
+ {
+ var result = await new PermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
+ Assert.False(result);
+ }
+
+ [Theory, BitAutoData]
+ [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)]
+ public async Task DoesNotAuthorize_OtherCustomPermissions(CurrentContextOrganization organizationClaims)
+ {
+ organizationClaims.Permissions.ManageGroups = true;
+ organizationClaims.Permissions = organizationClaims.Permissions.Invert();
+ var result = await new TestCustomPermissionRequirement().AuthorizeAsync(organizationClaims, () => Task.FromResult(false));
+ Assert.False(result);
+ }
+
+ private class PermissionRequirement() : BasePermissionRequirement(_ => false);
+ private class TestCustomPermissionRequirement() : BasePermissionRequirement(p => p.ManageGroups);
+}
diff --git a/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs b/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs
new file mode 100644
index 0000000000..1acfbd5be3
--- /dev/null
+++ b/test/Api.Test/AdminConsole/Authorization/Requirements/PermissionRequirementsTests.cs
@@ -0,0 +1,88 @@
+using Bit.Api.AdminConsole.Authorization;
+using Bit.Api.AdminConsole.Authorization.Requirements;
+using Bit.Core.Context;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data;
+using Bit.Core.Test.AdminConsole.AutoFixture;
+using Bit.Core.Test.AdminConsole.Helpers;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Xunit;
+
+namespace Bit.Api.Test.AdminConsole.Authorization.Requirements;
+
+public class PermissionRequirementsTests
+{
+ ///
+ /// Correlates each IOrganizationRequirement with its custom permission. If you add a new requirement,
+ /// add a new entry here to have it automatically included in the tests below.
+ ///
+ public static IEnumerable