1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

Merge branch 'master' into feature/flexible-collections

This commit is contained in:
Vincent Salucci
2023-09-01 08:30:03 -05:00
18 changed files with 232 additions and 67 deletions

View File

@ -1,6 +1,4 @@
using Bit.Core.OrganizationFeatures.OrganizationUsers; using Bit.Scim.Groups;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Scim.Groups;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Users; using Bit.Scim.Users;
using Bit.Scim.Users.Interfaces; using Bit.Scim.Users.Interfaces;
@ -23,7 +21,6 @@ public static class ScimServiceCollectionExtensions
public static void AddScimUserCommands(this IServiceCollection services) public static void AddScimUserCommands(this IServiceCollection services)
{ {
services.AddScoped<IDeleteOrganizationUserCommand, DeleteOrganizationUserCommand>();
services.AddScoped<IPatchUserCommand, PatchUserCommand>(); services.AddScoped<IPatchUserCommand, PatchUserCommand>();
services.AddScoped<IPostUserCommand, PostUserCommand>(); services.AddScoped<IPostUserCommand, PostUserCommand>();
} }

View File

@ -44,7 +44,7 @@
<div class="float-right"> <div class="float-right">
@if (org.Status == OrganizationStatusType.Pending) @if (org.Status == OrganizationStatusType.Pending)
{ {
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId', '@org.OrganizationName');"> <a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId');">
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i> <i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
</a> </a>
} }

View File

@ -1,14 +1,14 @@
<script> <script>
function resendOwnerInvite(orgId, orgName) { function resendOwnerInvite(orgId) {
if (confirm('Resend invite to "' + orgName + '"?')) { if (confirm('Resend invite to organization?')) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: '@Url.Action("ResendOwnerInvite", "Organizations")' + '?id=' + orgId, url: '@Url.Action("ResendOwnerInvite", "Organizations")' + '?id=' + orgId,
dataType: 'json', dataType: 'json',
contentType: false, contentType: false,
processData: false, processData: false,
success: function (response) { success: function (response) {
alert('Invitation has been resent!'); alert('Invitation has been resent!');
}, },
error: function (response) { error: function (response) {
alert("Error!"); alert("Error!");
@ -17,4 +17,4 @@
} }
return false; return false;
} }
</script> </script>

View File

@ -236,7 +236,7 @@ public class TwoFactorController : Controller
[HttpPost("get-webauthn")] [HttpPost("get-webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretVerificationRequestModel model) public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretVerificationRequestModel model)
{ {
var user = await CheckAsync(model, true); var user = await CheckAsync(model, false);
var response = new TwoFactorWebAuthnResponseModel(user); var response = new TwoFactorWebAuthnResponseModel(user);
return response; return response;
} }
@ -245,7 +245,7 @@ public class TwoFactorController : Controller
[ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly [ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody] SecretVerificationRequestModel model) public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody] SecretVerificationRequestModel model)
{ {
var user = await CheckAsync(model, true); var user = await CheckAsync(model, false);
var reg = await _userService.StartWebAuthnRegistrationAsync(user); var reg = await _userService.StartWebAuthnRegistrationAsync(user);
return reg; return reg;
} }
@ -254,7 +254,7 @@ public class TwoFactorController : Controller
[HttpPost("webauthn")] [HttpPost("webauthn")]
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model) public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
{ {
var user = await CheckAsync(model, true); var user = await CheckAsync(model, false);
var success = await _userService.CompleteWebAuthRegistrationAsync( var success = await _userService.CompleteWebAuthRegistrationAsync(
user, model.Id.Value, model.Name, model.DeviceResponse); user, model.Id.Value, model.Name, model.DeviceResponse);
@ -271,7 +271,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn( public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
[FromBody] TwoFactorWebAuthnDeleteRequestModel model) [FromBody] TwoFactorWebAuthnDeleteRequestModel model)
{ {
var user = await CheckAsync(model, true); var user = await CheckAsync(model, false);
await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value); await _userService.DeleteWebAuthnKeyAsync(user, model.Id.Value);
var response = new TwoFactorWebAuthnResponseModel(user); var response = new TwoFactorWebAuthnResponseModel(user);
return response; return response;

View File

@ -30,6 +30,7 @@ public class OrganizationUsersController : Controller
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -41,7 +42,8 @@ public class OrganizationUsersController : Controller
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery, ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand) IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -53,6 +55,7 @@ public class OrganizationUsersController : Controller
_currentContext = currentContext; _currentContext = currentContext;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -308,7 +311,7 @@ public class OrganizationUsersController : Controller
} }
var loggedInUserId = _userService.GetProperUserId(User); var loggedInUserId = _userService.GetProperUserId(User);
await _organizationService.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId); await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
} }
[HttpPut("{userId}/reset-password-enrollment")] [HttpPut("{userId}/reset-password-enrollment")]

View File

@ -3,6 +3,7 @@ using Bit.Api.Models.Public.Request;
using Bit.Api.Models.Public.Response; using Bit.Api.Models.Public.Response;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -19,19 +20,22 @@ public class MembersController : Controller
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
public MembersController( public MembersController(
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository, IGroupRepository groupRepository,
IOrganizationService organizationService, IOrganizationService organizationService,
IUserService userService, IUserService userService,
ICurrentContext currentContext) ICurrentContext currentContext,
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand)
{ {
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_groupRepository = groupRepository; _groupRepository = groupRepository;
_organizationService = organizationService; _organizationService = organizationService;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
} }
/// <summary> /// <summary>
@ -183,7 +187,7 @@ public class MembersController : Controller
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
await _organizationService.UpdateUserGroupsAsync(existingUser, model.GroupIds, null); await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(existingUser, model.GroupIds, null);
return new OkResult(); return new OkResult();
} }

View File

@ -28,10 +28,6 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
var userService = _serviceProvider.GetRequiredService<IUserService>(); var userService = _serviceProvider.GetRequiredService<IUserService>();
if (!(await userService.CanAccessPremium(user)))
{
return false;
}
var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
if (!HasProperMetaData(webAuthnProvider)) if (!HasProperMetaData(webAuthnProvider))
@ -45,10 +41,6 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
{ {
var userService = _serviceProvider.GetRequiredService<IUserService>(); var userService = _serviceProvider.GetRequiredService<IUserService>();
if (!(await userService.CanAccessPremium(user)))
{
return null;
}
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
var keys = LoadKeys(provider); var keys = LoadKeys(provider);
@ -81,7 +73,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user) public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
{ {
var userService = _serviceProvider.GetRequiredService<IUserService>(); var userService = _serviceProvider.GetRequiredService<IUserService>();
if (!(await userService.CanAccessPremium(user)) || string.IsNullOrWhiteSpace(token)) if (string.IsNullOrWhiteSpace(token))
{ {
return false; return false;
} }

View File

@ -57,7 +57,6 @@ public class TwoFactorProvider
case TwoFactorProviderType.Duo: case TwoFactorProviderType.Duo:
case TwoFactorProviderType.YubiKey: case TwoFactorProviderType.YubiKey:
case TwoFactorProviderType.U2f: // Keep to ensure old U2f keys are considered premium case TwoFactorProviderType.U2f: // Keep to ensure old U2f keys are considered premium
case TwoFactorProviderType.WebAuthn:
return true; return true;
default: default:
return false; return false;

View File

@ -46,4 +46,13 @@ public static class FeatureFlagKeys
.Select(x => (string)x.GetRawConstantValue()) .Select(x => (string)x.GetRawConstantValue())
.ToList(); .ToList();
} }
public static Dictionary<string, string> GetLocalOverrideFlagValues()
{
// place overriding values when needed locally (offline), or return null
return new Dictionary<string, string>()
{
{ TrustedDeviceEncryption, "true" }
};
}
} }

View File

@ -45,6 +45,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationLicenseCommandsQueries();
services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationAuthCommands(); services.AddOrganizationAuthCommands();
services.AddOrganizationUserCommands();
services.AddOrganizationUserCommandsQueries(); services.AddOrganizationUserCommandsQueries();
services.AddBaseOrganizationSubscriptionCommandsQueries(); services.AddBaseOrganizationSubscriptionCommandsQueries();
} }
@ -81,6 +82,12 @@ public static class OrganizationServiceCollectionExtensions
} }
} }
private static void AddOrganizationUserCommands(this IServiceCollection services)
{
services.AddScoped<IDeleteOrganizationUserCommand, DeleteOrganizationUserCommand>();
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
}
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services) private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
{ {
services.AddScoped<IGetOrganizationApiKeyQuery, GetOrganizationApiKeyQuery>(); services.AddScoped<IGetOrganizationApiKeyQuery, GetOrganizationApiKeyQuery>();

View File

@ -0,0 +1,8 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IUpdateOrganizationUserGroupsCommand
{
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
}

View File

@ -0,0 +1,34 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers;
public class UpdateOrganizationUserGroupsCommand : IUpdateOrganizationUserGroupsCommand
{
private readonly IEventService _eventService;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository;
public UpdateOrganizationUserGroupsCommand(
IEventService eventService,
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository)
{
_eventService = eventService;
_organizationService = organizationService;
_organizationUserRepository = organizationUserRepository;
}
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await _organizationService.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
}

View File

@ -54,7 +54,6 @@ public interface IOrganizationService
Task DeleteUserAsync(Guid organizationId, Guid userId); Task DeleteUserAsync(Guid organizationId, Guid userId);
Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId, Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId); IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable<ImportedGroup> groups, Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable<ImportedGroup> groups,
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds, IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
@ -82,4 +81,6 @@ public interface IOrganizationService
void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade);
Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,
OrganizationUserType? oldType, Permissions permissions);
} }

View File

@ -29,24 +29,12 @@ public class LaunchDarklyFeatureService : IFeatureService, IDisposable
// support configuration directly from settings // support configuration directly from settings
else if (globalSettings.LaunchDarkly?.FlagValues?.Any() is true) else if (globalSettings.LaunchDarkly?.FlagValues?.Any() is true)
{ {
var source = TestData.DataSource(); ldConfig.DataSource(BuildDataSource(globalSettings.LaunchDarkly.FlagValues));
foreach (var kvp in globalSettings.LaunchDarkly.FlagValues) }
{ // support local overrides
if (bool.TryParse(kvp.Value, out bool boolValue)) else if (FeatureFlagKeys.GetLocalOverrideFlagValues()?.Any() is true)
{ {
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(boolValue))); ldConfig.DataSource(BuildDataSource(FeatureFlagKeys.GetLocalOverrideFlagValues()));
}
else if (int.TryParse(kvp.Value, out int intValue))
{
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(intValue)));
}
else
{
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(kvp.Value)));
}
}
ldConfig.DataSource(source);
} }
else else
{ {
@ -187,4 +175,26 @@ public class LaunchDarklyFeatureService : IFeatureService, IDisposable
return builder.Build(); return builder.Build();
} }
private TestData BuildDataSource(Dictionary<string, string> values)
{
var source = TestData.DataSource();
foreach (var kvp in values)
{
if (bool.TryParse(kvp.Value, out bool boolValue))
{
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(boolValue)));
}
else if (int.TryParse(kvp.Value, out int intValue))
{
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(intValue)));
}
else
{
source.Update(source.Flag(kvp.Key).ValueForAll(LaunchDarkly.Sdk.LdValue.Of(kvp.Value)));
}
}
return source;
}
} }

View File

@ -1582,17 +1582,6 @@ public class OrganizationService : IOrganizationService
return hasOtherOwner; return hasOtherOwner;
} }
public async Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId)
{
if (loggedInUserId.HasValue)
{
await ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
}
await _organizationUserRepository.UpdateGroupsAsync(organizationUser.Id, groupIds);
await _eventService.LogOrganizationUserEventAsync(organizationUser,
EventType.OrganizationUser_UpdatedGroups);
}
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId) public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
{ {
// Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID // Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID
@ -2032,7 +2021,7 @@ public class OrganizationService : IOrganizationService
} }
} }
private async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions) public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions)
{ {
if (await _currentContext.OrganizationOwner(organizationId)) if (await _currentContext.OrganizationOwner(organizationId))
{ {

View File

@ -0,0 +1,50 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class UpdateOrganizationUserGroupsCommandTests
{
[Theory, BitAutoData]
public async Task UpdateUserGroups_Passes(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, null);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs()
.ValidateOrganizationUserUpdatePermissions(default, default, default, default);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
[Theory, BitAutoData]
public async Task UpdateUserGroups_WithSavingUserId_Passes(
OrganizationUser organizationUser,
IEnumerable<Guid> groupIds,
Guid savingUserId,
SutProvider<UpdateOrganizationUserGroupsCommand> sutProvider)
{
organizationUser.Permissions = null;
await sutProvider.Sut.UpdateUserGroupsAsync(organizationUser, groupIds, savingUserId);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.ValidateOrganizationUserUpdatePermissions(organizationUser.OrganizationId, organizationUser.Type, null, organizationUser.GetPermissions());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1)
.UpdateGroupsAsync(organizationUser.Id, groupIds);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
}

View File

@ -20,14 +20,6 @@ public class LaunchDarklyFeatureServiceTests
.Create(); .Create();
} }
[Fact]
public void Offline_WhenSelfHost()
{
var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings() { SelfHosted = true });
Assert.False(sutProvider.Sut.IsOnline());
}
[Theory, BitAutoData] [Theory, BitAutoData]
public void DefaultFeatureValue_WhenSelfHost(string key) public void DefaultFeatureValue_WhenSelfHost(string key)
{ {

View File

@ -1832,4 +1832,74 @@ public class OrganizationServiceTests
sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup); sutProvider.Sut.ValidateSecretsManagerPlan(plan, signup);
} }
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Owner,
InvitorUserType = OrganizationUserType.Admin
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithAdminAddingOwner_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("only an owner can configure another owner's account.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Admin,
InvitorUserType = OrganizationUserType.Owner
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithoutManageUsersPermission_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("your account does not have permission to manage users.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Admin,
InvitorUserType = OrganizationUserType.Custom
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingAdmin_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, organizationUserInvite.Permissions));
Assert.Contains("custom users can not manage admins or owners.", exception.Message.ToLowerInvariant());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.Custom,
InvitorUserType = OrganizationUserType.Custom
), BitAutoData]
public async Task ValidateOrganizationUserUpdatePermissions_WithCustomAddingUser_WithoutPermissions_Throws(
Guid organizationId,
OrganizationUserInvite organizationUserInvite,
SutProvider<OrganizationService> sutProvider)
{
var invitePermissions = new Permissions { AccessReports = true };
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessReports(organizationId).Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(organizationId, organizationUserInvite.Type.Value, null, invitePermissions));
Assert.Contains("custom users can only grant the same custom permissions that they have.", exception.Message.ToLowerInvariant());
}
} }