diff --git a/src/Admin/Controllers/LogsController.cs b/src/Admin/Controllers/LogsController.cs index 449c8cc860..d3934cb8e5 100644 --- a/src/Admin/Controllers/LogsController.cs +++ b/src/Admin/Controllers/LogsController.cs @@ -1,4 +1,5 @@ using Bit.Admin.Models; +using Bit.Admin.Utilities; using Bit.Core.Settings; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -11,6 +12,7 @@ namespace Bit.Admin.Controllers; [Authorize] [SelfHosted(NotSelfHostedOnly = true)] +[RequirePermission(Enums.Permission.Logs_View)] public class LogsController : Controller { private const string Database = "Diagnostics"; diff --git a/src/Admin/Controllers/OrganizationsController.cs b/src/Admin/Controllers/OrganizationsController.cs index 21f616267a..d95e883418 100644 --- a/src/Admin/Controllers/OrganizationsController.cs +++ b/src/Admin/Controllers/OrganizationsController.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Models; +using Bit.Admin.Enums; +using Bit.Admin.Models; +using Bit.Admin.Services; +using Bit.Admin.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; @@ -36,6 +39,7 @@ public class OrganizationsController : Controller private readonly IUserService _userService; private readonly IProviderRepository _providerRepository; private readonly ILogger _logger; + private readonly IAccessControlService _accessControlService; public OrganizationsController( IOrganizationService organizationService, @@ -54,7 +58,8 @@ public class OrganizationsController : Controller IReferenceEventService referenceEventService, IUserService userService, IProviderRepository providerRepository, - ILogger logger) + ILogger logger, + IAccessControlService accessControlService) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -73,8 +78,10 @@ public class OrganizationsController : Controller _userService = userService; _providerRepository = providerRepository; _logger = logger; + _accessControlService = accessControlService; } + [RequirePermission(Permission.Org_List_View)] public async Task Index(string name = null, string userEmail = null, bool? paid = null, int page = 1, int count = 25) { @@ -163,8 +170,8 @@ public class OrganizationsController : Controller [SelfHosted(NotSelfHostedOnly = true)] public async Task Edit(Guid id, OrganizationEditModel model) { - var organization = await _organizationRepository.GetByIdAsync(id); - model.ToOrganization(organization); + var organization = await GetOrganization(id, model); + await _organizationRepository.ReplaceAsync(organization); await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.OrganizationEditedByAdmin, organization) @@ -177,6 +184,7 @@ public class OrganizationsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Org_Delete)] public async Task Delete(Guid id) { var organization = await _organizationRepository.GetByIdAsync(id); @@ -241,4 +249,57 @@ public class OrganizationsController : Controller return Json(null); } + private async Task GetOrganization(Guid id, OrganizationEditModel model) + { + var organization = await _organizationRepository.GetByIdAsync(id); + + if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox)) + { + organization.Enabled = model.Enabled; + } + + if (_accessControlService.UserHasPermission(Permission.Org_Plan_Edit)) + { + organization.PlanType = model.PlanType.Value; + organization.Plan = model.Plan; + organization.Seats = model.Seats; + organization.MaxAutoscaleSeats = model.MaxAutoscaleSeats; + organization.MaxCollections = model.MaxCollections; + organization.MaxStorageGb = model.MaxStorageGb; + + //features + organization.SelfHost = model.SelfHost; + organization.Use2fa = model.Use2fa; + organization.UseApi = model.UseApi; + organization.UseGroups = model.UseGroups; + organization.UsePolicies = model.UsePolicies; + organization.UseSso = model.UseSso; + organization.UseKeyConnector = model.UseKeyConnector; + organization.UseScim = model.UseScim; + organization.UseDirectory = model.UseDirectory; + organization.UseEvents = model.UseEvents; + organization.UseResetPassword = model.UseResetPassword; + organization.UseCustomPermissions = model.UseCustomPermissions; + organization.UseTotp = model.UseTotp; + organization.UsersGetPremium = model.UsersGetPremium; + organization.UseSecretsManager = model.UseSecretsManager; + } + + if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit)) + { + organization.LicenseKey = model.LicenseKey; + organization.ExpirationDate = model.ExpirationDate; + } + + if (_accessControlService.UserHasPermission(Permission.Org_Billing_Edit)) + { + organization.BillingEmail = model.BillingEmail?.ToLowerInvariant()?.Trim(); + organization.Gateway = model.Gateway; + organization.GatewayCustomerId = model.GatewayCustomerId; + organization.GatewaySubscriptionId = model.GatewaySubscriptionId; + } + + return organization; + } + } diff --git a/src/Admin/Controllers/ProvidersController.cs b/src/Admin/Controllers/ProvidersController.cs index 3b16f2139d..3dbf1ca53b 100644 --- a/src/Admin/Controllers/ProvidersController.cs +++ b/src/Admin/Controllers/ProvidersController.cs @@ -1,4 +1,6 @@ -using Bit.Admin.Models; +using Bit.Admin.Enums; +using Bit.Admin.Models; +using Bit.Admin.Utilities; using Bit.Core.Entities.Provider; using Bit.Core.Enums.Provider; using Bit.Core.Providers.Interfaces; @@ -54,6 +56,7 @@ public class ProvidersController : Controller _createProviderCommand = createProviderCommand; } + [RequirePermission(Permission.Provider_List_View)] public async Task Index(string name = null, string userEmail = null, int page = 1, int count = 25) { if (page < 1) @@ -90,6 +93,7 @@ public class ProvidersController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Provider_Create)] public async Task Create(CreateProviderModel model) { if (!ModelState.IsValid) @@ -111,6 +115,7 @@ public class ProvidersController : Controller return RedirectToAction("Edit", new { id = provider.Id }); } + [RequirePermission(Permission.Provider_View)] public async Task View(Guid id) { var provider = await _providerRepository.GetByIdAsync(id); @@ -141,6 +146,7 @@ public class ProvidersController : Controller [HttpPost] [ValidateAntiForgeryToken] [SelfHosted(NotSelfHostedOnly = true)] + [RequirePermission(Permission.Provider_Edit)] public async Task Edit(Guid id, ProviderEditModel model) { var provider = await _providerRepository.GetByIdAsync(id); @@ -155,6 +161,7 @@ public class ProvidersController : Controller return RedirectToAction("Edit", new { id }); } + [RequirePermission(Permission.Provider_ResendEmailInvite)] public async Task ResendInvite(Guid ownerId, Guid providerId) { await _providerService.ResendProviderSetupInviteEmailAsync(providerId, ownerId); diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index 6f9c557341..8cfbe0a3a7 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -1,6 +1,8 @@ using System.Text; using System.Text.Json; +using Bit.Admin.Enums; using Bit.Admin.Models; +using Bit.Admin.Utilities; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; @@ -55,6 +57,7 @@ public class ToolsController : Controller _environment = environment; } + [RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)] public IActionResult ChargeBraintree() { return View(new ChargeBraintreeModel()); @@ -62,6 +65,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)] public async Task ChargeBraintree(ChargeBraintreeModel model) { if (!ModelState.IsValid) @@ -113,6 +117,7 @@ public class ToolsController : Controller return View(model); } + [RequirePermission(Permission.Tools_CreateEditTransaction)] public IActionResult CreateTransaction(Guid? organizationId = null, Guid? userId = null) { return View("CreateUpdateTransaction", new CreateUpdateTransactionModel @@ -124,6 +129,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_CreateEditTransaction)] public async Task CreateTransaction(CreateUpdateTransactionModel model) { if (!ModelState.IsValid) @@ -142,6 +148,7 @@ public class ToolsController : Controller } } + [RequirePermission(Permission.Tools_CreateEditTransaction)] public async Task EditTransaction(Guid id) { var transaction = await _transactionRepository.GetByIdAsync(id); @@ -154,6 +161,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_CreateEditTransaction)] public async Task EditTransaction(Guid id, CreateUpdateTransactionModel model) { if (!ModelState.IsValid) @@ -171,6 +179,7 @@ public class ToolsController : Controller } } + [RequirePermission(Permission.Tools_PromoteAdmin)] public IActionResult PromoteAdmin() { return View(); @@ -178,6 +187,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_PromoteAdmin)] public async Task PromoteAdmin(PromoteAdminModel model) { if (!ModelState.IsValid) @@ -207,6 +217,7 @@ public class ToolsController : Controller return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value }); } + [RequirePermission(Permission.Tools_GenerateLicenseFile)] public IActionResult GenerateLicense() { return View(); @@ -214,6 +225,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_GenerateLicenseFile)] public async Task GenerateLicense(LicenseModel model) { if (!ModelState.IsValid) @@ -285,6 +297,7 @@ public class ToolsController : Controller } } + [RequirePermission(Permission.Tools_ManageTaxRates)] public async Task TaxRate(int page = 1, int count = 25) { if (page < 1) @@ -307,6 +320,7 @@ public class ToolsController : Controller }); } + [RequirePermission(Permission.Tools_ManageTaxRates)] public async Task TaxRateAddEdit(string stripeTaxRateId = null) { if (string.IsNullOrWhiteSpace(stripeTaxRateId)) @@ -328,6 +342,7 @@ public class ToolsController : Controller } [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_ManageTaxRates)] public async Task TaxRateUpload(IFormFile file) { if (file == null || file.Length == 0) @@ -395,6 +410,7 @@ public class ToolsController : Controller [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.Tools_ManageTaxRates)] public async Task TaxRateAddEdit(TaxRateAddEditModel model) { var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode }); @@ -429,6 +445,7 @@ public class ToolsController : Controller return RedirectToAction("TaxRate"); } + [RequirePermission(Permission.Tools_ManageTaxRates)] public async Task TaxRateArchive(string stripeTaxRateId) { if (!string.IsNullOrWhiteSpace(stripeTaxRateId)) @@ -439,6 +456,7 @@ public class ToolsController : Controller return RedirectToAction("TaxRate"); } + [RequirePermission(Permission.Tools_ManageStripeSubscriptions)] public async Task StripeSubscriptions(StripeSubscriptionListOptions options) { options = options ?? new StripeSubscriptionListOptions(); @@ -465,6 +483,7 @@ public class ToolsController : Controller } [HttpPost] + [RequirePermission(Permission.Tools_ManageStripeSubscriptions)] public async Task StripeSubscriptions([FromForm] StripeSubscriptionsModel model) { if (!ModelState.IsValid) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 68cf70a35b..30565ca3d9 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -1,4 +1,7 @@ -using Bit.Admin.Models; +using Bit.Admin.Enums; +using Bit.Admin.Models; +using Bit.Admin.Services; +using Bit.Admin.Utilities; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; @@ -17,19 +20,23 @@ public class UsersController : Controller private readonly ICipherRepository _cipherRepository; private readonly IPaymentService _paymentService; private readonly GlobalSettings _globalSettings; + private readonly IAccessControlService _accessControlService; public UsersController( IUserRepository userRepository, ICipherRepository cipherRepository, IPaymentService paymentService, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IAccessControlService accessControlService) { _userRepository = userRepository; _cipherRepository = cipherRepository; _paymentService = paymentService; _globalSettings = globalSettings; + _accessControlService = accessControlService; } + [RequirePermission(Permission.User_List_View)] public async Task Index(string email, int page = 1, int count = 25) { if (page < 1) @@ -91,13 +98,36 @@ public class UsersController : Controller return RedirectToAction("Index"); } - model.ToUser(user); + var canUpgradePremium = _accessControlService.UserHasPermission(Permission.User_UpgradePremium); + + if (_accessControlService.UserHasPermission(Permission.User_Premium_Edit) || + canUpgradePremium) + { + user.MaxStorageGb = model.MaxStorageGb; + user.Premium = model.Premium; + } + + if (_accessControlService.UserHasPermission(Permission.User_Billing_Edit)) + { + user.Gateway = model.Gateway; + user.GatewayCustomerId = model.GatewayCustomerId; + user.GatewaySubscriptionId = model.GatewaySubscriptionId; + } + + if (_accessControlService.UserHasPermission(Permission.User_Licensing_Edit) || + canUpgradePremium) + { + user.LicenseKey = model.LicenseKey; + user.PremiumExpirationDate = model.PremiumExpirationDate; + } + await _userRepository.ReplaceAsync(user); return RedirectToAction("Edit", new { id }); } [HttpPost] [ValidateAntiForgeryToken] + [RequirePermission(Permission.User_Delete)] public async Task Delete(Guid id) { var user = await _userRepository.GetByIdAsync(id); diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs new file mode 100644 index 0000000000..2354e9435c --- /dev/null +++ b/src/Admin/Enums/Permissions.cs @@ -0,0 +1,53 @@ +namespace Bit.Admin.Enums; + +public enum Permission +{ + User_List_View, + User_UserInformation_View, + User_GeneralDetails_View, + User_Delete, + User_UpgradePremium, + User_BillingInformation_View, + User_BillingInformation_DownloadInvoice, + User_BillingInformation_CreateEditTransaction, + User_Premium_View, + User_Premium_Edit, + User_Licensing_View, + User_Licensing_Edit, + User_Billing_View, + User_Billing_Edit, + User_Billing_LaunchGateway, + + Org_List_View, + Org_OrgInformation_View, + Org_GeneralDetails_View, + Org_CheckEnabledBox, + Org_BusinessInformation_View, + Org_InitiateTrial, + Org_Delete, + Org_BillingInformation_View, + Org_BillingInformation_DownloadInvoice, + Org_BillingInformation_CreateEditTransaction, + Org_Plan_View, + Org_Plan_Edit, + Org_Licensing_View, + Org_Licensing_Edit, + Org_Billing_View, + Org_Billing_Edit, + Org_Billing_LaunchGateway, + + Provider_List_View, + Provider_Create, + Provider_Edit, + Provider_View, + Provider_ResendEmailInvite, + + Tools_ChargeBrainTreeCustomer, + Tools_PromoteAdmin, + Tools_GenerateLicenseFile, + Tools_ManageTaxRates, + Tools_ManageStripeSubscriptions, + Tools_CreateEditTransaction, + + Logs_View +} diff --git a/src/Admin/IdentityServer/CustomClaimsPrincipalFactory.cs b/src/Admin/IdentityServer/CustomClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..0e198a31be --- /dev/null +++ b/src/Admin/IdentityServer/CustomClaimsPrincipalFactory.cs @@ -0,0 +1,42 @@ +using System.Security.Claims; +using Bit.Admin.Services; +using Bit.Core.Settings; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; + +public class CustomClaimsPrincipalFactory : UserClaimsPrincipalFactory +{ + private IAccessControlService _accessControlService; + private readonly IGlobalSettings _globalSettings; + + public CustomClaimsPrincipalFactory( + UserManager userManager, + IOptions optionsAccessor, + IAccessControlService accessControlService, + IGlobalSettings globalSettings) + : base(userManager, optionsAccessor) + { + _accessControlService = accessControlService; + _globalSettings = globalSettings; + } + + public async override Task CreateAsync(IdentityUser user) + { + var principal = await base.CreateAsync(user); + + if (!_globalSettings.SelfHosted && + !string.IsNullOrEmpty(user.Email) && + principal.Identity != null) + { + var role = _accessControlService.GetUserRole(user.Email); + + if (!string.IsNullOrEmpty(role)) + { + ((ClaimsIdentity)principal.Identity).AddClaims( + new[] { new Claim(ClaimTypes.Role, role) }); + } + } + + return principal; + } +} diff --git a/src/Admin/IdentityServer/ServiceCollectionExtensions.cs b/src/Admin/IdentityServer/ServiceCollectionExtensions.cs index e8d20940d0..b8fe187c72 100644 --- a/src/Admin/IdentityServer/ServiceCollectionExtensions.cs +++ b/src/Admin/IdentityServer/ServiceCollectionExtensions.cs @@ -21,7 +21,8 @@ public static class ServiceCollectionExtensions var passwordlessIdentityBuilder = services.AddIdentity() .AddUserStore() .AddRoleStore() - .AddDefaultTokenProviders(); + .AddDefaultTokenProviders() + .AddClaimsPrincipalFactory(); var regularIdentityBuilder = services.AddIdentityCore() .AddUserStore(); diff --git a/src/Admin/Models/BillingInformationModel.cs b/src/Admin/Models/BillingInformationModel.cs index a90ec7955d..7445f01314 100644 --- a/src/Admin/Models/BillingInformationModel.cs +++ b/src/Admin/Models/BillingInformationModel.cs @@ -7,4 +7,5 @@ public class BillingInformationModel public BillingInfo BillingInfo { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public string Entity { get; set; } } diff --git a/src/Admin/Models/ProviderEditModel.cs b/src/Admin/Models/ProviderEditModel.cs index 2052bf6cfd..38fdb742a8 100644 --- a/src/Admin/Models/ProviderEditModel.cs +++ b/src/Admin/Models/ProviderEditModel.cs @@ -28,8 +28,6 @@ public class ProviderEditModel : ProviderViewModel public Provider ToProvider(Provider existingProvider) { - existingProvider.Name = Name; - existingProvider.BusinessName = BusinessName; existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim(); existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim(); return existingProvider; diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index d438e3000a..4252cd5cb4 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -57,18 +57,4 @@ public class UserEditModel : UserViewModel [Display(Name = "Premium Expiration Date")] public DateTime? PremiumExpirationDate { get; set; } - public User ToUser(User existingUser) - { - existingUser.Name = Name; - existingUser.Email = Email; - existingUser.EmailVerified = EmailVerified; - existingUser.Premium = Premium; - existingUser.MaxStorageGb = MaxStorageGb; - existingUser.Gateway = Gateway; - existingUser.GatewayCustomerId = GatewayCustomerId; - existingUser.GatewaySubscriptionId = GatewaySubscriptionId; - existingUser.LicenseKey = LicenseKey; - existingUser.PremiumExpirationDate = PremiumExpirationDate; - return existingUser; - } } diff --git a/src/Admin/Services/AccessControlService.cs b/src/Admin/Services/AccessControlService.cs new file mode 100644 index 0000000000..f45f30e216 --- /dev/null +++ b/src/Admin/Services/AccessControlService.cs @@ -0,0 +1,66 @@ +using System.Security.Claims; +using Bit.Admin.Enums; +using Bit.Admin.Utilities; +using Bit.Core.Settings; + +namespace Bit.Admin.Services; + +public class AccessControlService : IAccessControlService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IConfiguration _configuration; + private readonly IGlobalSettings _globalSettings; + + public AccessControlService( + IHttpContextAccessor httpContextAccessor, + IConfiguration configuration, + IGlobalSettings globalSettings) + { + _httpContextAccessor = httpContextAccessor; + _configuration = configuration; + _globalSettings = globalSettings; + } + + public bool UserHasPermission(Permission permission) + { + if (_globalSettings.SelfHosted) + { + return true; + } + + var userRole = GetUserRoleFromClaim(); + if (string.IsNullOrEmpty(userRole) || !RolePermissionMapping.RolePermissions.ContainsKey(userRole)) + { + return false; + } + + return RolePermissionMapping.RolePermissions[userRole].Contains(permission); + } + + public string GetUserRole(string userEmail) + { + var roles = _configuration.GetSection("adminSettings:role").GetChildren(); + + if (roles == null || !roles.Any()) + { + return null; + } + + userEmail = userEmail.ToLowerInvariant(); + + var userRole = roles.FirstOrDefault(s => (s.Value != null ? s.Value.ToLowerInvariant().Split(',').Contains(userEmail) : false)); + + if (userRole == null) + { + return null; + } + + return userRole.Key.ToLowerInvariant(); + } + + private string GetUserRoleFromClaim() + { + return _httpContextAccessor.HttpContext?.User?.Claims? + .FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; + } +} diff --git a/src/Admin/Services/IAccessControlService.cs b/src/Admin/Services/IAccessControlService.cs new file mode 100644 index 0000000000..a7d68fb297 --- /dev/null +++ b/src/Admin/Services/IAccessControlService.cs @@ -0,0 +1,9 @@ +using Bit.Admin.Enums; + +namespace Bit.Admin.Services; + +public interface IAccessControlService +{ + public bool UserHasPermission(Permission permission); + public string GetUserRole(string userEmail); +} diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 37d8445d25..9482be011a 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -7,6 +7,8 @@ using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Identity; using Stripe; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Bit.Admin.Services; #if !OSS using Bit.Commercial.Core.Utilities; @@ -64,6 +66,7 @@ public class Startup // Context services.AddScoped(); + services.TryAddSingleton(); // Identity services.AddPasswordlessIdentityServices(globalSettings); @@ -82,6 +85,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddScoped(); #if OSS services.AddOosServices(); diff --git a/src/Admin/Utilities/RequirePermissionAttribute.cs b/src/Admin/Utilities/RequirePermissionAttribute.cs new file mode 100644 index 0000000000..94c849a6d2 --- /dev/null +++ b/src/Admin/Utilities/RequirePermissionAttribute.cs @@ -0,0 +1,26 @@ +using Bit.Admin.Enums; +using Bit.Admin.Services; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Bit.Admin.Utilities; + +public class RequirePermissionAttribute : ActionFilterAttribute +{ + public Permission Permission { get; set; } + + public RequirePermissionAttribute(Permission permission) + { + Permission = permission; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + var accessControlService = context.HttpContext.RequestServices.GetRequiredService(); + + var hasPermission = accessControlService.UserHasPermission(Permission); + if (!hasPermission) + { + throw new UnauthorizedAccessException("Not authorized."); + } + } +} diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs new file mode 100644 index 0000000000..e934d4e59f --- /dev/null +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -0,0 +1,194 @@ +using Bit.Admin.Enums; + +namespace Bit.Admin.Utilities; + +public static class RolePermissionMapping +{ + //This is temporary and will be moved to the db in the next round of the rbac implementation + public static readonly Dictionary> RolePermissions = new Dictionary>() + { + { "owner", new List + { + Permission.User_List_View, + Permission.User_UserInformation_View, + Permission.User_GeneralDetails_View, + Permission.Org_CheckEnabledBox, + Permission.User_Delete, + Permission.User_UpgradePremium, + Permission.User_BillingInformation_View, + Permission.User_BillingInformation_DownloadInvoice, + Permission.User_Premium_View, + Permission.User_Premium_Edit, + Permission.User_Licensing_View, + Permission.User_Licensing_Edit, + Permission.User_Billing_View, + Permission.User_Billing_Edit, + Permission.User_Billing_LaunchGateway, + Permission.Org_List_View, + Permission.Org_OrgInformation_View, + Permission.Org_GeneralDetails_View, + Permission.Org_BusinessInformation_View, + Permission.Org_InitiateTrial, + Permission.Org_Delete, + Permission.Org_BillingInformation_View, + Permission.Org_BillingInformation_DownloadInvoice, + Permission.Org_Plan_View, + Permission.Org_Plan_Edit, + Permission.Org_Licensing_View, + Permission.Org_Licensing_Edit, + Permission.Org_Billing_View, + Permission.Org_Billing_Edit, + Permission.Org_Billing_LaunchGateway, + Permission.Provider_List_View, + Permission.Provider_Create, + Permission.Provider_View, + Permission.Provider_ResendEmailInvite, + Permission.Tools_ChargeBrainTreeCustomer, + Permission.Tools_PromoteAdmin, + Permission.Tools_GenerateLicenseFile, + Permission.Tools_ManageTaxRates, + Permission.Tools_ManageStripeSubscriptions, + Permission.Logs_View + } + }, + { "admin", new List + { + Permission.User_List_View, + Permission.User_UserInformation_View, + Permission.User_GeneralDetails_View, + Permission.Org_CheckEnabledBox, + Permission.User_Delete, + Permission.User_UpgradePremium, + Permission.User_BillingInformation_View, + Permission.User_BillingInformation_DownloadInvoice, + Permission.User_Premium_View, + Permission.User_Premium_Edit, + Permission.User_Licensing_View, + Permission.User_Licensing_Edit, + Permission.User_Billing_View, + Permission.User_Billing_Edit, + Permission.User_Billing_LaunchGateway, + Permission.Org_List_View, + Permission.Org_OrgInformation_View, + Permission.Org_GeneralDetails_View, + Permission.Org_BusinessInformation_View, + Permission.Org_Delete, + Permission.Org_BillingInformation_View, + Permission.Org_BillingInformation_DownloadInvoice, + Permission.Org_Plan_View, + Permission.Org_Plan_Edit, + Permission.Org_Licensing_View, + Permission.Org_Licensing_Edit, + Permission.Org_Billing_View, + Permission.Org_Billing_Edit, + Permission.Org_Billing_LaunchGateway, + Permission.Org_InitiateTrial, + Permission.Provider_List_View, + Permission.Provider_Create, + Permission.Provider_View, + Permission.Provider_ResendEmailInvite, + Permission.Tools_ChargeBrainTreeCustomer, + Permission.Tools_PromoteAdmin, + Permission.Tools_GenerateLicenseFile, + Permission.Tools_ManageTaxRates, + Permission.Tools_ManageStripeSubscriptions, + Permission.Logs_View + } + }, + { "cs", new List + { + Permission.User_List_View, + Permission.User_UserInformation_View, + Permission.User_GeneralDetails_View, + Permission.User_UpgradePremium, + Permission.User_BillingInformation_View, + Permission.User_BillingInformation_DownloadInvoice, + Permission.User_Premium_View, + Permission.User_Licensing_View, + Permission.User_Billing_View, + Permission.User_Billing_LaunchGateway, + Permission.Org_List_View, + Permission.Org_OrgInformation_View, + Permission.Org_GeneralDetails_View, + Permission.Org_BusinessInformation_View, + Permission.Org_BillingInformation_View, + Permission.Org_BillingInformation_DownloadInvoice, + Permission.Org_Plan_View, + Permission.Org_Licensing_View, + Permission.Org_Billing_View, + Permission.Org_Billing_LaunchGateway, + Permission.Provider_List_View, + Permission.Provider_View, + Permission.Logs_View + } + }, + { "billing", new List + { + Permission.User_List_View, + Permission.User_UserInformation_View, + Permission.User_GeneralDetails_View, + Permission.User_UpgradePremium, + Permission.User_BillingInformation_View, + Permission.User_BillingInformation_DownloadInvoice, + Permission.User_BillingInformation_CreateEditTransaction, + Permission.User_Premium_View, + Permission.User_Licensing_View, + Permission.User_Billing_View, + Permission.User_Billing_Edit, + Permission.User_Billing_LaunchGateway, + Permission.Org_List_View, + Permission.Org_OrgInformation_View, + Permission.Org_GeneralDetails_View, + Permission.Org_BusinessInformation_View, + Permission.Org_BillingInformation_View, + Permission.Org_BillingInformation_DownloadInvoice, + Permission.Org_BillingInformation_CreateEditTransaction, + Permission.Org_Plan_View, + Permission.Org_Plan_Edit, + Permission.Org_Licensing_View, + Permission.Org_Billing_View, + Permission.Org_Billing_Edit, + Permission.Org_Billing_LaunchGateway, + Permission.Provider_Edit, + Permission.Provider_View, + Permission.Provider_List_View, + Permission.Tools_ChargeBrainTreeCustomer, + Permission.Tools_GenerateLicenseFile, + Permission.Tools_ManageTaxRates, + Permission.Tools_ManageStripeSubscriptions, + Permission.Tools_CreateEditTransaction, + Permission.Logs_View + } + }, + { "sales", new List + { + Permission.User_List_View, + Permission.User_UserInformation_View, + Permission.User_GeneralDetails_View, + Permission.Org_CheckEnabledBox, + Permission.User_BillingInformation_View, + Permission.User_BillingInformation_DownloadInvoice, + Permission.User_Premium_View, + Permission.User_Licensing_View, + Permission.User_Licensing_Edit, + Permission.Org_List_View, + Permission.Org_OrgInformation_View, + Permission.Org_GeneralDetails_View, + Permission.Org_BusinessInformation_View, + Permission.Org_InitiateTrial, + Permission.Org_BillingInformation_View, + Permission.Org_BillingInformation_DownloadInvoice, + Permission.Org_Plan_View, + Permission.Org_Plan_Edit, + Permission.Org_Licensing_View, + Permission.Org_Licensing_Edit, + Permission.Provider_List_View, + Permission.Provider_Create, + Permission.Provider_Edit, + Permission.Provider_View, + Permission.Provider_ResendEmailInvite, + Permission.Logs_View + } + }, + }; +} diff --git a/src/Admin/Views/Organizations/Edit.cshtml b/src/Admin/Views/Organizations/Edit.cshtml index d959f04a01..2d5aa1aed5 100644 --- a/src/Admin/Views/Organizations/Edit.cshtml +++ b/src/Admin/Views/Organizations/Edit.cshtml @@ -1,6 +1,13 @@ -@model OrganizationEditModel +@using Bit.Admin.Enums; +@inject Bit.Admin.Services.IAccessControlService AccessControlService +@model OrganizationEditModel @{ ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name; + + var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View); + var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View); + var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial); + var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete); } @section Scripts { @@ -79,24 +86,41 @@

Provider Relationship

@await Html.PartialAsync("_ProviderInformation", Model.Provider) } -

Organization Information

-@await Html.PartialAsync("_ViewInformation", Model) -

Billing Information

-@await Html.PartialAsync("_BillingInformation", - new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id }) + +@if (canViewOrganizationInformation) +{ +

Organization Information

+ @await Html.PartialAsync("_ViewInformation", Model) +} + +@if (canViewBillingInformation) +{ +

Billing Information

+ @await Html.PartialAsync("_BillingInformation", + new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" }) +} + @await Html.PartialAsync("_OrganizationForm", Model) +
- - -
- -
+ @if (canInitiateTrial) + { + + + } + @if (canDelete) + { +
+ +
+ }
+ diff --git a/src/Admin/Views/Providers/Admins.cshtml b/src/Admin/Views/Providers/Admins.cshtml index 397f6fa309..533cb3034e 100644 --- a/src/Admin/Views/Providers/Admins.cshtml +++ b/src/Admin/Views/Providers/Admins.cshtml @@ -1,4 +1,11 @@ +@using Bit.Admin.Enums; +@inject Bit.Admin.Services.IAccessControlService AccessControlService @model ProviderViewModel + +@{ + var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite); +} +

Provider Admins

@@ -31,7 +38,8 @@ @if(admin.Status.Equals(ProviderUserStatusType.Confirmed) - && @Model.Provider.Status.Equals(ProviderStatusType.Pending)) + && @Model.Provider.Status.Equals(ProviderStatusType.Pending) + && canResendEmailInvite) { @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString()) { diff --git a/src/Admin/Views/Providers/Edit.cshtml b/src/Admin/Views/Providers/Edit.cshtml index cdf8c2eccb..ab32b7cad2 100644 --- a/src/Admin/Views/Providers/Edit.cshtml +++ b/src/Admin/Views/Providers/Edit.cshtml @@ -1,6 +1,11 @@ -@model ProviderEditModel +@using Bit.Admin.Enums; +@inject Bit.Admin.Services.IAccessControlService AccessControlService + +@model ProviderEditModel @{ ViewData["Title"] = "Provider: " + Model.Provider.Name; + + var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit); }

Provider @Model.Provider.Name

@@ -10,29 +15,21 @@ @await Html.PartialAsync("Admins", Model)

General

-
-
-
- - -
-
-
+
+
Name
+
@Model.Provider.Name
+

Business Information

-
-
-
- - -
-
-
+
+
Business Name
+
@Model.Provider.BusinessName
+

Billing

- +
@@ -46,6 +43,10 @@
@await Html.PartialAsync("Organizations", Model) -
- -
+@if (canEdit) +{ +
+ +
+} + diff --git a/src/Admin/Views/Providers/Index.cshtml b/src/Admin/Views/Providers/Index.cshtml index f7fedb9232..522d06f31e 100644 --- a/src/Admin/Views/Providers/Index.cshtml +++ b/src/Admin/Views/Providers/Index.cshtml @@ -1,7 +1,12 @@ -@using Bit.SharedWeb.Utilities +@using Bit.SharedWeb.Utilities +@using Bit.Admin.Enums; +@inject Bit.Admin.Services.IAccessControlService AccessControlService + @model ProvidersModel @{ ViewData["Title"] = "Providers"; + + var canCreateProvider = AccessControlService.UserHasPermission(Permission.Provider_Create); }

Providers

@@ -16,9 +21,12 @@
- + @if (canCreateProvider) + { + + }
diff --git a/src/Admin/Views/Shared/_BillingInformation.cshtml b/src/Admin/Views/Shared/_BillingInformation.cshtml index e01fe2fc0e..dab0587ef8 100644 --- a/src/Admin/Views/Shared/_BillingInformation.cshtml +++ b/src/Admin/Views/Shared/_BillingInformation.cshtml @@ -1,4 +1,15 @@ -@model BillingInformationModel +@using Bit.Admin.Enums; +@inject Bit.Admin.Services.IAccessControlService AccessControlService +@model BillingInformationModel + +@{ + var canManageTransactions = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_CreateEditTransaction) + : AccessControlService.UserHasPermission(Permission.Org_BillingInformation_CreateEditTransaction); + + var canDownloadInvoice = Model.Entity == "User" ? AccessControlService.UserHasPermission(Permission.User_BillingInformation_DownloadInvoice) + : AccessControlService.UserHasPermission(Permission.Org_BillingInformation_DownloadInvoice); +} +
Account @(Model.BillingInfo.Balance <= 0 ? "Credit" : "Balance")
@Math.Abs(Model.BillingInfo.Balance).ToString("C")
@@ -16,11 +27,14 @@ @invoice.Number @invoice.Amount.ToString("C") @(invoice.Paid ? "Paid" : "Unpaid") - - - - - + @if (canDownloadInvoice) + { + + + + + + } } @@ -46,10 +60,13 @@ @transaction.PaymentMethodType.ToString() @transaction.Details @transaction.Amount.ToString("C") - - - + @if (canManageTransactions) + { + + + + } } @@ -59,9 +76,12 @@ {

No transactions.

} - - New Transaction - + @if (canManageTransactions) + { + + New Transaction + + }
diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 3954091335..55a380da07 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -1,5 +1,25 @@ -@inject SignInManager SignInManager +@using Bit.Admin.Enums; + +@inject SignInManager SignInManager @inject Bit.Core.Settings.GlobalSettings GlobalSettings +@inject Bit.Admin.Services.IAccessControlService AccessControlService + +@{ + var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View); + var canViewOrgs = AccessControlService.UserHasPermission(Permission.Org_List_View); + var canViewProviders = AccessControlService.UserHasPermission(Permission.Provider_List_View); + var canViewLogs = AccessControlService.UserHasPermission(Permission.Logs_View); + var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer); + var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction); + var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin); + var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); + var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates); + var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); + + var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || + canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; +} + @@ -32,58 +52,91 @@