mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[AC-292] Public Api - allow configuration of custom permissions (#4022)
* Also refactor OrganizationService user invite methods
This commit is contained in:
@ -0,0 +1,254 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Public.Models;
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Public.Controllers;
|
||||
|
||||
public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
private Organization _organization;
|
||||
private string _ownerEmail;
|
||||
|
||||
public MembersControllerTests(ApiApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
// Create the owner account
|
||||
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
|
||||
// Create the organization
|
||||
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
// Authorize with the organization api key
|
||||
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_Member_Success()
|
||||
{
|
||||
var (userEmail1, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Custom, new Permissions { AccessImportExport = true, ManagePolicies = true, AccessReports = true });
|
||||
var (userEmail2, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Owner);
|
||||
var (userEmail3, orgUser3) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.User);
|
||||
var (userEmail4, orgUser4) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Admin);
|
||||
|
||||
var response = await _client.GetAsync($"/public/members");
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<MemberResponseModel>>();
|
||||
Assert.NotNull(result?.Data);
|
||||
Assert.Equal(5, result.Data.Count());
|
||||
|
||||
// The owner
|
||||
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||
m.Email == _ownerEmail && m.Type == OrganizationUserType.Owner));
|
||||
|
||||
// The custom user
|
||||
var user1Result = result.Data.SingleOrDefault(m => m.Email == userEmail1);
|
||||
Assert.Equal(OrganizationUserType.Custom, user1Result.Type);
|
||||
AssertHelper.AssertPropertyEqual(
|
||||
new PermissionsModel { AccessImportExport = true, ManagePolicies = true, AccessReports = true },
|
||||
user1Result.Permissions);
|
||||
|
||||
// Everyone else
|
||||
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||
m.Email == userEmail2 && m.Type == OrganizationUserType.Owner));
|
||||
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||
m.Email == userEmail3 && m.Type == OrganizationUserType.User));
|
||||
Assert.NotNull(result.Data.SingleOrDefault(m =>
|
||||
m.Email == userEmail4 && m.Type == OrganizationUserType.Admin));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_CustomMember_Success()
|
||||
{
|
||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
|
||||
|
||||
var response = await _client.GetAsync($"/public/members/{orgUser.Id}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(email, result.Email);
|
||||
|
||||
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||
AssertHelper.AssertPropertyEqual(new PermissionsModel { AccessReports = true, ManageScim = true },
|
||||
result.Permissions);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true, true)]
|
||||
[BitAutoData(false, true)]
|
||||
[BitAutoData(true, false)]
|
||||
public async Task Get_CustomMember_WithDeprecatedPermissions_TreatsAsUser(bool editAssignedCollections, bool deleteAssignedCollections)
|
||||
{
|
||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Custom, new Permissions { EditAssignedCollections = editAssignedCollections, DeleteAssignedCollections = deleteAssignedCollections });
|
||||
|
||||
var response = await _client.GetAsync($"/public/members/{orgUser.Id}");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(email, result.Email);
|
||||
|
||||
Assert.Equal(OrganizationUserType.User, result.Type);
|
||||
Assert.Null(result.Permissions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Post_CustomMember_Success()
|
||||
{
|
||||
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
var request = new MemberCreateRequestModel
|
||||
{
|
||||
Email = email,
|
||||
Type = OrganizationUserType.Custom,
|
||||
ExternalId = "myCustomUser",
|
||||
AccessAll = false,
|
||||
Collections = [],
|
||||
Groups = []
|
||||
};
|
||||
|
||||
var response = await _client.PostAsync("/public/members", JsonContent.Create(request));
|
||||
|
||||
// Assert against the response
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(email, result.Email);
|
||||
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||
Assert.Equal("myCustomUser", result.ExternalId);
|
||||
Assert.False(result.AccessAll);
|
||||
Assert.Empty(result.Collections);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var orgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||
|
||||
Assert.Equal(email, orgUser.Email);
|
||||
Assert.Equal(OrganizationUserType.Custom, orgUser.Type);
|
||||
Assert.Equal("myCustomUser", orgUser.ExternalId);
|
||||
Assert.False(orgUser.AccessAll);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, orgUser.Status);
|
||||
Assert.Equal(_organization.Id, orgUser.OrganizationId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_CustomMember_Success()
|
||||
{
|
||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.User);
|
||||
|
||||
var request = new MemberUpdateRequestModel
|
||||
{
|
||||
Type = OrganizationUserType.Custom,
|
||||
Permissions = new PermissionsModel
|
||||
{
|
||||
DeleteAnyCollection = true,
|
||||
EditAnyCollection = true,
|
||||
AccessEventLogs = true
|
||||
},
|
||||
AccessAll = false,
|
||||
ExternalId = "example",
|
||||
Collections = []
|
||||
};
|
||||
|
||||
var response = await _client.PutAsync($"/public/members/{orgUser.Id}", JsonContent.Create(request));
|
||||
|
||||
// Assert against the response
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(email, result.Email);
|
||||
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||
Assert.Equal("example", result.ExternalId);
|
||||
AssertHelper.AssertPropertyEqual(
|
||||
new PermissionsModel { DeleteAnyCollection = true, EditAnyCollection = true, AccessEventLogs = true },
|
||||
result.Permissions);
|
||||
Assert.False(result.AccessAll);
|
||||
Assert.Empty(result.Collections);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||
|
||||
Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);
|
||||
Assert.Equal("example", updatedOrgUser.ExternalId);
|
||||
Assert.False(updatedOrgUser.AccessAll);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, updatedOrgUser.Status);
|
||||
Assert.Equal(_organization.Id, updatedOrgUser.OrganizationId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Permissions property is optional and should not overwrite existing Permissions if not provided.
|
||||
/// This is to preserve backwards compatibility with existing usage.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Put_ExistingCustomMember_NullPermissions_DoesNotOverwritePermissions()
|
||||
{
|
||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Custom, new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true });
|
||||
|
||||
var request = new MemberUpdateRequestModel
|
||||
{
|
||||
Type = OrganizationUserType.Custom,
|
||||
AccessAll = false,
|
||||
ExternalId = "example",
|
||||
Collections = []
|
||||
};
|
||||
|
||||
var response = await _client.PutAsync($"/public/members/{orgUser.Id}", JsonContent.Create(request));
|
||||
|
||||
// Assert against the response
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var result = await response.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(result);
|
||||
|
||||
Assert.Equal(OrganizationUserType.Custom, result.Type);
|
||||
AssertHelper.AssertPropertyEqual(
|
||||
new PermissionsModel { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
|
||||
result.Permissions);
|
||||
|
||||
// Assert against the database values
|
||||
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var updatedOrgUser = await organizationUserRepository.GetByIdAsync(result.Id);
|
||||
|
||||
Assert.Equal(OrganizationUserType.Custom, updatedOrgUser.Type);
|
||||
AssertHelper.AssertPropertyEqual(
|
||||
new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
|
||||
orgUser.GetPermissions());
|
||||
}
|
||||
}
|
@ -73,4 +73,13 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{
|
||||
return await _identityApplicationFactory.TokenFromAccessTokenAsync(clientId, clientSecret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for logging in with an Organization api key.
|
||||
/// Currently used for the Public Api
|
||||
/// </summary>
|
||||
public async Task<string> LoginWithOrganizationApiKeyAsync(string clientId, string clientSecret)
|
||||
{
|
||||
return await _identityApplicationFactory.TokenFromOrganizationApiKeyAsync(clientId, clientSecret);
|
||||
}
|
||||
}
|
||||
|
37
test/Api.IntegrationTest/Helpers/LoginHelper.cs
Normal file
37
test/Api.IntegrationTest/Helpers/LoginHelper.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Helpers;
|
||||
|
||||
public class LoginHelper
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
|
||||
public LoginHelper(ApiApplicationFactory factory, HttpClient client)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task LoginWithOrganizationApiKeyAsync(Guid organizationId)
|
||||
{
|
||||
var (clientId, apiKey) = await GetOrganizationApiKey(_factory, organizationId);
|
||||
var token = await _factory.LoginWithOrganizationApiKeyAsync(clientId, apiKey);
|
||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
_client.DefaultRequestHeaders.Add("client_id", clientId);
|
||||
}
|
||||
|
||||
private async Task<(string clientId, string apiKey)> GetOrganizationApiKey<T>(
|
||||
WebApplicationFactoryBase<T> factory,
|
||||
Guid organizationId)
|
||||
where T : class
|
||||
{
|
||||
var organizationApiKeyRepository = factory.GetService<IOrganizationApiKeyRepository>();
|
||||
var apiKeys = await organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(organizationId);
|
||||
var clientId = $"organization.{organizationId}";
|
||||
return (clientId, apiKeys.SingleOrDefault().ApiKey);
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
@ -15,7 +17,9 @@ public static class OrganizationTestHelpers
|
||||
string ownerEmail = "integration-test@bitwarden.com",
|
||||
string name = "Integration Test Org",
|
||||
string billingEmail = "integration-test@bitwarden.com",
|
||||
string ownerKey = "test-key") where T : class
|
||||
string ownerKey = "test-key",
|
||||
int passwordManagerSeats = 0,
|
||||
PaymentMethodType paymentMethod = PaymentMethodType.None) where T : class
|
||||
{
|
||||
var userRepository = factory.GetService<IUserRepository>();
|
||||
var organizationService = factory.GetService<IOrganizationService>();
|
||||
@ -29,17 +33,23 @@ public static class OrganizationTestHelpers
|
||||
Plan = plan,
|
||||
OwnerKey = ownerKey,
|
||||
Owner = owner,
|
||||
AdditionalSeats = passwordManagerSeats,
|
||||
PaymentMethodType = paymentMethod
|
||||
});
|
||||
|
||||
return new Tuple<Organization, OrganizationUser>(signUpResult.organization, signUpResult.organizationUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OrganizationUser. The user account must already be created.
|
||||
/// </summary>
|
||||
public static async Task<OrganizationUser> CreateUserAsync<T>(
|
||||
WebApplicationFactoryBase<T> factory,
|
||||
Guid organizationId,
|
||||
string userEmail,
|
||||
OrganizationUserType type,
|
||||
bool accessSecretsManager = false
|
||||
bool accessSecretsManager = false,
|
||||
Permissions? permissions = null
|
||||
) where T : class
|
||||
{
|
||||
var userRepository = factory.GetService<IUserRepository>();
|
||||
@ -59,8 +69,36 @@ public static class OrganizationTestHelpers
|
||||
AccessSecretsManager = accessSecretsManager,
|
||||
};
|
||||
|
||||
if (permissions != null)
|
||||
{
|
||||
orgUser.SetPermissions(permissions);
|
||||
}
|
||||
|
||||
await organizationUserRepository.CreateAsync(orgUser);
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new User account with a unique email address and a corresponding OrganizationUser for
|
||||
/// the specified organization.
|
||||
/// </summary>
|
||||
public static async Task<(string, OrganizationUser)> CreateNewUserWithAccountAsync(
|
||||
ApiApplicationFactory factory,
|
||||
Guid organizationId,
|
||||
OrganizationUserType userType,
|
||||
Permissions? permissions = null
|
||||
)
|
||||
{
|
||||
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
|
||||
// Create user
|
||||
await factory.LoginWithNewAccount(email);
|
||||
|
||||
// Create organizationUser
|
||||
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(factory, organizationId, email, userType,
|
||||
permissions: permissions);
|
||||
|
||||
return (email, organizationUser);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user