1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

Implemented Custom role and permissions (#1057)

* Implemented Custom role and permissions

* Converted permissions columns to a json blob

* Code review fixes for Permissions

* sql build fix

* Update Permissions.cs

* formatting

* Update IOrganizationService.cs

* reworked a conditional

* built out tests for relevant organization service methods

* removed unused usings

* fixed a broken test and a bad empty string init

* removed 'Attribute' from some attribute instances
This commit is contained in:
Addison Beck 2021-01-12 11:02:39 -05:00 committed by GitHub
parent 99b95b5330
commit 63fcdc1418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1116 additions and 149 deletions

View File

@ -45,7 +45,7 @@ namespace Bit.Portal.Controllers
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
!_enterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
return Redirect("~/");
}
@ -65,7 +65,7 @@ namespace Bit.Portal.Controllers
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
!_enterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
return Redirect("~/");
}
@ -85,7 +85,7 @@ namespace Bit.Portal.Controllers
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
!_enterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
return Redirect("~/");
}

View File

@ -41,7 +41,7 @@ namespace Bit.Portal.Controllers
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
!_enterprisePortalCurrentContext.CanManageSsoForSelectedOrganization)
{
return Redirect("~/");
}
@ -63,7 +63,7 @@ namespace Bit.Portal.Controllers
}
if (!_enterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso ||
!_enterprisePortalCurrentContext.AdminForSelectedOrganization)
!_enterprisePortalCurrentContext.CanManageSsoForSelectedOrganization)
{
return Redirect("~/");
}

View File

@ -7,6 +7,7 @@ using Bit.Core.Repositories;
using System.Linq;
using System.Collections.Generic;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Portal
{
@ -37,6 +38,14 @@ namespace Bit.Portal
public bool OwnerForSelectedOrganization =>
SelectedOrganizationDetails?.Type == Core.Enums.OrganizationUserType.Owner;
public bool CanManagePoliciesForSelectedOrganization =>
AdminForSelectedOrganization || SelectedOrganizationDetailsPermissions.ManagePolicies == true;
public bool CanManageSsoForSelectedOrganization =>
AdminForSelectedOrganization || SelectedOrganizationDetailsPermissions.ManageSso == true;
public Permissions SelectedOrganizationDetailsPermissions => CoreHelpers.LoadClassFromJsonData<Permissions>(SelectedOrganizationDetails?.Permissions);
public async override Task SetContextAsync(ClaimsPrincipal user)
{
var nameId = user.FindFirstValue(ClaimTypes.NameIdentifier);

View File

@ -10,7 +10,7 @@
</div>
<div class="row">
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
EnterprisePortalCurrentContext.CanManageSsoForSelectedOrganization)
{
<div class="col">
<a class="card p-5 border border-primary" asp-area="" asp-controller="Sso" asp-action="Index">
@ -20,7 +20,7 @@
}
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
EnterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
<div class="col">
<a class="card p-5 border border-primary" asp-area="" asp-controller="Policies" asp-action="Index">
@ -28,4 +28,4 @@
</a>
</div>
}
</div>
</div>

View File

@ -32,7 +32,7 @@
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UseSso &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
EnterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Sso" asp-action="Index">
@ -42,7 +42,7 @@
}
@if (EnterprisePortalCurrentContext.SelectedOrganizationDetails.UsePolicies &&
EnterprisePortalCurrentContext.AdminForSelectedOrganization)
EnterprisePortalCurrentContext.CanManagePoliciesForSelectedOrganization)
{
<li class="nav-item">
<a class="nav-link" asp-area="" asp-controller="Policies" asp-action="Index">

View File

@ -60,7 +60,7 @@ namespace Bit.Api.Controllers
{
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -137,7 +137,7 @@ namespace Bit.Api.Controllers
public async Task<CipherMiniResponseModel> PostAdmin([FromBody]CipherCreateRequestModel model)
{
var cipher = model.Cipher.ToOrganizationCipher();
if (!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
if (!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -181,7 +181,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -200,7 +200,7 @@ namespace Bit.Api.Controllers
{
var userId = _userService.GetProperUserId(User).Value;
var orgIdGuid = new Guid(organizationId);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManageAllCollections(orgIdGuid) && !_currentContext.AccessReports(orgIdGuid))
{
throw new NotFoundException();
}
@ -243,7 +243,7 @@ namespace Bit.Api.Controllers
}
var orgId = new Guid(organizationId);
if (!_currentContext.OrganizationAdmin(orgId))
if (!_currentContext.AccessImportExport(orgId))
{
throw new NotFoundException();
}
@ -308,7 +308,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -338,7 +338,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -371,7 +371,7 @@ namespace Bit.Api.Controllers
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!_currentContext.OrganizationAdmin(new Guid(model.OrganizationId)))
!_currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
@ -398,7 +398,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -427,7 +427,7 @@ namespace Bit.Api.Controllers
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!_currentContext.OrganizationAdmin(new Guid(model.OrganizationId)))
!_currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
@ -456,7 +456,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -550,7 +550,7 @@ namespace Bit.Api.Controllers
else
{
var orgId = new Guid(organizationId);
if (!_currentContext.OrganizationAdmin(orgId))
if (!_currentContext.ManageAllCollections(orgId))
{
throw new NotFoundException();
}
@ -593,7 +593,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -651,7 +651,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationAdmin(cipher.OrganizationId.Value))
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}

View File

@ -45,13 +45,13 @@ namespace Bit.Api.Controllers
public async Task<CollectionGroupDetailsResponseModel> GetDetails(string orgId, string id)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationManager(orgIdGuid))
if (!ManageAnyCollections(orgIdGuid) && !_currentContext.ManageUsers(orgIdGuid))
{
throw new NotFoundException();
}
var idGuid = new Guid(id);
if (_currentContext.OrganizationAdmin(orgIdGuid))
if (_currentContext.ManageAllCollections(orgIdGuid))
{
var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(idGuid);
if (collectionDetails?.Item1 == null || collectionDetails.Item1.OrganizationId != orgIdGuid)
@ -76,7 +76,7 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<CollectionResponseModel>> Get(string orgId)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManageAllCollections(orgIdGuid) && !_currentContext.ManageUsers(orgIdGuid))
{
throw new NotFoundException();
}
@ -108,14 +108,14 @@ namespace Bit.Api.Controllers
public async Task<CollectionResponseModel> Post(string orgId, [FromBody]CollectionRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationManager(orgIdGuid))
if (!ManageAnyCollections(orgIdGuid))
{
throw new NotFoundException();
}
var collection = model.ToCollection(orgIdGuid);
await _collectionService.SaveAsync(collection, model.Groups?.Select(g => g.ToSelectionReadOnly()),
!_currentContext.OrganizationAdmin(orgIdGuid) ? _currentContext.UserId : null);
!_currentContext.ManageAllCollections(orgIdGuid) ? _currentContext.UserId : null);
return new CollectionResponseModel(collection);
}
@ -154,7 +154,7 @@ namespace Bit.Api.Controllers
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
{
if (!_currentContext.OrganizationManager(orgId))
if (!ManageAnyCollections(orgId))
{
throw new NotFoundException();
}
@ -169,5 +169,10 @@ namespace Bit.Api.Controllers
return collection;
}
private bool ManageAnyCollections(Guid orgId)
{
return _currentContext.ManageAssignedCollections(orgId) || _currentContext.ManageAllCollections(orgId);
}
}
}

View File

@ -61,7 +61,7 @@ namespace Bit.Api.Controllers
var canView = false;
if (cipher.OrganizationId.HasValue)
{
canView = _currentContext.OrganizationAdmin(cipher.OrganizationId.Value);
canView = _currentContext.AccessEventLogs(cipher.OrganizationId.Value);
}
else if (cipher.UserId.HasValue)
{
@ -86,7 +86,7 @@ namespace Bit.Api.Controllers
[FromQuery]DateTime? start = null, [FromQuery]DateTime? end = null, [FromQuery]string continuationToken = null)
{
var orgId = new Guid(id);
if (!_currentContext.OrganizationAdmin(orgId))
if (!_currentContext.AccessEventLogs(orgId))
{
throw new NotFoundException();
}
@ -104,7 +104,7 @@ namespace Bit.Api.Controllers
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || !organizationUser.UserId.HasValue ||
!_currentContext.OrganizationAdmin(organizationUser.OrganizationId))
!_currentContext.AccessEventLogs(organizationUser.OrganizationId))
{
throw new NotFoundException();
}

View File

@ -34,7 +34,7 @@ namespace Bit.Api.Controllers
public async Task<GroupResponseModel> Get(string orgId, string id)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
@ -46,7 +46,7 @@ namespace Bit.Api.Controllers
public async Task<GroupDetailsResponseModel> GetDetails(string orgId, string id)
{
var groupDetails = await _groupRepository.GetByIdWithCollectionsAsync(new Guid(id));
if (groupDetails?.Item1 == null || !_currentContext.OrganizationAdmin(groupDetails.Item1.OrganizationId))
if (groupDetails?.Item1 == null || !_currentContext.ManageGroups(groupDetails.Item1.OrganizationId))
{
throw new NotFoundException();
}
@ -58,7 +58,11 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<GroupResponseModel>> Get(string orgId)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationManager(orgIdGuid))
var canAccess = _currentContext.ManageGroups(orgIdGuid) ||
_currentContext.ManageAssignedCollections(orgIdGuid) ||
_currentContext.ManageAllCollections(orgIdGuid);
if (!canAccess)
{
throw new NotFoundException();
}
@ -73,7 +77,7 @@ namespace Bit.Api.Controllers
{
var idGuid = new Guid(id);
var group = await _groupRepository.GetByIdAsync(idGuid);
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
@ -86,7 +90,7 @@ namespace Bit.Api.Controllers
public async Task<GroupResponseModel> Post(string orgId, [FromBody]GroupRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManageGroups(orgIdGuid))
{
throw new NotFoundException();
}
@ -101,7 +105,7 @@ namespace Bit.Api.Controllers
public async Task<GroupResponseModel> Put(string orgId, string id, [FromBody]GroupRequestModel model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
@ -114,7 +118,7 @@ namespace Bit.Api.Controllers
public async Task PutUsers(string orgId, string id, [FromBody]IEnumerable<Guid> model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
@ -126,7 +130,7 @@ namespace Bit.Api.Controllers
public async Task Delete(string orgId, string id)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
@ -139,7 +143,7 @@ namespace Bit.Api.Controllers
public async Task Delete(string orgId, string id, string orgUserId)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !_currentContext.OrganizationAdmin(group.OrganizationId))
if (group == null || !_currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}

View File

@ -9,6 +9,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core;
using System.Collections.Generic;
using Bit.Core.Models.Business;
namespace Bit.Api.Controllers
{
@ -46,7 +47,7 @@ namespace Bit.Api.Controllers
public async Task<OrganizationUserDetailsResponseModel> Get(string orgId, string id)
{
var organizationUser = await _organizationUserRepository.GetByIdWithCollectionsAsync(new Guid(id));
if (organizationUser == null || !_currentContext.OrganizationAdmin(organizationUser.Item1.OrganizationId))
if (organizationUser == null || !_currentContext.ManageUsers(organizationUser.Item1.OrganizationId))
{
throw new NotFoundException();
}
@ -58,7 +59,7 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(string orgId)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationManager(orgGuidId))
if (!_currentContext.ManageAssignedCollections(orgGuidId) && !_currentContext.ManageGroups(orgGuidId))
{
throw new NotFoundException();
}
@ -74,7 +75,7 @@ namespace Bit.Api.Controllers
public async Task<IEnumerable<string>> GetGroups(string orgId, string id)
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || !_currentContext.OrganizationAdmin(organizationUser.OrganizationId))
if (organizationUser == null || !_currentContext.ManageGroups(organizationUser.OrganizationId))
{
throw new NotFoundException();
}
@ -88,21 +89,20 @@ namespace Bit.Api.Controllers
public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.InviteUserAsync(orgGuidId, userId.Value, model.Emails, model.Type.Value,
model.AccessAll, null, model.Collections?.Select(c => c.ToSelectionReadOnly()));
var result = await _organizationService.InviteUserAsync(orgGuidId, userId.Value, null, new OrganizationUserInvite(model));
}
[HttpPost("{id}/reinvite")]
public async Task Reinvite(string orgId, string id)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
@ -127,7 +127,7 @@ namespace Bit.Api.Controllers
public async Task Confirm(string orgId, string id, [FromBody]OrganizationUserConfirmRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
@ -142,7 +142,7 @@ namespace Bit.Api.Controllers
public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
@ -163,7 +163,7 @@ namespace Bit.Api.Controllers
public async Task PutGroups(string orgId, string id, [FromBody]OrganizationUserUpdateGroupsRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}
@ -174,7 +174,8 @@ namespace Bit.Api.Controllers
throw new NotFoundException();
}
await _organizationService.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)));
var loggedInUserId = _userService.GetProperUserId(User);
await _organizationService.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
}
[HttpDelete("{id}")]
@ -182,7 +183,7 @@ namespace Bit.Api.Controllers
public async Task Delete(string orgId, string id)
{
var orgGuidId = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgGuidId))
if (!_currentContext.ManageUsers(orgGuidId))
{
throw new NotFoundException();
}

View File

@ -52,7 +52,7 @@ namespace Bit.Api.Controllers
public async Task<PolicyResponseModel> Get(string orgId, int type)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}
@ -69,7 +69,7 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<PolicyResponseModel>> Get(string orgId)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationManager(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}
@ -108,7 +108,7 @@ namespace Bit.Api.Controllers
public async Task<PolicyResponseModel> Put(string orgId, int type, [FromBody]PolicyRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}

View File

@ -167,7 +167,7 @@ namespace Bit.Api.Controllers
var user = await CheckAsync(model.MasterPasswordHash, false);
var orgIdGuid = new Guid(id);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}
@ -190,7 +190,7 @@ namespace Bit.Api.Controllers
var user = await CheckAsync(model.MasterPasswordHash, false);
var orgIdGuid = new Guid(id);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}
@ -331,7 +331,7 @@ namespace Bit.Api.Controllers
var user = await CheckAsync(model.MasterPasswordHash, false);
var orgIdGuid = new Guid(id);
if (!_currentContext.OrganizationAdmin(orgIdGuid))
if (!_currentContext.ManagePolicies(orgIdGuid))
{
throw new NotFoundException();
}

View File

@ -5,6 +5,7 @@ using System.Net;
using System.Threading.Tasks;
using Bit.Core;
using Bit.Core.Models.Api.Public;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
@ -116,8 +117,15 @@ namespace Bit.Api.Public.Controllers
public async Task<IActionResult> Post([FromBody]MemberCreateRequestModel model)
{
var associations = model.Collections?.Select(c => c.ToSelectionReadOnly());
var user = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null,
model.Email, model.Type.Value, model.AccessAll.Value, model.ExternalId, associations);
var invite = new OrganizationUserInvite
{
Emails = new List<string> { model.Email },
Type = model.Type.Value,
AccessAll = model.AccessAll.Value,
Collections = associations
};
var userPromise = await _organizationService.InviteUserAsync(_currentContext.OrganizationId.Value, null, model.ExternalId, invite);
var user = userPromise.FirstOrDefault();
var response = new MemberResponseModel(user, associations);
return new JsonResult(response);
}
@ -178,7 +186,7 @@ namespace Bit.Api.Public.Controllers
{
return new NotFoundResult();
}
await _organizationService.UpdateUserGroupsAsync(existingUser, model.GroupIds);
await _organizationService.UpdateUserGroupsAsync(existingUser, model.GroupIds, null);
return new OkResult();
}

View File

@ -8,6 +8,7 @@ using Bit.Core.Repositories;
using System.Threading.Tasks;
using System.Security.Claims;
using Bit.Core.Utilities;
using Bit.Core.Models.Data;
namespace Bit.Core
{
@ -148,6 +149,18 @@ namespace Bit.Core
Type = OrganizationUserType.Manager
}));
}
if (claimsDict.ContainsKey("orgcustom"))
{
Organizations.AddRange(claimsDict["orgcustom"].Select(c =>
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Custom,
Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict)
}));
}
return Task.FromResult(0);
}
@ -174,6 +187,61 @@ namespace Bit.Core
return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Owner) ?? false;
}
public bool OrganizationCustom(Guid orgId)
{
return Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false;
}
public bool AccessBusinessPortal(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessBusinessPortal) ?? false);
}
public bool AccessEventLogs(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessEventLogs) ?? false);
}
public bool AccessImportExport(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessImportExport) ?? false);
}
public bool AccessReports(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.AccessReports) ?? false);
}
public bool ManageAllCollections(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageAllCollections) ?? false);
}
public bool ManageAssignedCollections(Guid orgId)
{
return OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageAssignedCollections) ?? false);
}
public bool ManageGroups(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageGroups) ?? false);
}
public bool ManagePolicies(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManagePolicies) ?? false);
}
public bool ManageSso(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageSso) ?? false);
}
public bool ManageUsers(Guid orgId)
{
return OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId && o.Permissions.ManageUsers) ?? false);
}
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId)
{
@ -196,6 +264,29 @@ namespace Bit.Core
return claims[type].FirstOrDefault()?.Value;
}
private Permissions SetOrganizationPermissionsFromClaims(string organizationId, Dictionary<string, IEnumerable<Claim>> claimsDict)
{
bool hasClaim(string claimKey)
{
return claimsDict.ContainsKey(claimKey) ?
claimsDict[claimKey].Any(x => x.Value == organizationId) : false;
}
return new Permissions
{
AccessBusinessPortal = hasClaim("accessbusinessportal"),
AccessEventLogs = hasClaim("accesseventlogs"),
AccessImportExport = hasClaim(""),
AccessReports = hasClaim("accessreports"),
ManageAllCollections = hasClaim(""),
ManageAssignedCollections = hasClaim(""),
ManageGroups = hasClaim(""),
ManagePolicies = hasClaim(""),
ManageSso = hasClaim("managesso"),
ManageUsers = hasClaim("manageusers")
};
}
public class CurrentContentOrganization
{
public CurrentContentOrganization() { }
@ -204,10 +295,12 @@ namespace Bit.Core
{
Id = orgUser.OrganizationId;
Type = orgUser.Type;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
}
public Guid Id { get; set; }
public OrganizationUserType Type { get; set; }
public Permissions Permissions { get; set; }
}
}
}

View File

@ -6,5 +6,6 @@
Admin = 1,
User = 2,
Manager = 3,
Custom = 4,
}
}

View File

@ -20,7 +20,8 @@ namespace Bit.Core.IdentityServer
"orgowner",
"orgadmin",
"orgmanager",
"orguser"
"orguser",
"orgcustom",
}),
new ApiResource("internal", new string[] { JwtClaimTypes.Subject }),
new ApiResource("api.push", new string[] { JwtClaimTypes.Subject }),

View File

@ -1,4 +1,5 @@
using Bit.Core.Models.Table;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
@ -17,6 +18,8 @@ namespace Bit.Core.Models.Api
[StringLength(50)]
public string BillingEmail { get; set; }
public Permissions Permissions { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization, GlobalSettings globalSettings)
{
if (!globalSettings.SelfHosted)

View File

@ -1,8 +1,9 @@
using Bit.Core.Models.Table;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System;
using System.Linq;
using System.Text.Json;
namespace Bit.Core.Models.Api
{
@ -13,6 +14,7 @@ namespace Bit.Core.Models.Api
[Required]
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
@ -62,11 +64,16 @@ namespace Bit.Core.Models.Api
[Required]
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public OrganizationUser ToOrganizationUser(OrganizationUser existingUser)
{
existingUser.Type = Type.Value;
existingUser.Permissions = JsonSerializer.Serialize(Permissions, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
existingUser.AccessAll = AccessAll;
return existingUser;
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Models.Data;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Api
{
public class OrganizationUserResponseModel : ResponseModel
@ -22,6 +22,7 @@ namespace Bit.Core.Models.Api
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
}
public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser, string obj = "organizationUser")
@ -37,6 +38,7 @@ namespace Bit.Core.Models.Api
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
}
public string Id { get; set; }
@ -44,6 +46,7 @@ namespace Bit.Core.Models.Api
public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
}
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel

View File

@ -1,6 +1,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Core.Models.Api
{
public class ProfileOrganizationResponseModel : ResponseModel
@ -29,6 +29,7 @@ namespace Bit.Core.Models.Api
Enabled = organization.Enabled;
SsoBound = !string.IsNullOrWhiteSpace(organization.SsoExternalId);
Identifier = organization.Identifier;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organization.Permissions);
}
public string Id { get; set; }
@ -53,5 +54,6 @@ namespace Bit.Core.Models.Api
public bool Enabled { get; set; }
public bool SsoBound { get; set; }
public string Identifier { get; set; }
public Permissions Permissions { get; set; }
}
}

View File

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Business
{
public class OrganizationUserInvite
{
public IEnumerable<string> Emails { get; set; }
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnly> Collections { get; set; }
public OrganizationUserInvite() {}
public OrganizationUserInvite(OrganizationUserInviteRequestModel requestModel)
{
Emails = requestModel.Emails;
Type = requestModel.Type.Value;
AccessAll = requestModel.AccessAll;
Collections = requestModel.Collections.Select(c => c.ToSelectionReadOnly());
Permissions = requestModel.Permissions;
}
}
}

View File

@ -27,5 +27,6 @@ namespace Bit.Core.Models.Data
public bool Enabled { get; set; }
public string SsoExternalId { get; set; }
public string Identifier { get; set; }
public string Permissions { get; set; }
}
}

View File

@ -21,6 +21,7 @@ namespace Bit.Core.Models.Data
public bool AccessAll { get; set; }
public string ExternalId { get; set; }
public string SsoExternalId { get; set; }
public string Permissions { get; set; }
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{

View File

@ -0,0 +1,16 @@
namespace Bit.Core.Models.Data
{
public class Permissions
{
public bool AccessBusinessPortal { get; set; }
public bool AccessEventLogs { get; set; }
public bool AccessImportExport { get; set; }
public bool AccessReports { get; set; }
public bool ManageAssignedCollections { get; set; }
public bool ManageAllCollections { get; set; }
public bool ManageGroups { get; set; }
public bool ManagePolicies { get; set; }
public bool ManageSso { get; set; }
public bool ManageUsers { get; set; }
}
}

View File

@ -17,6 +17,7 @@ namespace Bit.Core.Models.Table
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public string Permissions { get; set; }
public void SetNewId()
{

View File

@ -30,11 +30,7 @@ namespace Bit.Core.Services
Task UpdateAsync(Organization organization, bool updateBilling = false);
Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
IEnumerable<SelectionReadOnly> collections);
Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string externalId, OrganizationUserInvite orgUserInvite);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService);
@ -44,7 +40,7 @@ namespace Bit.Core.Services
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
Task DeleteUserAsync(Guid organizationId, Guid userId);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
int? version = null);

View File

@ -13,6 +13,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Data;
using System.IO;
using Newtonsoft.Json;
using System.Text.Json;
namespace Bit.Core.Services
{
@ -972,45 +973,25 @@ namespace Bit.Core.Services
await UpdateAsync(organization);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
var results = await InviteUserAsync(organizationId, invitingUserId, new List<string> { email },
type, accessAll, externalId, collections);
var result = results.FirstOrDefault();
if (result == null)
{
throw new BadRequestException("This user has already been invited.");
}
return result;
}
public async Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<string> emails, OrganizationUserType type, bool accessAll, string externalId,
IEnumerable<SelectionReadOnly> collections)
string externalId, OrganizationUserInvite invite)
{
var organization = await GetOrgById(organizationId);
if (organization == null)
if (organization == null || invite?.Emails == null)
{
throw new NotFoundException();
}
if (type == OrganizationUserType.Owner && invitingUserId.HasValue)
if (invitingUserId.HasValue && invite.Type.HasValue)
{
var invitingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(invitingUserId.Value);
var anyOwners = invitingUserOrgs.Any(
u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (!anyOwners)
{
throw new BadRequestException("Only owners can invite new owners.");
}
await ValidateOrganizationUserUpdatePermissions(invitingUserId.Value, organizationId, invite.Type.Value, null);
}
if (organization.Seats.HasValue)
{
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
var availableSeats = organization.Seats.Value - userCount;
if (availableSeats < emails.Count())
if (availableSeats < invite.Emails.Count())
{
throw new BadRequestException("You have reached the maximum number of users " +
$"({organization.Seats.Value}) for this organization.");
@ -1019,7 +1000,7 @@ namespace Bit.Core.Services
var orgUsers = new List<OrganizationUser>();
var orgUserInvitedCount = 0;
foreach (var email in emails)
foreach (var email in invite.Emails)
{
// Make sure user is not already invited
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
@ -1035,17 +1016,21 @@ namespace Bit.Core.Services
UserId = null,
Email = email.ToLowerInvariant(),
Key = null,
Type = type,
Type = invite.Type.Value,
Status = OrganizationUserStatusType.Invited,
AccessAll = accessAll,
AccessAll = invite.AccessAll,
ExternalId = externalId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
RevisionDate = DateTime.UtcNow,
Permissions = System.Text.Json.JsonSerializer.Serialize(invite.Permissions, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}),
};
if (!orgUser.AccessAll && collections.Any())
if (!orgUser.AccessAll && invite.Collections.Any())
{
await _organizationUserRepository.CreateAsync(orgUser, collections);
await _organizationUserRepository.CreateAsync(orgUser, invite.Collections);
}
else
{
@ -1264,21 +1249,14 @@ namespace Bit.Core.Services
throw new BadRequestException("Invite the user first.");
}
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
if (user.Equals(originalUser)) {
throw new BadRequestException("Please make changes before saving.");
}
if (savingUserId.HasValue)
{
var savingUserOrgs = await _organizationUserRepository.GetManyByUserAsync(savingUserId.Value);
var savingUserIsOrgOwner = savingUserOrgs
.Any(u => u.OrganizationId == user.OrganizationId && u.Type == OrganizationUserType.Owner);
if (!savingUserIsOrgOwner)
{
var originalUser = await _organizationUserRepository.GetByIdAsync(user.Id);
var isOwner = originalUser.Type == OrganizationUserType.Owner;
var nowOwner = user.Type == OrganizationUserType.Owner;
if ((isOwner && !nowOwner) || (!isOwner && nowOwner))
{
throw new BadRequestException("Only an owner can change the user type of another owner.");
}
}
await ValidateOrganizationUserUpdatePermissions(savingUserId.Value, user.OrganizationId, user.Type, originalUser.Type);
}
var confirmedOwners = (await GetConfirmedOwnersAsync(user.OrganizationId)).ToList();
@ -1367,8 +1345,12 @@ namespace Bit.Core.Services
}
}
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds)
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await ValidateOrganizationUserUpdatePermissions(loggedInUserId.Value, organizationUser.OrganizationId, organizationUser.Type, null);
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
await _eventService.LogOrganizationUserEventAsync(organizationUser,
EventType.OrganizationUser_UpdatedGroups);
@ -1502,8 +1484,16 @@ namespace Bit.Core.Services
try
{
var newUser = await InviteUserAsync(organizationId, importingUserId, user.Email,
OrganizationUserType.User, false, user.ExternalId, new List<SelectionReadOnly>());
var invite = new OrganizationUserInvite
{
Emails = new List<string> { user.Email },
Type = OrganizationUserType.User,
AccessAll = false,
Collections = new List<SelectionReadOnly>(),
};
var newUserPromise = await InviteUserAsync(organizationId, importingUserId, user.ExternalId, invite);
var newUser = newUserPromise.FirstOrDefault();
existingExternalUsersIdDict.Add(newUser.ExternalId, newUser.Id);
}
catch (BadRequestException)
@ -1673,5 +1663,59 @@ namespace Bit.Core.Services
$"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users.");
}
}
private async Task ValidateOrganizationUserUpdatePermissions(Guid loggedInUserId, Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType)
{
var loggedInUserOrgs = await _organizationUserRepository.GetManyByUserAsync(loggedInUserId);
var loggedInAsOrgOwner = loggedInUserOrgs
.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Owner);
if (loggedInAsOrgOwner)
{
return;
}
var isOwner = oldType == OrganizationUserType.Owner;
var nowOwner = newType == OrganizationUserType.Owner;
var ownerUserConfigurationAttempt = (isOwner && nowOwner) || !(isOwner.Equals(nowOwner));
if (ownerUserConfigurationAttempt)
{
throw new BadRequestException("Only an Owner can configure another Owner's account.");
}
var loggedInAsOrgAdmin = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Admin);
if (loggedInAsOrgAdmin)
{
return;
}
var isCustom = oldType == OrganizationUserType.Custom;
var nowCustom = newType == OrganizationUserType.Custom;
var customUserConfigurationAttempt = (isCustom && nowCustom) || !(isCustom.Equals(nowCustom));
if (customUserConfigurationAttempt)
{
throw new BadRequestException("Only Owners and Admins can configure Custom accounts.");
}
var loggedInAsOrgCustom = loggedInUserOrgs.Any(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
if (!loggedInAsOrgCustom)
{
return;
}
var loggedInCustomOrgUser = loggedInUserOrgs.First(u => u.OrganizationId == organizationId && u.Type == OrganizationUserType.Custom);
var loggedInUserPermissions = CoreHelpers.LoadClassFromJsonData<Permissions>(loggedInCustomOrgUser.Permissions);
if (!loggedInUserPermissions.ManageUsers)
{
throw new BadRequestException("Your account does not have permission to manage users.");
}
var isAdmin = oldType == OrganizationUserType.Admin;
var nowAdmin = newType == OrganizationUserType.Admin;
var adminUserConfigurationAttempt = (isAdmin && nowAdmin) || !(isAdmin.Equals(nowAdmin));
if (adminUserConfigurationAttempt)
{
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
}
}

View File

@ -20,6 +20,7 @@ using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Blob;
using Bit.Core.Models.Table;
using IdentityModel;
using System.Text.Json;
namespace Bit.Core.Utilities
{
@ -730,6 +731,62 @@ namespace Bit.Core.Utilities
claims.Add(new KeyValuePair<string, string>("orguser", org.Id.ToString()));
}
break;
case Enums.OrganizationUserType.Custom:
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgcustom", org.Id.ToString()));
if (org.Permissions.AccessBusinessPortal)
{
claims.Add(new KeyValuePair<string, string>("accessbusinessportal", org.Id.ToString()));
}
if (org.Permissions.AccessEventLogs)
{
claims.Add(new KeyValuePair<string, string>("accesseventlogs", org.Id.ToString()));
}
if (org.Permissions.AccessImportExport)
{
claims.Add(new KeyValuePair<string, string>("accessimportexport", org.Id.ToString()));
}
if (org.Permissions.AccessReports)
{
claims.Add(new KeyValuePair<string, string>("accessreports", org.Id.ToString()));
}
if (org.Permissions.ManageAllCollections)
{
claims.Add(new KeyValuePair<string, string>("manageallcollections", org.Id.ToString()));
}
if (org.Permissions.ManageAssignedCollections)
{
claims.Add(new KeyValuePair<string, string>("manageassignedcollections", org.Id.ToString()));
}
if (org.Permissions.ManageGroups)
{
claims.Add(new KeyValuePair<string, string>("managegroups", org.Id.ToString()));
}
if (org.Permissions.ManagePolicies)
{
claims.Add(new KeyValuePair<string, string>("managepolicies", org.Id.ToString()));
}
if (org.Permissions.ManageSso)
{
claims.Add(new KeyValuePair<string, string>("managesso", org.Id.ToString()));
}
if (org.Permissions.ManageUsers)
{
claims.Add(new KeyValuePair<string, string>("manageusers", org.Id.ToString()));
}
}
break;
default:
break;
}
@ -737,5 +794,20 @@ namespace Bit.Core.Utilities
}
return claims;
}
public static T LoadClassFromJsonData<T>(string jsonData) where T : new()
{
if (string.IsNullOrWhiteSpace(jsonData))
{
return new T();
}
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
}
}
}

View File

@ -9,7 +9,8 @@
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
@ -26,7 +27,8 @@ BEGIN
[AccessAll],
[ExternalId],
[CreationDate],
[RevisionDate]
[RevisionDate],
[Permissions]
)
VALUES
(
@ -40,6 +42,7 @@ BEGIN
@AccessAll,
@ExternalId,
@CreationDate,
@RevisionDate
@RevisionDate,
@Permissions
)
END
END

View File

@ -10,12 +10,13 @@
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions
;WITH [AvailableCollectionsCTE] AS(
SELECT
@ -41,4 +42,4 @@ BEGIN
@Collections
WHERE
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
END
END

View File

@ -9,7 +9,8 @@
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
@ -26,9 +27,10 @@ BEGIN
[AccessAll] = @AccessAll,
[ExternalId] = @ExternalId,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
[RevisionDate] = @RevisionDate,
[Permissions] = @Permissions
WHERE
[Id] = @Id
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END

View File

@ -10,13 +10,13 @@
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions
-- Update
UPDATE
[Target]
@ -72,4 +72,4 @@ BEGIN
WHERE
[Id] = CU.[CollectionId]
)
END
END

View File

@ -1,15 +1,16 @@
CREATE TABLE [dbo].[OrganizationUser] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL,
[Email] NVARCHAR (50) NULL,
[Key] VARCHAR (MAX) NULL,
[Status] TINYINT NOT NULL,
[Type] TINYINT NOT NULL,
[AccessAll] BIT NOT NULL,
[ExternalId] NVARCHAR (300) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NULL,
[Email] NVARCHAR (50) NULL,
[Key] VARCHAR (MAX) NULL,
[Status] TINYINT NOT NULL,
[Type] TINYINT NOT NULL,
[AccessAll] BIT NOT NULL,
[ExternalId] NVARCHAR (300) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
[Permissions] NVARCHAR (MAX) NULL,
CONSTRAINT [PK_OrganizationUser] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationUser_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])

View File

@ -22,7 +22,8 @@ SELECT
OU.[Key],
OU.[Status],
OU.[Type],
SU.[ExternalId] SsoExternalId
SU.[ExternalId] SsoExternalId,
OU.[Permissions]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN

View File

@ -12,10 +12,11 @@ SELECT
OU.[Type],
OU.[AccessAll],
OU.[ExternalId],
SU.[ExternalId] SsoExternalId
SU.[ExternalId] SsoExternalId,
OU.[Permissions]
FROM
[dbo].[OrganizationUser] OU
LEFT JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]

View File

@ -51,7 +51,7 @@ namespace Bit.Core.Test.AutoFixture.CipherFixtures
{
public InlineKnownUserCipherAutoDataAttribute(string userId, params object[] values) : base(new ICustomization[]
{ new SutProviderCustomization(), new UserCipher { UserId = new Guid(userId) } }, values)
{ }
{ }
}
internal class OrganizationCipherAutoDataAttribute : CustomAutoDataAttribute

View File

@ -0,0 +1,114 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using AutoFixture;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Utilities;
namespace Bit.Core.Test.AutoFixture.OrganizationFixtures
{
internal class PaidOrganization : ICustomization
{
public PlanType CheckedPlanType { get; set; }
public void Customize(IFixture fixture)
{
var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != Enums.PlanType.Free && !p.Disabled).Select(p => p.Type).ToList();
var lowestActivePaidPlan = validUpgradePlans.First();
CheckedPlanType = CheckedPlanType.Equals(Enums.PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType;
validUpgradePlans.Remove(lowestActivePaidPlan);
fixture.Customize<Organization>(composer => composer
.With(o => o.PlanType, CheckedPlanType));
fixture.Customize<OrganizationUpgrade>(composer => composer
.With(ou => ou.Plan, validUpgradePlans.First()));
}
}
internal class FreeOrganizationUpgrade : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<Organization>(composer => composer
.With(o => o.PlanType, PlanType.Free));
var plansToIgnore = new List<PlanType> { PlanType.Free, PlanType.Custom };
var validPlans = StaticStore.Plans.Where(p => !plansToIgnore.Contains(p.Type) && !p.Disabled).Select(p => p.Type).ToList();
fixture.Customize<OrganizationUpgrade>(composer => composer
.With(ou => ou.Plan, validPlans.Last()));
fixture.Customize<Organization>(composer => composer
.Without(o => o.GatewaySubscriptionId));
}
}
internal class OrganizationInvite : ICustomization
{
public OrganizationUserType InviteeUserType { get; set; }
public OrganizationUserType InvitorUserType { get; set; }
public string PermissionsBlob { get; set; }
public void Customize(IFixture fixture)
{
var organizationId = new Guid();
PermissionsBlob = PermissionsBlob ?? JsonSerializer.Serialize(new Permissions(), new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
fixture.Customize<Organization>(composer => composer
.With(o => o.Id, organizationId));
fixture.Customize<OrganizationUser>(composer => composer
.With(ou => ou.OrganizationId, organizationId)
.With(ou => ou.Type, InvitorUserType)
.With(ou => ou.Permissions, PermissionsBlob));
fixture.Customize<OrganizationUserInvite>(composer => composer
.With(oi => oi.Type, InviteeUserType));
}
}
internal class PaidOrganizationAutoDataAttribute : CustomAutoDataAttribute
{
public PaidOrganizationAutoDataAttribute(int planType = 0) : base(new SutProviderCustomization(),
new PaidOrganization { CheckedPlanType = (PlanType)planType })
{ }
}
internal class InlinePaidOrganizationAutoDataAttribute : InlineCustomAutoDataAttribute
{
public InlinePaidOrganizationAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
typeof(PaidOrganization) }, values)
{ }
}
internal class FreeOrganizationUpgradeAutoDataAttribute : CustomAutoDataAttribute
{
public FreeOrganizationUpgradeAutoDataAttribute() : base(new SutProviderCustomization(), new FreeOrganizationUpgrade())
{ }
}
internal class InlineFreeOrganizationUpgradeAutoDataAttribute : InlineCustomAutoDataAttribute
{
public InlineFreeOrganizationUpgradeAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
typeof(FreeOrganizationUpgrade) }, values)
{ }
}
internal class OrganizationInviteAutoDataAttribute : CustomAutoDataAttribute
{
public OrganizationInviteAutoDataAttribute(int inviteeUserType = 0, int invitorUserType = 0, string permissionsBlob = null) : base(new SutProviderCustomization(),
new OrganizationInvite
{
InviteeUserType = (OrganizationUserType)inviteeUserType,
InvitorUserType = (OrganizationUserType)invitorUserType,
PermissionsBlob = permissionsBlob,
})
{ }
}
internal class InlineOrganizationInviteAutoDataAttribute : InlineCustomAutoDataAttribute
{
public InlineOrganizationInviteAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization),
typeof(OrganizationInvite) }, values)
{ }
}
}

View File

@ -3,11 +3,18 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.DataProtection;
using NSubstitute;
using Xunit;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Exceptions;
using Bit.Core.Enums;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using System.Text.Json;
namespace Bit.Core.Test.Services
{
@ -138,5 +145,216 @@ namespace Bit.Core.Test.Services
await orgUserRepo.Received(1).UpsertAsync(Arg.Any<OrganizationUser>());
await orgUserRepo.Received(2).CreateAsync(Arg.Any<OrganizationUser>());
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task UpgradePlan_OrganizationIsNull_Throws(Guid organizationId, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns(Task.FromResult<Organization>(null));
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.UpgradePlanAsync(organizationId, upgrade));
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task UpgradePlan_GatewayCustomIdIsNull_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
organization.GatewayCustomerId = string.Empty;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("no payment method", exception.Message);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task UpgradePlan_AlreadyInPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
upgrade.Plan = organization.PlanType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("already on this plan", exception.Message);
}
[Theory, PaidOrganizationAutoData]
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
Assert.Contains("can only upgrade", exception.Message);
}
[Theory]
[FreeOrganizationUpgradeAutoData]
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAsync(organization);
}
[Theory]
[OrganizationInviteAutoData]
public async Task InviteUser_NoEmails_Throws(Organization organization, OrganizationUser invitor,
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
{
invite.Emails = null;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
}
[Theory]
[OrganizationInviteAutoData(
inviteeUserType: (int)OrganizationUserType.Owner,
invitorUserType: (int)OrganizationUserType.Admin
)]
public async Task InviteUser_NonOwnerConfiguringOwner_Throws(Organization organization, OrganizationUserInvite invite,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByUserAsync(invitor.Id).Returns(new List<OrganizationUser> { invitor });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
Assert.Contains("only an owner", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteAutoData(
inviteeUserType: (int)OrganizationUserType.Custom,
invitorUserType: (int)OrganizationUserType.Admin
)]
public async Task InviteUser_NonAdminConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByUserAsync(invitor.Id).Returns(new List<OrganizationUser> { invitor });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
Assert.Contains("only owners and admins", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteAutoData(
inviteeUserType: (int)OrganizationUserType.Manager,
invitorUserType: (int)OrganizationUserType.Custom
)]
public async Task InviteUser_CustomUserWithoutManageUsersConfiguringUser_Throws(Organization organization, OrganizationUserInvite invite,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = false },
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
Assert.Contains("account does not have permission", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteAutoData(
inviteeUserType: (int)OrganizationUserType.Admin,
invitorUserType: (int)OrganizationUserType.Custom
)]
public async Task InviteUser_CustomUserConfiguringAdmin_Throws(Organization organization, OrganizationUserInvite invite,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite));
Assert.Contains("can not manage admins", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteAutoData(
inviteeUserType: (int)OrganizationUserType.User,
invitorUserType: (int)OrganizationUserType.Custom
)]
public async Task InviteUser_Passes(Organization organization, OrganizationUserInvite invite,
OrganizationUser invitor, SutProvider<OrganizationService> sutProvider)
{
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByUserAsync(invitor.UserId.Value).Returns(new List<OrganizationUser> { invitor });
await sutProvider.Sut.InviteUserAsync(organization.Id, invitor.UserId, null, invite);
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveUser_NoUserId_Throws(OrganizationUser user, Guid? savingUserId,
IEnumerable<SelectionReadOnly> collections, SutProvider<OrganizationService> sutProvider)
{
user.Id = default(Guid);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveUserAsync(user, savingUserId, collections));
Assert.Contains("invite the user first", exception.Message.ToLowerInvariant());
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveUser_NoChangeToData_Throws(OrganizationUser user, Guid? savingUserId,
IEnumerable<SelectionReadOnly> collections, SutProvider<OrganizationService> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetByIdAsync(user.Id).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveUserAsync(user, savingUserId, collections));
Assert.Contains("make changes before saving", exception.Message.ToLowerInvariant());
}
[Theory, CustomAutoData(typeof(SutProviderCustomization))]
public async Task SaveUser_Passes(OrganizationUser oldUserData, OrganizationUser newUserData,
IEnumerable<SelectionReadOnly> collections, OrganizationUser savingUser, SutProvider<OrganizationService> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
newUserData.Id = oldUserData.Id;
newUserData.UserId = oldUserData.UserId;
newUserData.OrganizationId = savingUser.OrganizationId = oldUserData.OrganizationId;
savingUser.Type = OrganizationUserType.Owner;
organizationUserRepository.GetByIdAsync(oldUserData.Id).Returns(oldUserData);
organizationUserRepository.GetManyByUserAsync(savingUser.UserId.Value).Returns(new List<OrganizationUser> { savingUser });
await sutProvider.Sut.SaveUserAsync(newUserData, savingUser.UserId, collections);
}
}
}

View File

@ -0,0 +1,331 @@
IF COL_LENGTH('[dbo].[OrganizationUser]', 'Permissions') IS NULL
BEGIN
ALTER TABLE
[dbo].[OrganizationUser]
ADD
[Permissions] NVARCHAR(MAX) NULL
END
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserView')
BEGIN
DROP VIEW [dbo].[OrganizationUserView];
END
GO
CREATE VIEW [dbo].[OrganizationUserView]
AS
SELECT
*
FROM
[dbo].[OrganizationUser]
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView];
END
GO
CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView]
AS
SELECT
OU.[UserId],
OU.[OrganizationId],
O.[Name],
O.[Enabled],
O.[UsePolicies],
O.[UseSso],
O.[UseGroups],
O.[UseDirectory],
O.[UseEvents],
O.[UseTotp],
O.[Use2fa],
O.[UseApi],
O.[SelfHost],
O.[UsersGetPremium],
O.[Seats],
O.[MaxCollections],
O.[MaxStorageGb],
O.[Identifier],
OU.[Key],
OU.[Status],
OU.[Type],
SU.[ExternalId] SsoExternalId,
OU.[Permissions]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
GO
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserUserDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationUserUserDetailsView];
END
GO
CREATE VIEW [dbo].[OrganizationUserUserDetailsView]
AS
SELECT
OU.[Id],
OU.[UserId],
OU.[OrganizationId],
U.[Name],
ISNULL(U.[Email], OU.[Email]) Email,
U.[TwoFactorProviders],
U.[Premium],
OU.[Status],
OU.[Type],
OU.[AccessAll],
OU.[ExternalId],
SU.[ExternalId] SsoExternalId,
OU.[Permissions]
FROM
[dbo].[OrganizationUser] OU
LEFT JOIN
[dbo].[User] U ON U.[Id] = OU.[UserId]
LEFT JOIN
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
GO
IF OBJECT_ID('[dbo].[OrganizationUser_Create]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_Create]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_Create]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@Email NVARCHAR(50),
@Key VARCHAR(MAX),
@Status TINYINT,
@Type TINYINT,
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OrganizationUser]
(
[Id],
[OrganizationId],
[UserId],
[Email],
[Key],
[Status],
[Type],
[AccessAll],
[ExternalId],
[CreationDate],
[RevisionDate],
[Permissions]
)
VALUES
(
@Id,
@OrganizationId,
@UserId,
@Email,
@Key,
@Status,
@Type,
@AccessAll,
@ExternalId,
@CreationDate,
@RevisionDate,
@Permissions
)
END
GO
IF OBJECT_ID('[dbo].[OrganizationUser_CreateWithCollections]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_CreateWithCollections]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_CreateWithCollections]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@Email NVARCHAR(50),
@Key VARCHAR(MAX),
@Status TINYINT,
@Type TINYINT,
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions
;WITH [AvailableCollectionsCTE] AS(
SELECT
[Id]
FROM
[dbo].[Collection]
WHERE
[OrganizationId] = @OrganizationId
)
INSERT INTO [dbo].[CollectionUser]
(
[CollectionId],
[OrganizationUserId],
[ReadOnly],
[HidePasswords]
)
SELECT
[Id],
@Id,
[ReadOnly],
[HidePasswords]
FROM
@Collections
WHERE
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
END
GO
IF OBJECT_ID('[dbo].[OrganizationUser_Update]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_Update]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_Update]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@Email NVARCHAR(50),
@Key VARCHAR(MAX),
@Status TINYINT,
@Type TINYINT,
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OrganizationUser]
SET
[OrganizationId] = @OrganizationId,
[UserId] = @UserId,
[Email] = @Email,
[Key] = @Key,
[Status] = @Status,
[Type] = @Type,
[AccessAll] = @AccessAll,
[ExternalId] = @ExternalId,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[Permissions] = @Permissions
WHERE
[Id] = @Id
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
GO
IF OBJECT_ID('[dbo].[OrganizationUser_UpdateWithCollections]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@Email NVARCHAR(50),
@Key VARCHAR(MAX),
@Status TINYINT,
@Type TINYINT,
@AccessAll BIT,
@ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions
-- Update
UPDATE
[Target]
SET
[Target].[ReadOnly] = [Source].[ReadOnly],
[Target].[HidePasswords] = [Source].[HidePasswords]
FROM
[dbo].[CollectionUser] AS [Target]
INNER JOIN
@Collections AS [Source] ON [Source].[Id] = [Target].[CollectionId]
WHERE
[Target].[OrganizationUserId] = @Id
AND (
[Target].[ReadOnly] != [Source].[ReadOnly]
OR [Target].[HidePasswords] != [Source].[HidePasswords]
)
-- Insert
INSERT INTO
[dbo].[CollectionUser]
SELECT
[Source].[Id],
@Id,
[Source].[ReadOnly],
[Source].[HidePasswords]
FROM
@Collections AS [Source]
INNER JOIN
[dbo].[Collection] C ON C.[Id] = [Source].[Id] AND C.[OrganizationId] = @OrganizationId
WHERE
NOT EXISTS (
SELECT
1
FROM
[dbo].[CollectionUser]
WHERE
[CollectionId] = [Source].[Id]
AND [OrganizationUserId] = @Id
)
-- Delete
DELETE
CU
FROM
[dbo].[CollectionUser] CU
WHERE
CU.[OrganizationUserId] = @Id
AND NOT EXISTS (
SELECT
1
FROM
@Collections
WHERE
[Id] = CU.[CollectionId]
)
END
GO