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 RequirementData => new List + { + new object[] { new AccessEventLogsRequirement(), nameof(Permissions.AccessEventLogs) }, + new object[] { new AccessImportExportRequirement(), nameof(Permissions.AccessImportExport) }, + new object[] { new AccessReportsRequirement(), nameof(Permissions.AccessReports) }, + new object[] { new ManageAccountRecoveryRequirement(), nameof(Permissions.ManageResetPassword) }, + new object[] { new ManageGroupsRequirement(), nameof(Permissions.ManageGroups) }, + new object[] { new ManagePoliciesRequirement(), nameof(Permissions.ManagePolicies) }, + new object[] { new ManageScimRequirement(), nameof(Permissions.ManageScim) }, + new object[] { new ManageSsoRequirement(), nameof(Permissions.ManageSso) }, + new object[] { new ManageUsersRequirement(), nameof(Permissions.ManageUsers) }, + }; + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task Authorizes_Provider(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(true)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Owner)] + public async Task Authorizes_Owner(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin)] + public async Task Authorizes_Admin(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task Authorizes_Custom_With_Correct_Permission(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization) + { + organization.Permissions.SetPermission(permissionName, true); + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.True(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.Custom)] + public async Task DoesNotAuthorize_Custom_With_Other_Permissions(IOrganizationRequirement requirement, string permissionName, CurrentContextOrganization organization) + { + organization.Permissions.SetPermission(permissionName, true); + organization.Permissions = organization.Permissions.Invert(); + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.False(result); + } + + [Theory] + [BitMemberAutoData(nameof(RequirementData))] + [CurrentContextOrganizationCustomize(Type = OrganizationUserType.User)] + public async Task DoesNotAuthorize_User(IOrganizationRequirement requirement, string _, CurrentContextOrganization organization) + { + var result = await requirement.AuthorizeAsync(organization, () => Task.FromResult(false)); + Assert.False(result); + } +} diff --git a/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs index f346c47624..17045e29f0 100644 --- a/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs +++ b/test/Core.Test/AdminConsole/Helpers/PermissionsHelpers.cs @@ -6,6 +6,23 @@ namespace Bit.Core.Test.AdminConsole.Helpers; public static class PermissionsHelpers { + /// + /// Sets the specified permission. + /// + /// The permission name specified as a string - using `nameof` is highly recommended. + /// The value to set the permission to. + /// No value; this mutates the permissions object. + public static void SetPermission(this Permissions permissions, string permissionName, bool value) + { + var prop = typeof(Permissions).GetProperty(permissionName); + if (prop == null) + { + throw new NullReferenceException("Invalid property name."); + } + + prop.SetValue(permissions, true); + } + /// /// Return a new Permission object with inverted permissions. /// This is useful to test negative cases, e.g. "all other permissions should fail".