1
0
mirror of https://github.com/bitwarden/server synced 2025-12-22 19:23:45 +00:00

Add RBAC to Bitwarden Portal (#2853)

* Auth/pm-48 (#2680)

* PM-48 - add user's role as a claim and establish access control service

* PM-48 - remove function unrelated to the role claim

* PM-48 - fix whitespace issues

* PM-48 - move registration of CustomClaimsPrincipalFactory, replace role claim type string with constant, streamline code that retrieves the user's role

* Auth/pm-47 (#2699)

* PM-48 - add user's role as a claim and establish access control service

* PM-48 - remove function unrelated to the role claim

* PM-48 - fix whitespace issues

* PM-47 - add list of permission enums, role:permissions mapping, and function that determines if the logged in user has the given permission

* PM-47 - remove unneeded service registration, set role to lowercase

* PM-47 - fix code style issues

* PM-46 - create permission filter attribute (#2753)

* Auth/pm-54 add rbac for users (#2758)

* PM-54 - add permission gates to User elements

* PM-54 - fix formatting

* PM-54 - remove unused function

* PM-54 - fix variable reference, add permission to billing role

* PM-54 - handle Upgrade Premium button functionality and fix spelling

* PM-54 - change permission name to be more accurate

* PM-49 - update role retrieval (#2779)

* Auth/[PM-50] add rbac for logs (#2782)

* PM-50 - add rbac for logs

* PM-50 - remove unnecessary action filter

* PM-51 - add RBAC for tools (#2799)

* Auth/[pm-52] add rbac providers (#2818)

* PM-52 add rbac for providers

* PM-52 - update redirect action

* PM-52 - add back edit functionality and permission

* PM-52 - reverse changes around removing edit functionality

* PM-52 - moved permission check to variable assignement

* PM-53 - add rbac for organizations (#2798)

* PM-52 - add missed permission to billing role (#2836)

* Fixed merge conflicts.

* [PM-1846] Updates to add RBAC back after merge conflicts (#2870)

* Updates to add RBAC to changes from reseller.

* Added back checks for delete and initiating a trial.

* Removed extraneous Razor tag.

---------

Co-authored-by: dgoodman-bw <109169446+dgoodman-bw@users.noreply.github.com>
Co-authored-by: Danielle Goodman <dgoodman@bitwarden.com>
Co-authored-by: Jacob Fink <jfink@bitwarden.com>
This commit is contained in:
Todd Martin
2023-05-04 15:18:49 -04:00
committed by GitHub
parent 2ac513e15a
commit 0bd0910c39
24 changed files with 1101 additions and 410 deletions

View File

@@ -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";

View File

@@ -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<OrganizationsController> _logger;
private readonly IAccessControlService _accessControlService;
public OrganizationsController(
IOrganizationService organizationService,
@@ -54,7 +58,8 @@ public class OrganizationsController : Controller
IReferenceEventService referenceEventService,
IUserService userService,
IProviderRepository providerRepository,
ILogger<OrganizationsController> logger)
ILogger<OrganizationsController> 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<IActionResult> 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<IActionResult> 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<IActionResult> Delete(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
@@ -241,4 +249,57 @@ public class OrganizationsController : Controller
return Json(null);
}
private async Task<Organization> 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;
}
}

View File

@@ -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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> ResendInvite(Guid ownerId, Guid providerId)
{
await _providerService.ResendProviderSetupInviteEmailAsync(providerId, ownerId);

View File

@@ -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<IActionResult> 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<IActionResult> CreateTransaction(CreateUpdateTransactionModel model)
{
if (!ModelState.IsValid)
@@ -142,6 +148,7 @@ public class ToolsController : Controller
}
}
[RequirePermission(Permission.Tools_CreateEditTransaction)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GenerateLicense(LicenseModel model)
{
if (!ModelState.IsValid)
@@ -285,6 +297,7 @@ public class ToolsController : Controller
}
}
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> 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<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
{
if (string.IsNullOrWhiteSpace(stripeTaxRateId))
@@ -328,6 +342,7 @@ public class ToolsController : Controller
}
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_ManageTaxRates)]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
{
options = options ?? new StripeSubscriptionListOptions();
@@ -465,6 +483,7 @@ public class ToolsController : Controller
}
[HttpPost]
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
public async Task<IActionResult> StripeSubscriptions([FromForm] StripeSubscriptionsModel model)
{
if (!ModelState.IsValid)

View File

@@ -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<IActionResult> 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<IActionResult> Delete(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);