diff --git a/Directory.Build.props b/Directory.Build.props index f4decd764e..8d5dc85fbe 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net6.0 - 2023.2.1 + 2023.2.2 Bit.$(MSBuildProjectName) true enable diff --git a/bitwarden-server.sln b/bitwarden-server.sln index e9eda387f0..bd0e4648a8 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -278,6 +278,10 @@ Global {07143DFA-F242-47A4-A15E-39C9314D4140}.Debug|Any CPU.Build.0 = Debug|Any CPU {07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.ActiveCfg = Release|Any CPU {07143DFA-F242-47A4-A15E-39C9314D4140}.Release|Any CPU.Build.0 = Release|Any CPU + {D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -322,6 +326,7 @@ Global {B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B} {7E9A7DD5-EB78-4AC5-BFD5-64573FD2533B} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {07143DFA-F242-47A4-A15E-39C9314D4140} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {D9A2CCBB-FB0A-4BBA-A9ED-BA9FF277C880} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs index 61558ad228..a41c551ef4 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/CreateSecretCommand.cs @@ -26,10 +26,15 @@ public class CreateSecretCommand : ICreateSecretCommand var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var project = secret.Projects?.FirstOrDefault(); + if (project == null) + { + throw new NotFoundException(); + } + var hasAccess = accessClient switch { AccessClientType.NoAccessCheck => true, - AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), + AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), _ => false, }; diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs index 583208adce..1d55114442 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/UpdateSecretCommand.cs @@ -31,16 +31,7 @@ public class UpdateSecretCommand : IUpdateSecretCommand var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); - var project = updatedSecret.Projects?.FirstOrDefault(); - - var hasAccess = accessClient switch - { - AccessClientType.NoAccessCheck => true, - AccessClientType.User => project != null && await _projectRepository.UserHasWriteAccessToProject(project.Id, userId), - _ => false, - }; - - if (!hasAccess) + if (!await HasAccessToOriginalAndUpdatedProject(accessClient, secret, updatedSecret, userId)) { throw new NotFoundException(); } @@ -54,4 +45,21 @@ public class UpdateSecretCommand : IUpdateSecretCommand await _secretRepository.UpdateAsync(secret); return secret; } + + public async Task HasAccessToOriginalAndUpdatedProject(AccessClientType accessClient, Secret secret, Secret updatedSecret, Guid userId) + { + switch (accessClient) + { + case AccessClientType.NoAccessCheck: + return true; + case AccessClientType.User: + var oldProject = secret.Projects?.FirstOrDefault(); + var newProject = updatedSecret.Projects?.FirstOrDefault(); + var accessToOld = oldProject != null && await _projectRepository.UserHasWriteAccessToProject(oldProject.Id, userId); + var accessToNew = newProject != null && await _projectRepository.UserHasWriteAccessToProject(newProject.Id, userId); + return accessToOld && accessToNew; + default: + return false; + } + } } diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/DeleteServiceAccountsCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/DeleteServiceAccountsCommand.cs new file mode 100644 index 0000000000..39d340d75d --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/DeleteServiceAccountsCommand.cs @@ -0,0 +1,83 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; + +namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts; + +public class DeleteServiceAccountsCommand : IDeleteServiceAccountsCommand +{ + private readonly IServiceAccountRepository _serviceAccountRepository; + private readonly ICurrentContext _currentContext; + + public DeleteServiceAccountsCommand( + IServiceAccountRepository serviceAccountRepository, + ICurrentContext currentContext) + { + _serviceAccountRepository = serviceAccountRepository; + _currentContext = currentContext; + } + + public async Task>> DeleteServiceAccounts(List ids, Guid userId) + { + if (ids.Any() != true || userId == new Guid()) + { + throw new ArgumentNullException(); + } + + var serviceAccounts = (await _serviceAccountRepository.GetManyByIds(ids))?.ToList(); + + if (serviceAccounts?.Any() != true || serviceAccounts.Count != ids.Count) + { + throw new NotFoundException(); + } + + // Ensure all service accounts belongs to the same organization + var organizationId = serviceAccounts.First().OrganizationId; + if (serviceAccounts.Any(p => p.OrganizationId != organizationId)) + { + throw new BadRequestException(); + } + + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var results = new List>(serviceAccounts.Count); + var deleteIds = new List(); + + foreach (var sa in serviceAccounts) + { + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(sa.Id, userId), + _ => false, + }; + + if (!hasAccess) + { + results.Add(new Tuple(sa, "access denied")); + } + else + { + results.Add(new Tuple(sa, "")); + deleteIds.Add(sa.Id); + } + } + + if (deleteIds.Count > 0) + { + await _serviceAccountRepository.DeleteManyByIdAsync(deleteIds); + } + + return results; + } +} + diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 3f871d8532..7922dd0341 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -28,6 +28,7 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs index b456309051..805922ec18 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs @@ -157,7 +157,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli } } - public async Task> GetManyByGrantedProjectIdAsync(Guid id) + public async Task> GetManyByGrantedProjectIdAsync(Guid id, Guid userId) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -169,11 +169,19 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli .Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User) .Include(ap => ((GroupProjectAccessPolicy)ap).Group) .Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount) + .Select(ap => new + { + ap, + CurrentUserInGroup = ap is GroupProjectAccessPolicy && + ((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g => + g.OrganizationUser.User.Id == userId), + }) .ToListAsync(); - return entities.Select(MapToCore); + + return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); } - public async Task> GetManyByGrantedServiceAccountIdAsync(Guid id) + public async Task> GetManyByGrantedServiceAccountIdAsync(Guid id, Guid userId) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -183,9 +191,16 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id) .Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User) .Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group) + .Select(ap => new + { + ap, + CurrentUserInGroup = ap is GroupServiceAccountAccessPolicy && + ((GroupServiceAccountAccessPolicy)ap).Group.GroupUsers.Any(g => + g.OrganizationUser.User.Id == userId), + }) .ToListAsync(); - return entities.Select(MapToCore); + return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup)); } public async Task DeleteAsync(Guid id) @@ -237,4 +252,26 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli .Map(ap), _ => throw new ArgumentException("Unsupported access policy type"), }; + + private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore( + BaseAccessPolicy baseAccessPolicyEntity, bool currentUserInGroup) + { + switch (baseAccessPolicyEntity) + { + case GroupProjectAccessPolicy ap: + { + var mapped = Mapper.Map(ap); + mapped.CurrentUserInGroup = currentUserInGroup; + return mapped; + } + case GroupServiceAccountAccessPolicy ap: + { + var mapped = Mapper.Map(ap); + mapped.CurrentUserInGroup = currentUserInGroup; + return mapped; + } + default: + return MapToCore(baseAccessPolicyEntity); + } + } } diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs index 5dd560c456..9b2bfd54f0 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs @@ -32,6 +32,16 @@ public class ServiceAccountRepository : Repository>(serviceAccounts); } + public async Task> GetManyByIds(IEnumerable ids) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var serviceAccounts = await dbContext.ServiceAccount + .Where(c => ids.Contains(c.Id)) + .ToListAsync(); + return Mapper.Map>(serviceAccounts); + } + public async Task UserHasReadAccessToServiceAccount(Guid id, Guid userId) { using var scope = ServiceScopeFactory.CreateScope(); @@ -71,6 +81,26 @@ public class ServiceAccountRepository : Repository>(serviceAccounts); } + public async Task DeleteManyByIdAsync(IEnumerable ids) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + // Policies can't have a cascade delete, so we need to delete them manually. + var policies = dbContext.AccessPolicies.Where(ap => + ((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId.HasValue && ids.Contains(((ServiceAccountProjectAccessPolicy)ap).ServiceAccountId!.Value) || + ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId.HasValue && ids.Contains(((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value) || + ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId.HasValue && ids.Contains(((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId!.Value)); + dbContext.RemoveRange(policies); + + var apiKeys = dbContext.ApiKeys.Where(a => a.ServiceAccountId.HasValue && ids.Contains(a.ServiceAccountId!.Value)); + dbContext.RemoveRange(apiKeys); + + var serviceAccounts = dbContext.ServiceAccount.Where(c => ids.Contains(c.Id)); + dbContext.RemoveRange(serviceAccounts); + await dbContext.SaveChangesAsync(); + } + private static Expression> UserHasReadAccessToServiceAccount(Guid userId) => sa => sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) || sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)); diff --git a/bitwarden_license/src/Scim/entrypoint.sh b/bitwarden_license/src/Scim/entrypoint.sh index 0d54517bb8..4d593ef1af 100644 --- a/bitwarden_license/src/Scim/entrypoint.sh +++ b/bitwarden_license/src/Scim/entrypoint.sh @@ -35,7 +35,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Scim.dll diff --git a/bitwarden_license/src/Sso/entrypoint.sh b/bitwarden_license/src/Sso/entrypoint.sh index 675362e6f7..3f6a5eee24 100644 --- a/bitwarden_license/src/Sso/entrypoint.sh +++ b/bitwarden_license/src/Sso/entrypoint.sh @@ -35,10 +35,15 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx +fi + chown -R $USERNAME:$GROUPNAME /app -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Sso.dll diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs index faa6e7ec5e..5fe37c6974 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/UpdateSecretCommandTests.cs @@ -34,6 +34,7 @@ public class UpdateSecretCommandTests public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider sutProvider, Guid userId, Project mockProject) { sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true); + data.Projects = new List() { mockProject }; if (permissionType == PermissionType.RunAsAdmin) { @@ -41,7 +42,6 @@ public class UpdateSecretCommandTests } else { - data.Projects = new List() { mockProject }; sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); sutProvider.GetDependency().UserHasWriteAccessToProject((Guid)(data.Projects?.First().Id), userId).Returns(true); } diff --git a/src/Admin/entrypoint.sh b/src/Admin/entrypoint.sh index 814ae43e96..975460bad0 100644 --- a/src/Admin/entrypoint.sh +++ b/src/Admin/entrypoint.sh @@ -35,7 +35,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Admin.dll diff --git a/src/Api/Controllers/DevicesController.cs b/src/Api/Controllers/DevicesController.cs index cc4ae0f128..a73fe546c4 100644 --- a/src/Api/Controllers/DevicesController.cs +++ b/src/Api/Controllers/DevicesController.cs @@ -4,6 +4,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -130,6 +131,14 @@ public class DevicesController : Controller await _deviceService.DeleteAsync(device); } + [AllowAnonymous] + [HttpGet("knowndevice")] + public async Task GetByIdentifierQuery( + [FromHeader(Name = "X-Request-Email")] string email, + [FromHeader(Name = "X-Device-Identifier")] string deviceIdentifier) + => await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(email), deviceIdentifier); + + [Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")] [AllowAnonymous] [HttpGet("knowndevice/{email}/{identifier}")] public async Task GetByIdentifier(string email, string identifier) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index e4c4003199..eb242a475f 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -5,7 +5,6 @@ using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Api.SecretsManager; -using Bit.Api.Utilities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -225,28 +224,6 @@ public class OrganizationsController : Controller return new OrganizationResponseModel(result.Item1); } - [Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")] - [HttpPost("license")] - [SelfHosted(SelfHostedOnly = true)] - public async Task PostLicense(OrganizationCreateLicenseRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); - if (license == null) - { - throw new BadRequestException("Invalid license"); - } - - var result = await _organizationService.SignUpAsync(license, user, model.Key, - model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); - return new OrganizationResponseModel(result.Item1); - } - [HttpPut("{id}")] [HttpPost("{id}")] public async Task Put(string id, [FromBody] OrganizationUpdateRequestModel model) @@ -447,34 +424,6 @@ public class OrganizationsController : Controller } } - [Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")] - [HttpPost("{id}/license")] - [SelfHosted(SelfHostedOnly = true)] - public async Task PostLicense(string id, LicenseRequestModel model) - { - var orgIdGuid = new Guid(id); - if (!await _currentContext.OrganizationOwner(orgIdGuid)) - { - throw new NotFoundException(); - } - - var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); - if (license == null) - { - throw new BadRequestException("Invalid license"); - } - - var selfHostedOrganizationDetails = await _organizationRepository.GetSelfHostedOrganizationDetailsById(orgIdGuid); - if (selfHostedOrganizationDetails == null) - { - throw new NotFoundException(); - } - - var existingOrganization = await _organizationRepository.GetByLicenseKeyAsync(license.LicenseKey); - - await _updateOrganizationLicenseCommand.UpdateLicenseAsync(selfHostedOrganizationDetails, license, existingOrganization); - } - [HttpPost("{id}/import")] public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model) { diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs index 876a71fa3d..e8a007541f 100644 --- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs +++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs @@ -73,7 +73,7 @@ public class AccessPoliciesController : Controller var (accessClient, userId) = await GetAccessClientTypeAsync(project.OrganizationId); var policies = request.ToBaseAccessPoliciesForProject(id); await _createAccessPoliciesCommand.CreateManyAsync(policies, userId, accessClient); - var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id); + var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id, userId); return new ProjectAccessPoliciesResponseModel(results); } @@ -81,9 +81,8 @@ public class AccessPoliciesController : Controller public async Task GetProjectAccessPoliciesAsync([FromRoute] Guid id) { var project = await _projectRepository.GetByIdAsync(id); - await CheckUserHasWriteAccessToProjectAsync(project); - - var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id); + var (_, userId) = await CheckUserHasWriteAccessToProjectAsync(project); + var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id, userId); return new ProjectAccessPoliciesResponseModel(results); } @@ -106,7 +105,7 @@ public class AccessPoliciesController : Controller var (accessClient, userId) = await GetAccessClientTypeAsync(serviceAccount.OrganizationId); var policies = request.ToBaseAccessPoliciesForServiceAccount(id); await _createAccessPoliciesCommand.CreateManyAsync(policies, userId, accessClient); - var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id); + var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id, userId); return new ServiceAccountAccessPoliciesResponseModel(results); } @@ -115,9 +114,8 @@ public class AccessPoliciesController : Controller [FromRoute] Guid id) { var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); - await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount); - - var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id); + var (_, userId) = await CheckUserHasWriteAccessToServiceAccountAsync(serviceAccount); + var results = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(id, userId); return new ServiceAccountAccessPoliciesResponseModel(results); } @@ -206,7 +204,7 @@ public class AccessPoliciesController : Controller var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var userResponses = organizationUsers - .Where(user => user.AccessSecretsManager) + .Where(user => user.AccessSecretsManager && user.Status == OrganizationUserStatusType.Confirmed) .Select(userDetails => new PotentialGranteeResponseModel(userDetails)); return new ListResponseModel(userResponses.Concat(groupResponses)); @@ -244,7 +242,7 @@ public class AccessPoliciesController : Controller return new ListResponseModel(projectResponses); } - private async Task CheckUserHasWriteAccessToProjectAsync(Project project) + private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToProjectAsync(Project project) { if (project == null) { @@ -263,9 +261,10 @@ public class AccessPoliciesController : Controller { throw new NotFoundException(); } + return (accessClient, userId); } - private async Task CheckUserHasWriteAccessToServiceAccountAsync(ServiceAccount serviceAccount) + private async Task<(AccessClientType AccessClientType, Guid UserId)> CheckUserHasWriteAccessToServiceAccountAsync(ServiceAccount serviceAccount) { if (serviceAccount == null) { @@ -285,6 +284,7 @@ public class AccessPoliciesController : Controller { throw new NotFoundException(); } + return (accessClient, userId); } private async Task<(AccessClientType AccessClientType, Guid UserId)> GetAccessClientTypeAsync(Guid organizationId) diff --git a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs index d54ec3624d..e077d67cbc 100644 --- a/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsManagerPortingController.cs @@ -61,6 +61,11 @@ public class SecretsManagerPortingController : Controller throw new BadRequestException("You cannot import this much data at once, the limit is 1000 projects and 6000 secrets."); } + if (importRequest.Secrets.Any(s => s.ProjectIds.Count() > 1)) + { + throw new BadRequestException("A secret can only be in one project at a time."); + } + await _importCommand.ImportAsync(organizationId, importRequest.ToSMImport()); } } diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs index ccc851c8a2..d610f27521 100644 --- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs +++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs @@ -25,6 +25,7 @@ public class ServiceAccountsController : Controller private readonly ICreateAccessTokenCommand _createAccessTokenCommand; private readonly ICreateServiceAccountCommand _createServiceAccountCommand; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; + private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand; private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand; public ServiceAccountsController( @@ -35,6 +36,7 @@ public class ServiceAccountsController : Controller ICreateAccessTokenCommand createAccessTokenCommand, ICreateServiceAccountCommand createServiceAccountCommand, IUpdateServiceAccountCommand updateServiceAccountCommand, + IDeleteServiceAccountsCommand deleteServiceAccountsCommand, IRevokeAccessTokensCommand revokeAccessTokensCommand) { _currentContext = currentContext; @@ -43,6 +45,7 @@ public class ServiceAccountsController : Controller _apiKeyRepository = apiKeyRepository; _createServiceAccountCommand = createServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand; + _deleteServiceAccountsCommand = deleteServiceAccountsCommand; _revokeAccessTokensCommand = revokeAccessTokensCommand; _createAccessTokenCommand = createAccessTokenCommand; } @@ -67,6 +70,41 @@ public class ServiceAccountsController : Controller return new ListResponseModel(responses); } + [HttpGet("{id}")] + public async Task GetByServiceAccountIdAsync( + [FromRoute] Guid id) + { + var userId = _userService.GetProperUserId(User).Value; + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id); + + if (serviceAccount == null) + { + throw new NotFoundException(); + } + + if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId)) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); + + var hasAccess = accessClient switch + { + AccessClientType.NoAccessCheck => true, + AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(id, userId), + _ => false, + }; + + if (!hasAccess) + { + throw new NotFoundException(); + } + + return new ServiceAccountResponseModel(serviceAccount); + } + [HttpPost("/organizations/{organizationId}/service-accounts")] public async Task CreateAsync([FromRoute] Guid organizationId, [FromBody] ServiceAccountCreateRequestModel createRequest) @@ -90,6 +128,16 @@ public class ServiceAccountsController : Controller return new ServiceAccountResponseModel(result); } + [HttpPost("delete")] + public async Task> BulkDeleteAsync([FromBody] List ids) + { + var userId = _userService.GetProperUserId(User).Value; + + var results = await _deleteServiceAccountsCommand.DeleteServiceAccounts(ids, userId); + var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2)); + return new ListResponseModel(responses); + } + [HttpGet("{id}/access-tokens")] public async Task> GetAccessTokens([FromRoute] Guid id) { diff --git a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs index a298e15b3a..cedaf66f40 100644 --- a/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/AccessPolicyResponseModel.cs @@ -37,6 +37,7 @@ public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMode OrganizationUserId = accessPolicy.OrganizationUserId; GrantedProjectId = accessPolicy.GrantedProjectId; OrganizationUserName = GetUserDisplayName(accessPolicy.User); + UserId = accessPolicy.User?.Id; } public UserProjectAccessPolicyResponseModel() : base(new UserProjectAccessPolicy(), _objectName) @@ -45,6 +46,7 @@ public class UserProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMode public Guid? OrganizationUserId { get; set; } public string? OrganizationUserName { get; set; } + public Guid? UserId { get; set; } public Guid? GrantedProjectId { get; set; } } @@ -58,6 +60,7 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo OrganizationUserId = accessPolicy.OrganizationUserId; GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; OrganizationUserName = GetUserDisplayName(accessPolicy.User); + UserId = accessPolicy.User?.Id; } public UserServiceAccountAccessPolicyResponseModel() : base(new UserServiceAccountAccessPolicy(), _objectName) @@ -66,6 +69,7 @@ public class UserServiceAccountAccessPolicyResponseModel : BaseAccessPolicyRespo public Guid? OrganizationUserId { get; set; } public string? OrganizationUserName { get; set; } + public Guid? UserId { get; set; } public Guid? GrantedServiceAccountId { get; set; } } @@ -79,6 +83,7 @@ public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMod GroupId = accessPolicy.GroupId; GrantedProjectId = accessPolicy.GrantedProjectId; GroupName = accessPolicy.Group?.Name; + CurrentUserInGroup = accessPolicy.CurrentUserInGroup; } public GroupProjectAccessPolicyResponseModel() : base(new GroupProjectAccessPolicy(), _objectName) @@ -87,6 +92,7 @@ public class GroupProjectAccessPolicyResponseModel : BaseAccessPolicyResponseMod public Guid? GroupId { get; set; } public string? GroupName { get; set; } + public bool? CurrentUserInGroup { get; set; } public Guid? GrantedProjectId { get; set; } } @@ -100,6 +106,7 @@ public class GroupServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResp GroupId = accessPolicy.GroupId; GroupName = accessPolicy.Group?.Name; GrantedServiceAccountId = accessPolicy.GrantedServiceAccountId; + CurrentUserInGroup = accessPolicy.CurrentUserInGroup; } public GroupServiceAccountAccessPolicyResponseModel() : base(new GroupServiceAccountAccessPolicy(), _objectName) @@ -109,6 +116,7 @@ public class GroupServiceAccountAccessPolicyResponseModel : BaseAccessPolicyResp public Guid? GroupId { get; set; } public string? GroupName { get; set; } public Guid? GrantedServiceAccountId { get; set; } + public bool? CurrentUserInGroup { get; set; } } public class ServiceAccountProjectAccessPolicyResponseModel : BaseAccessPolicyResponseModel diff --git a/src/Api/entrypoint.sh b/src/Api/entrypoint.sh index 19c30c616c..76c46596f3 100644 --- a/src/Api/entrypoint.sh +++ b/src/Api/entrypoint.sh @@ -35,7 +35,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Api.dll diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index e38a892425..05dbcf0f6e 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -147,7 +147,7 @@ public class FreshdeskController : Controller { var freshdeskAuthkey = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_billingSettings.FreshdeskApiKey}:X")); var httpClient = _httpClientFactory.CreateClient("FreshdeskApi"); - request.Headers.Add("Authorization", freshdeskAuthkey); + request.Headers.Add("Authorization", $"Basic {freshdeskAuthkey}"); var response = await httpClient.SendAsync(request); if (response.StatusCode != System.Net.HttpStatusCode.TooManyRequests || retriedCount > 3) { diff --git a/src/Billing/entrypoint.sh b/src/Billing/entrypoint.sh index 5fb0c5c67a..6d98cfa6f6 100644 --- a/src/Billing/entrypoint.sh +++ b/src/Billing/entrypoint.sh @@ -35,7 +35,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Billing.dll diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3a7b654438..19117f1b79 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -22,3 +22,8 @@ public static class AuthenticationSchemes { public const string BitwardenExternalCookieAuthenticationScheme = "bw.external"; } + +public static class FeatureFlagKeys +{ + public const string SecretsManager = "secrets-manager"; +} diff --git a/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IDeleteServiceAccountsCommand.cs b/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IDeleteServiceAccountsCommand.cs new file mode 100644 index 0000000000..23260b06b8 --- /dev/null +++ b/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/IDeleteServiceAccountsCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; + +public interface IDeleteServiceAccountsCommand +{ + Task>> DeleteServiceAccounts(List ids, Guid userId); +} + diff --git a/src/Core/SecretsManager/Entities/AccessPolicy.cs b/src/Core/SecretsManager/Entities/AccessPolicy.cs index bce9de0629..6de856d8df 100644 --- a/src/Core/SecretsManager/Entities/AccessPolicy.cs +++ b/src/Core/SecretsManager/Entities/AccessPolicy.cs @@ -41,6 +41,7 @@ public class GroupProjectAccessPolicy : BaseAccessPolicy { public Guid? GroupId { get; set; } public Group? Group { get; set; } + public bool? CurrentUserInGroup { get; set; } public Guid? GrantedProjectId { get; set; } public Project? GrantedProject { get; set; } } @@ -49,6 +50,7 @@ public class GroupServiceAccountAccessPolicy : BaseAccessPolicy { public Guid? GroupId { get; set; } public Group? Group { get; set; } + public bool? CurrentUserInGroup { get; set; } public Guid? GrantedServiceAccountId { get; set; } public ServiceAccount? GrantedServiceAccount { get; set; } } diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs index 7241c81eb3..198ea76145 100644 --- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs +++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs @@ -9,8 +9,8 @@ public interface IAccessPolicyRepository Task> CreateManyAsync(List baseAccessPolicies); Task AccessPolicyExists(BaseAccessPolicy baseAccessPolicy); Task GetByIdAsync(Guid id); - Task> GetManyByGrantedProjectIdAsync(Guid id); - Task> GetManyByGrantedServiceAccountIdAsync(Guid id); + Task> GetManyByGrantedProjectIdAsync(Guid id, Guid userId); + Task> GetManyByGrantedServiceAccountIdAsync(Guid id, Guid userId); Task> GetManyByServiceAccountIdAsync(Guid id, Guid userId, AccessClientType accessType); Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy); diff --git a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs index 740d597ecf..194df493fd 100644 --- a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs @@ -7,8 +7,10 @@ public interface IServiceAccountRepository { Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); Task GetByIdAsync(Guid id); + Task> GetManyByIds(IEnumerable ids); Task CreateAsync(ServiceAccount serviceAccount); Task ReplaceAsync(ServiceAccount serviceAccount); + Task DeleteManyByIdAsync(IEnumerable ids); Task UserHasReadAccessToServiceAccount(Guid id, Guid userId); Task UserHasWriteAccessToServiceAccount(Guid id, Guid userId); Task> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType); diff --git a/src/Core/Services/IFeatureService.cs b/src/Core/Services/IFeatureService.cs index 956eab9175..0d8e7a4222 100644 --- a/src/Core/Services/IFeatureService.cs +++ b/src/Core/Services/IFeatureService.cs @@ -1,6 +1,39 @@ -namespace Bit.Core.Services; +using Bit.Core.Context; + +namespace Bit.Core.Services; public interface IFeatureService { + /// + /// Checks whether online access to feature status is available. + /// + /// True if the service is online, otherwise false. bool IsOnline(); + + /// + /// Checks whether a given feature is enabled. + /// + /// The key of the feature to check. + /// A context providing information that can be used to evaluate whether a feature should be on or off. + /// The default value for the feature. + /// True if the feature is enabled, otherwise false. + bool IsEnabled(string key, ICurrentContext currentContext, bool defaultValue = false); + + /// + /// Gets the integer variation of a feature. + /// + /// The key of the feature to check. + /// A context providing information that can be used to evaluate the feature value. + /// The default value for the feature. + /// The feature variation value. + int GetIntVariation(string key, ICurrentContext currentContext, int defaultValue = 0); + + /// + /// Gets the string variation of a feature. + /// + /// The key of the feature to check. + /// A context providing information that can be used to evaluate the feature value. + /// The default value for the feature. + /// The feature variation value. + string GetStringVariation(string key, ICurrentContext currentContext, string defaultValue = null); } diff --git a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs index d69a916817..eeb2e57238 100644 --- a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs +++ b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Settings; +using Bit.Core.Context; +using Bit.Core.Settings; using LaunchDarkly.Sdk.Server; using LaunchDarkly.Sdk.Server.Integrations; @@ -47,8 +48,44 @@ public class LaunchDarklyFeatureService : IFeatureService, IDisposable return _client.Initialized && !_client.IsOffline(); } + public bool IsEnabled(string key, ICurrentContext currentContext, bool defaultValue = false) + { + return _client.BoolVariation(key, BuildContext(currentContext), defaultValue); + } + + public int GetIntVariation(string key, ICurrentContext currentContext, int defaultValue = 0) + { + return _client.IntVariation(key, BuildContext(currentContext), defaultValue); + } + + public string GetStringVariation(string key, ICurrentContext currentContext, string defaultValue = null) + { + return _client.StringVariation(key, BuildContext(currentContext), defaultValue); + } + public void Dispose() { _client?.Dispose(); } + + private LaunchDarkly.Sdk.Context BuildContext(ICurrentContext currentContext) + { + var builder = LaunchDarkly.Sdk.Context.MultiBuilder(); + + if (currentContext.UserId.HasValue) + { + var user = LaunchDarkly.Sdk.Context.Builder(currentContext.UserId.Value.ToString()); + user.Kind(LaunchDarkly.Sdk.ContextKind.Default); + builder.Add(user.Build()); + } + + if (currentContext.OrganizationId.HasValue) + { + var org = LaunchDarkly.Sdk.Context.Builder(currentContext.OrganizationId.Value.ToString()); + org.Kind("org"); + builder.Add(org.Build()); + } + + return builder.Build(); + } } diff --git a/src/Events/entrypoint.sh b/src/Events/entrypoint.sh index 1f382295a2..57cd16c5bc 100644 --- a/src/Events/entrypoint.sh +++ b/src/Events/entrypoint.sh @@ -35,7 +35,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Events.dll diff --git a/src/EventsProcessor/entrypoint.sh b/src/EventsProcessor/entrypoint.sh index 814fcd19f4..0ae7b82cb5 100644 --- a/src/EventsProcessor/entrypoint.sh +++ b/src/EventsProcessor/entrypoint.sh @@ -34,7 +34,9 @@ mkdir -p /etc/bitwarden/logs #mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -#cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ -# && update-ca-certificates +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ + && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/EventsProcessor.dll diff --git a/src/Icons/entrypoint.sh b/src/Icons/entrypoint.sh index 5d7ac5623e..9ed16fba23 100644 --- a/src/Icons/entrypoint.sh +++ b/src/Icons/entrypoint.sh @@ -34,7 +34,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Icons.dll diff --git a/src/Identity/entrypoint.sh b/src/Identity/entrypoint.sh index 491a23cd10..eb96642d36 100644 --- a/src/Identity/entrypoint.sh +++ b/src/Identity/entrypoint.sh @@ -35,10 +35,15 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/identity/identity.pfx /app/identity.pfx +fi + chown -R $USERNAME:$GROUPNAME /app -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Identity.dll diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 755e69264d..ce5b07b1c0 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -186,13 +186,13 @@ public class CollectionRepository : Repository c.Id).Contains(cg.Collection.Id) + from c in collections + join cg in dbContext.CollectionGroups on c.Id equals cg.CollectionId group cg by cg.CollectionId into g select g; var users = - from cu in dbContext.CollectionUsers - where cu.Collection.OrganizationId == organizationId - && collections.Select(c => c.Id).Contains(cu.Collection.Id) + from c in collections + join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId group cu by cu.CollectionId into u select u; - return collections.Select(collection => new Tuple( collection, diff --git a/src/Notifications/entrypoint.sh b/src/Notifications/entrypoint.sh index 84874e2447..e1555b6c50 100644 --- a/src/Notifications/entrypoint.sh +++ b/src/Notifications/entrypoint.sh @@ -34,7 +34,9 @@ mkdir -p /etc/bitwarden/logs mkdir -p /etc/bitwarden/ca-certificates chown -R $USERNAME:$GROUPNAME /etc/bitwarden -cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ +if [[ $globalSettings__selfHosted == "true" ]]; then + cp /etc/bitwarden/ca-certificates/*.crt /usr/local/share/ca-certificates/ >/dev/null 2>&1 \ && update-ca-certificates +fi exec gosu $USERNAME:$GROUPNAME dotnet /app/Notifications.dll diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs index c6e481ebd5..a37567739e 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/AccessPoliciesControllerTest.cs @@ -702,7 +702,6 @@ public class AccessPoliciesControllerTest : IClassFixture result!.UserAccessPolicies.First(ap => ap.OrganizationUserId == ownerOrgUserId).OrganizationUserId); Assert.True(result.UserAccessPolicies.First().Read); Assert.True(result.UserAccessPolicies.First().Write); - AssertHelper.AssertRecent(result.UserAccessPolicies.First().RevisionDate); var createdAccessPolicy = await _accessPolicyRepository.GetByIdAsync(result.UserAccessPolicies.First().Id); @@ -710,7 +709,6 @@ public class AccessPoliciesControllerTest : IClassFixture Assert.Equal(result.UserAccessPolicies.First().Read, createdAccessPolicy!.Read); Assert.Equal(result.UserAccessPolicies.First().Write, createdAccessPolicy.Write); Assert.Equal(result.UserAccessPolicies.First().Id, createdAccessPolicy.Id); - AssertHelper.AssertRecent(createdAccessPolicy.RevisionDate); } [Fact] diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs index 5665bee8a6..d855465e6e 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs @@ -123,12 +123,14 @@ public class ProjectsControllerTest : IClassFixture, IAsy var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); var orgUserId = adminOrgUser.Id; + var currentUserId = adminOrgUser.UserId!.Value; if (permissionType == PermissionType.RunAsUserWithPermission) { var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); await LoginAsync(email); orgUserId = orgUser.Id; + currentUserId = orgUser.UserId!.Value; } var request = new ProjectCreateRequestModel { Name = _mockEncryptedString }; @@ -150,7 +152,7 @@ public class ProjectsControllerTest : IClassFixture, IAsy Assert.Null(createdProject.DeletedDate); // Check permissions have been bootstrapped. - var accessPolicies = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(createdProject.Id); + var accessPolicies = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(createdProject.Id, currentUserId); Assert.NotNull(accessPolicies); var ap = (UserProjectAccessPolicy)accessPolicies.First(); Assert.Equal(createdProject.Id, ap.GrantedProjectId); diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs index 83036a41a6..8bb74bedac 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsControllerTest.cs @@ -148,11 +148,14 @@ public class SecretsControllerTest : IClassFixture, IAsyn var (org, _) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var project = await _projectRepository.CreateAsync(new Project { Name = "123" }); + var request = new SecretCreateRequestModel { + ProjectIds = new Guid[] { project.Id }, Key = _mockEncryptedString, Value = _mockEncryptedString, - Note = _mockEncryptedString + Note = _mockEncryptedString, }; var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request); diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs index 32cae25037..bf7a837043 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ServiceAccountsControllerTests.cs @@ -138,7 +138,7 @@ public class ServiceAccountsControllerTest : IClassFixture { initialServiceAccount.Id }; + + var response = await _client.PutAsJsonAsync("/service-accounts/delete", request); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Delete_MissingAccessPolicy_AccessDenied() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + var ids = new List { serviceAccount.Id }; + + var response = await _client.PostAsJsonAsync("/service-accounts/delete", ids); + + var results = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(results); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Delete_Success(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + await _apiKeyRepository.CreateAsync(new ApiKey { ServiceAccountId = serviceAccount.Id }); + + if (permissionType == PermissionType.RunAsAdmin) + { + await LoginAsync(_email); + } + else + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + await _accessPolicyRepository.CreateManyAsync(new List { + new UserServiceAccountAccessPolicy + { + GrantedServiceAccountId = serviceAccount.Id, + OrganizationUserId = orgUser.Id, + Write = true, + Read = true, + }, + }); + } + + var ids = new List { serviceAccount.Id }; + + var response = await _client.PostAsJsonAsync("/service-accounts/delete", ids); + response.EnsureSuccessStatusCode(); + + var sa = await _serviceAccountRepository.GetManyByIds(ids); + Assert.Empty(sa); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)] diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 9a3757fb36..b1855d569f 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -113,7 +113,7 @@ public class AccessPoliciesControllerTests var result = await sutProvider.Sut.GetProjectAccessPoliciesAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); Assert.Empty(result.GroupAccessPolicies); Assert.Empty(result.UserAccessPolicies); @@ -135,7 +135,7 @@ public class AccessPoliciesControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectAccessPoliciesAsync(id)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedProjectIdAsync(Arg.Any()); + .GetManyByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -161,13 +161,13 @@ public class AccessPoliciesControllerTests break; } - sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default) + sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default, default) .ReturnsForAnyArgs(new List { resultAccessPolicy }); var result = await sutProvider.Sut.GetProjectAccessPoliciesAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByGrantedProjectIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); Assert.Empty(result.GroupAccessPolicies); Assert.NotEmpty(result.UserAccessPolicies); @@ -187,13 +187,13 @@ public class AccessPoliciesControllerTests sutProvider.GetDependency().UserHasWriteAccessToProject(default, default) .ReturnsForAnyArgs(false); - sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default) + sutProvider.GetDependency().GetManyByGrantedProjectIdAsync(default, default) .ReturnsForAnyArgs(new List { resultAccessPolicy }); await Assert.ThrowsAsync(() => sutProvider.Sut.GetProjectAccessPoliciesAsync(id)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedProjectIdAsync(Arg.Any()); + .GetManyByGrantedProjectIdAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -222,7 +222,7 @@ public class AccessPoliciesControllerTests var result = await sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); Assert.Empty(result.UserAccessPolicies); Assert.Empty(result.GroupAccessPolicies); @@ -243,7 +243,7 @@ public class AccessPoliciesControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedServiceAccountIdAsync(Arg.Any()); + .GetManyByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); } [Theory] @@ -270,13 +270,13 @@ public class AccessPoliciesControllerTests break; } - sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default) + sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default, default) .ReturnsForAnyArgs(new List { resultAccessPolicy }); var result = await sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByGrantedServiceAccountIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any()); Assert.Empty(result.GroupAccessPolicies); Assert.NotEmpty(result.UserAccessPolicies); @@ -295,13 +295,13 @@ public class AccessPoliciesControllerTests sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(default, default) .ReturnsForAnyArgs(false); - sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default) + sutProvider.GetDependency().GetManyByGrantedServiceAccountIdAsync(default, default) .ReturnsForAnyArgs(new List { resultAccessPolicy }); await Assert.ThrowsAsync(() => sutProvider.Sut.GetServiceAccountAccessPoliciesAsync(id)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetManyByGrantedServiceAccountIdAsync(Arg.Any()); + .GetManyByGrantedServiceAccountIdAsync(Arg.Any(), Arg.Any()); } [Theory] diff --git a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs index 1491f41aab..efc84bd784 100644 --- a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs +++ b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs @@ -1,8 +1,10 @@ using AutoFixture; +using Bit.Core.Context; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; using Xunit; namespace Bit.Core.Test.Services; @@ -25,4 +27,68 @@ public class LaunchDarklyFeatureServiceTests Assert.False(sutProvider.Sut.IsOnline()); } + + [Theory, BitAutoData] + public void DefaultFeatureValue_WhenSelfHost(string key) + { + var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings() { SelfHosted = true }); + + var currentContext = Substitute.For(); + currentContext.UserId.Returns(Guid.NewGuid()); + + Assert.False(sutProvider.Sut.IsEnabled(key, currentContext)); + } + + [Fact] + public void DefaultFeatureValue_NoSdkKey() + { + var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings()); + + var currentContext = Substitute.For(); + currentContext.UserId.Returns(Guid.NewGuid()); + + Assert.False(sutProvider.Sut.IsEnabled(FeatureFlagKeys.SecretsManager, currentContext)); + } + + [Fact(Skip = "For local development")] + public void FeatureValue_Boolean() + { + var settings = new Core.Settings.GlobalSettings(); + settings.LaunchDarkly.SdkKey = "somevalue"; + + var sutProvider = GetSutProvider(settings); + + var currentContext = Substitute.For(); + currentContext.UserId.Returns(Guid.NewGuid()); + + Assert.False(sutProvider.Sut.IsEnabled(FeatureFlagKeys.SecretsManager, currentContext)); + } + + [Fact(Skip = "For local development")] + public void FeatureValue_Int() + { + var settings = new Core.Settings.GlobalSettings(); + settings.LaunchDarkly.SdkKey = "somevalue"; + + var sutProvider = GetSutProvider(settings); + + var currentContext = Substitute.For(); + currentContext.UserId.Returns(Guid.NewGuid()); + + Assert.Equal(0, sutProvider.Sut.GetIntVariation(FeatureFlagKeys.SecretsManager, currentContext)); + } + + [Fact(Skip = "For local development")] + public void FeatureValue_String() + { + var settings = new Core.Settings.GlobalSettings(); + settings.LaunchDarkly.SdkKey = "somevalue"; + + var sutProvider = GetSutProvider(settings); + + var currentContext = Substitute.For(); + currentContext.UserId.Returns(Guid.NewGuid()); + + Assert.Null(sutProvider.Sut.GetStringVariation(FeatureFlagKeys.SecretsManager, currentContext)); + } } diff --git a/util/MsSqlMigratorUtility/packages.lock.json b/util/MsSqlMigratorUtility/packages.lock.json index 75580714cb..3ee9a8fc95 100644 --- a/util/MsSqlMigratorUtility/packages.lock.json +++ b/util/MsSqlMigratorUtility/packages.lock.json @@ -260,6 +260,58 @@ "IdentityModel": "4.4.0" } }, + "LaunchDarkly.Cache": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "0bEnUVFVeW1TTDXb/bW6kS3FLQTLeGtw7Xh8yt6WNO56utVmtgcrMLvcnF6yeTn+N4FXrKfW09KkLNmK8YYQvw==" + }, + "LaunchDarkly.CommonSdk": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "YYYq+41gZRMQ8dIoMC6HOq/dI+4RY3HsexLLAaE9T1+1tVMeQkbCqak7sVeKX4QcE7xlXx23lWgipYUkRoRUyw==", + "dependencies": { + "System.Collections.Immutable": "1.7.1" + } + }, + "LaunchDarkly.EventSource": { + "type": "Transitive", + "resolved": "5.0.1", + "contentHash": "DN44Ry5M4lyrjiF7LEu0Ijco7Wm8R7mJopN+giYsYjkQlszsXdFvm3POoehIDAOtL1HHl5bZvF9k9xK034u3IA==", + "dependencies": { + "LaunchDarkly.Logging": "[1.0.1, 3.0.0)" + } + }, + "LaunchDarkly.InternalSdk": { + "type": "Transitive", + "resolved": "3.1.0", + "contentHash": "jW8VEfFciuCcJUEuvSzmrbMVYYXwGL/ZWHUZLiA4aDOQ1LcEXp32uK405NQW/izEypUfWB+9TaSjPpFIC+5Wzw==", + "dependencies": { + "LaunchDarkly.CommonSdk": "6.0.0", + "LaunchDarkly.Logging": "[2.0.0, 3.0.0)", + "System.Collections.Immutable": "1.7.1" + } + }, + "LaunchDarkly.Logging": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "lsLKNqAZ7HIlkdTIrf4FetfRA1SUDE3WlaZQn79aSVkLjYWEhUhkDDK7hORGh4JoA3V2gXN+cIvJQax2uR/ijA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0" + } + }, + "LaunchDarkly.ServerSdk": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "gkTWb+f5QlsXIqFAciBef3qKZU2y0Hy3Fpt4pvZoxNcnBKg2PNTDSnbpbYEKPeQ1yk1avNaI/tKprnahfrmJFg==", + "dependencies": { + "LaunchDarkly.Cache": "1.0.2", + "LaunchDarkly.CommonSdk": "6.0.0", + "LaunchDarkly.EventSource": "5.0.1", + "LaunchDarkly.InternalSdk": "3.1.0", + "LaunchDarkly.Logging": "2.0.0", + "System.Collections.Immutable": "1.7.1" + } + }, "libsodium": { "type": "Transitive", "resolved": "1.0.18.2", @@ -1325,8 +1377,8 @@ }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "RVSM6wZUo6L2y6P3vN6gjUtyJ2IF2RVtrepF3J7nrDKfFQd5u/SnSUFclchYQis8/k5scHy9E+fVeKVQLnnkzw==" + "resolved": "1.7.1", + "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" }, "System.Collections.NonGeneric": { "type": "Transitive", @@ -2686,6 +2738,7 @@ "Handlebars.Net": "[2.1.2, )", "IdentityServer4": "[4.1.2, )", "IdentityServer4.AccessTokenValidation": "[3.0.1, )", + "LaunchDarkly.ServerSdk": "[7.0.0, )", "MailKit": "[3.2.0, )", "Microsoft.AspNetCore.Authentication.JwtBearer": "[6.0.4, )", "Microsoft.Azure.Cosmos.Table": "[1.0.8, )",