1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 08:32:50 -05:00

Merge branch 'master' into feature/billing-obfuscation

This commit is contained in:
Rui Tome
2023-02-03 10:15:41 +00:00
154 changed files with 17081 additions and 1370 deletions

View File

@ -38,7 +38,7 @@ jobs:
testing:
name: Testing
runs-on: windows-2022
runs-on: ubuntu-22.04
env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
@ -46,13 +46,10 @@ jobs:
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
with:
dotnet-version: "6.0.x"
- name: Set up MSBuild
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
- name: Print environment
run: |
dotnet --info
msbuild -version
nuget help | grep Version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
@ -64,20 +61,23 @@ jobs:
run: dotnet restore --locked-mode
shell: pwsh
- name: Remove SQL proj
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
- name: Build OSS solution
run: msbuild bitwarden-server.sln /p:Configuration=Debug /p:DefineConstants="OSS" /verbosity:minimal
run: dotnet build bitwarden-server.sln -p:Configuration=Debug -p:DefineConstants="OSS" --verbosity minimal
shell: pwsh
- name: Build solution
run: msbuild bitwarden-server.sln /p:Configuration=Debug /verbosity:minimal
run: dotnet build bitwarden-server.sln -p:Configuration=Debug --verbosity minimal
shell: pwsh
- name: Test OSS solution
run: dotnet test ./test --configuration Debug --no-build --logger "trx;LogFileName=oss-test-results.trx" || true
run: dotnet test ./test --configuration Debug --no-build --logger "trx;LogFileName=oss-test-results.trx"
shell: pwsh
- name: Test Bitwarden solution
run: dotnet test ./bitwarden_license/test --configuration Debug --no-build --logger "trx;LogFileName=bw-test-results.trx" || true
run: dotnet test ./bitwarden_license/test --configuration Debug --no-build --logger "trx;LogFileName=bw-test-results.trx"
shell: pwsh
- name: Report test results
@ -249,6 +249,14 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Set up image tag
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
########## Build Docker Image ##########
- name: Setup project name
@ -277,28 +285,44 @@ jobs:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
run: docker build -t $PROJECT_NAME ${{ matrix.base_path }}/${{ matrix.project_name }}
########## ACR ##########
########## QA ACR ##########
- name: Login to Azure - QA Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
- name: Login to QA ACR
run: az acr login -n bitwardenqa
- name: Tag and Push image to Azure ACR QA registry
- name: Tag and push image to QA ACR
env:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
REGISTRY: bitwardenqa.azurecr.io
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
docker tag $PROJECT_NAME \
$REGISTRY/$PROJECT_NAME:$IMAGE_TAG
docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG
$REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
docker push $REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
- name: Log out of Docker
run: docker logout
########## PROD ACR ##########
- name: Login to Azure - PROD Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Login to PROD ACR
run: az acr login -n bitwardenprod
- name: Tag and push image to PROD ACR
env:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
REGISTRY: bitwardenprod.azurecr.io
run: |
docker tag $PROJECT_NAME \
$REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
docker push $REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
- name: Log out of Docker
run: docker logout
@ -366,14 +390,9 @@ jobs:
PROJECT_NAME: ${{ steps.setup.outputs.project_name }}
REGISTRY: bitwarden
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
docker tag $PROJECT_NAME \
$REGISTRY/$PROJECT_NAME:$IMAGE_TAG
docker push $REGISTRY/$PROJECT_NAME:$IMAGE_TAG
$REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
docker push $REGISTRY/$PROJECT_NAME:${{ env.IMAGE_TAG }}
- name: Log out of Docker and disable Docker Notary
if: |

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Full Server - Self-hosted" type="CompoundRunConfigurationType">
<toRun name="Admin: Admin-SelfHost" type="LaunchSettings" />
<toRun name="Api: Api-SelfHost" type="LaunchSettings" />
<toRun name="Events: Events-SelfHost" type="LaunchSettings" />
<toRun name="Identity: Identity-SelfHost" type="LaunchSettings" />
<toRun name="Notifications: Notifications-SelfHost" type="LaunchSettings" />
<toRun name="Sso: Sso-SelfHost" type="LaunchSettings" />
<method v="2" />
</configuration>
</component>

14
.run/Full Server.run.xml Normal file
View File

@ -0,0 +1,14 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Full Server" type="CompoundRunConfigurationType">
<toRun name="Admin" type="LaunchSettings" />
<toRun name="Api" type="LaunchSettings" />
<toRun name="Billing" type="LaunchSettings" />
<toRun name="Events" type="LaunchSettings" />
<toRun name="EventsProcessor" type="LaunchSettings" />
<toRun name="Icons" type="LaunchSettings" />
<toRun name="Identity" type="LaunchSettings" />
<toRun name="Notifications" type="LaunchSettings" />
<toRun name="Sso" type="LaunchSettings" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Min Server - Self-hosted" type="CompoundRunConfigurationType">
<toRun name="Api: Api-SelfHost" type="LaunchSettings" />
<toRun name="Identity: Identity-SelfHost" type="LaunchSettings" />
<method v="2" />
</configuration>
</component>

7
.run/Min Server.run.xml Normal file
View File

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Min Server" type="CompoundRunConfigurationType">
<toRun name="Api" type="LaunchSettings" />
<toRun name="Identity" type="LaunchSettings" />
<method v="2" />
</configuration>
</component>

View File

@ -33,6 +33,12 @@ public class CreateAccessTokenCommand : ICreateAccessTokenCommand
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(apiKey.ServiceAccountId.Value);
if (!_currentContext.AccessSecretsManager(serviceAccount.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -46,7 +52,7 @@ public class CreateAccessTokenCommand : ICreateAccessTokenCommand
if (!hasAccess)
{
throw new UnauthorizedAccessException();
throw new NotFoundException();
}
apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);

View File

@ -36,7 +36,12 @@ public class DeleteProjectCommand : IDeleteProjectCommand
var organizationId = projects.First().OrganizationId;
if (projects.Any(p => p.OrganizationId != organizationId))
{
throw new UnauthorizedAccessException();
throw new BadRequestException();
}
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);

View File

@ -26,6 +26,11 @@ public class UpdateProjectCommand : IUpdateProjectCommand
throw new NotFoundException();
}
if (!_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -38,7 +43,7 @@ public class UpdateProjectCommand : IUpdateProjectCommand
if (!hasAccess)
{
throw new UnauthorizedAccessException();
throw new NotFoundException();
}
project.Name = updatedProject.Name;

View File

@ -1,4 +1,5 @@
using Bit.Core.Exceptions;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -7,10 +8,12 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Secrets;
public class DeleteSecretCommand : IDeleteSecretCommand
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
public DeleteSecretCommand(ISecretRepository secretRepository)
public DeleteSecretCommand(ICurrentContext currentContext, ISecretRepository secretRepository)
{
_currentContext = currentContext;
_secretRepository = secretRepository;
}
@ -23,6 +26,18 @@ public class DeleteSecretCommand : IDeleteSecretCommand
throw new NotFoundException();
}
// Ensure all secrets belongs to the same organization
var organizationId = secrets.First().OrganizationId;
if (secrets.Any(p => p.OrganizationId != organizationId))
{
throw new BadRequestException();
}
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var results = ids.Select(id =>
{
var secret = secrets.FirstOrDefault(secret => secret.Id == id);

View File

@ -1,4 +1,5 @@
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -6,15 +7,34 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
public class CreateServiceAccountCommand : ICreateServiceAccountCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public CreateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository)
public CreateServiceAccountCommand(
IAccessPolicyRepository accessPolicyRepository,
IOrganizationUserRepository organizationUserRepository,
IServiceAccountRepository serviceAccountRepository)
{
_accessPolicyRepository = accessPolicyRepository;
_organizationUserRepository = organizationUserRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount)
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
{
return await _serviceAccountRepository.CreateAsync(serviceAccount);
var createdServiceAccount = await _serviceAccountRepository.CreateAsync(serviceAccount);
var user = await _organizationUserRepository.GetByOrganizationAsync(createdServiceAccount.OrganizationId,
userId);
var accessPolicy = new UserServiceAccountAccessPolicy
{
OrganizationUserId = user.Id,
GrantedServiceAccountId = createdServiceAccount.Id,
Read = true,
Write = true,
};
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
return createdServiceAccount;
}
}

View File

@ -26,6 +26,11 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
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);
@ -38,7 +43,7 @@ public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
if (!hasAccess)
{
throw new UnauthorizedAccessException();
throw new NotFoundException();
}
serviceAccount.Name = updatedServiceAccount.Name;

View File

@ -138,7 +138,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>?> GetManyByProjectId(Guid id)
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -153,10 +153,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.ToListAsync();
return !entities.Any() ? null : entities.Select(MapToCore);
return entities.Select(MapToCore);
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedServiceAccountIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.AccessPolicies.Where(ap =>
((UserServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id ||
((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccountId == id)
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
.ToListAsync();
return entities.Select(MapToCore);
}
public async Task DeleteAsync(Guid id)
{
using (var scope = ServiceScopeFactory.CreateScope())
@ -178,6 +193,8 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
UserProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserProjectAccessPolicy>(ap),
GroupProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupProjectAccessPolicy>(ap),
ServiceAccountProjectAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
UserServiceAccountAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.UserServiceAccountAccessPolicy>(ap),
GroupServiceAccountAccessPolicy ap => Mapper.Map<Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy>(ap),
_ => throw new ArgumentException("Unsupported access policy type")
};
}

View File

@ -36,7 +36,7 @@ public class CreateServiceAccountCommandTests
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.CreateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(data, userId));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
@ -49,6 +49,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
@ -64,6 +65,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(saData.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);

View File

@ -28,7 +28,7 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data, Guid userId,
public async Task Delete_OneIdNotFound_Throws_NotFoundException(List<Guid> data, Guid userId,
SutProvider<DeleteProjectCommand> sutProvider)
{
var project = new Project()
@ -49,6 +49,7 @@ public class DeleteProjectCommandTests
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(Arg.Any<Guid>(), userId).Returns(true);
@ -65,11 +66,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_User_No_Permission(List<Guid> data, Guid userId, Guid organizationId,
public async Task Delete_User_No_Permission(List<Guid> data, Guid userId, Guid organizationId,
SutProvider<DeleteProjectCommand> sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(userId, userId).Returns(false);
@ -86,11 +88,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
public async Task DeleteSecrets_OrganizationAdmin_Success(List<Guid> data, Guid userId, Guid organizationId,
public async Task Delete_OrganizationAdmin_Success(List<Guid> data, Guid userId, Guid organizationId,
SutProvider<DeleteProjectCommand> sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);

View File

@ -33,6 +33,7 @@ public class UpdateProjectCommandTests
public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
@ -51,8 +52,9 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(false);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project, userId));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -63,6 +65,7 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId);

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -56,6 +57,7 @@ public class DeleteSecretCommandTests
}
sutProvider.GetDependency<ISecretRepository>().GetManyByIds(data).Returns(secrets);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(default).ReturnsForAnyArgs(true);
var results = await sutProvider.Sut.DeleteSecrets(data);

View File

@ -1,4 +1,6 @@
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
@ -15,9 +17,18 @@ public class CreateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task CreateAsync_CallsCreate(ServiceAccount data,
SutProvider<CreateServiceAccountCommand> sutProvider)
Guid userId,
SutProvider<CreateServiceAccountCommand> sutProvider)
{
await sutProvider.Sut.CreateAsync(data);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
.Returns(new OrganizationUser() { Id = userId });
sutProvider.GetDependency<IServiceAccountRepository>()
.CreateAsync(Arg.Any<ServiceAccount>())
.Returns(data);
await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));

View File

@ -18,7 +18,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -30,7 +30,7 @@ public class UpdateServiceAccountCommandTests
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@ -39,6 +39,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true);
@ -54,6 +55,7 @@ public class UpdateServiceAccountCommandTests
public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
await sutProvider.Sut.UpdateAsync(data, userId);
@ -66,6 +68,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@ -87,6 +90,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@ -108,6 +112,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);

View File

@ -6,7 +6,15 @@
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
param([switch]$all = $false, [switch]$postgres = $false, [switch]$mysql = $false, [switch]$mssql = $false, [switch]$sqlite = $false)
param(
[switch]$all = $false,
[switch]$postgres = $false,
[switch]$mysql = $false,
[switch]$mssql = $false,
[switch]$sqlite = $false,
[switch]$selfhost = $false,
[switch]$pipeline = $false
)
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
$mssql = $true;
@ -21,6 +29,12 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
}
if ($all -or $mssql) {
if ($selfhost) {
$migrationArgs = "-s"
} elseif ($pipeline) {
$migrationArgs = "-p"
}
Write-Host "Starting Microsoft SQL Server Migrations"
docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
@ -30,7 +44,7 @@ if ($all -or $mssql) {
--network=bitwardenserver_default `
--rm `
mcr.microsoft.com/mssql-tools `
/mnt/helpers/run_migrations.sh @args
/mnt/helpers/run_migrations.sh $migrationArgs
}
$currentDir = Get-Location

View File

@ -227,6 +227,7 @@ RUN mkdir -p /var/log/bitwarden
RUN mkdir -p /var/log/nginx/logs
RUN mkdir -p /etc/nginx/http.d
RUN mkdir -p /var/run/nginx
RUN mkdir -p /var/lib/nginx/tmp
RUN touch /var/run/nginx/nginx.pid
RUN mkdir -p /app

View File

@ -57,7 +57,7 @@ server {
include /etc/nginx/security-headers-ssl.conf;
{{/if}}
include /etc/nginx/security-headers.conf;
add_header Content-Security-Policy "{{{String.Coalesce env.BW_CSP "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory; object-src 'self' blob:;"}}}";
add_header Content-Security-Policy "{{{String.Coalesce env.BW_CSP "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; connect-src 'self' https://api.pwnedpasswords.com https://api.2fa.directory; object-src 'self' blob:;"}}}";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Robots-Tag "noindex, nofollow";
}
@ -101,7 +101,7 @@ server {
root /app/Web;
}
location /attachments {
location /attachments/ {
alias /etc/bitwarden/attachments/;
}
{{#if (String.Equal env.BW_ENABLE_API "true")}}

View File

@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Admin.Models;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -18,7 +19,7 @@ public class ToolsController : Controller
{
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly IUserService _userService;
private readonly ITransactionRepository _transactionRepository;
private readonly IInstallationRepository _installationRepository;
@ -30,7 +31,7 @@ public class ToolsController : Controller
public ToolsController(
GlobalSettings globalSettings,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
IUserService userService,
ITransactionRepository transactionRepository,
IInstallationRepository installationRepository,
@ -41,7 +42,7 @@ public class ToolsController : Controller
{
_globalSettings = globalSettings;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_userService = userService;
_transactionRepository = transactionRepository;
_installationRepository = installationRepository;
@ -259,7 +260,7 @@ public class ToolsController : Controller
if (organization != null)
{
var license = await _organizationService.GenerateLicenseAsync(organization,
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization,
model.InstallationId.Value, model.Version);
var ms = new MemoryStream();
await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);

View File

@ -1,6 +1,9 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.OrganizationLicenses;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -14,26 +17,26 @@ namespace Bit.Api.Controllers;
[SelfHosted(NotSelfHostedOnly = true)]
public class LicensesController : Controller
{
private readonly ILicensingService _licensingService;
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
private readonly ICurrentContext _currentContext;
public LicensesController(
ILicensingService licensingService,
IUserRepository userRepository,
IUserService userService,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
ICurrentContext currentContext)
{
_licensingService = licensingService;
_userRepository = userRepository;
_userService = userService;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
_currentContext = currentContext;
}
@ -55,21 +58,30 @@ public class LicensesController : Controller
return license;
}
/// <summary>
/// Used by self-hosted installations to get an updated license file
/// </summary>
[HttpGet("organization/{id}")]
public async Task<OrganizationLicense> GetOrganization(string id, [FromQuery] string key)
public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)
{
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
if (org == null)
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
if (organization == null)
{
return null;
throw new NotFoundException("Organization not found.");
}
else if (!org.LicenseKey.Equals(key))
if (!organization.LicenseKey.Equals(model.LicenseKey))
{
await Task.Delay(2000);
throw new BadRequestException("Invalid license key.");
}
var license = await _organizationService.GenerateLicenseAsync(org, _currentContext.InstallationId.Value);
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))
{
throw new BadRequestException("Invalid Billing Sync Key");
}
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);
return license;
}
}

View File

@ -191,7 +191,7 @@ public class OrganizationConnectionsController : Controller
Guid? organizationConnectionId,
OrganizationConnectionRequestModel model,
Func<OrganizationConnectionRequestModel<T>, Task> validateAction = null)
where T : new()
where T : IConnectionConfig
{
var typedModel = new OrganizationConnectionRequestModel<T>(model);
if (validateAction != null)

View File

@ -5,6 +5,7 @@ using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;

View File

@ -11,6 +11,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -37,6 +38,7 @@ public class OrganizationsController : Controller
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly GlobalSettings _globalSettings;
public OrganizationsController(
@ -53,6 +55,7 @@ public class OrganizationsController : Controller
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
IOrganizationApiKeyRepository organizationApiKeyRepository,
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
GlobalSettings globalSettings)
{
_organizationRepository = organizationRepository;
@ -68,6 +71,7 @@ public class OrganizationsController : Controller
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
_organizationApiKeyRepository = organizationApiKeyRepository;
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
_globalSettings = globalSettings;
}
@ -149,7 +153,8 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
var license = await _organizationService.GenerateLicenseAsync(orgIdGuid, installationId);
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
if (license == null)
{
throw new NotFoundException();
@ -215,6 +220,7 @@ 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<OrganizationResponseModel> PostLicense(OrganizationCreateLicenseRequestModel model)
@ -448,6 +454,7 @@ 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)

View File

@ -0,0 +1,117 @@
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response.Organizations;
using Bit.Api.Utilities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers.SelfHosted;
[Route("organizations/licenses/self-hosted")]
[Authorize("Application")]
[SelfHosted(SelfHostedOnly = true)]
public class SelfHostedOrganizationLicensesController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly ISelfHostedGetOrganizationLicenseQuery _selfHostedGetOrganizationLicenseQuery;
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IUserService _userService;
public SelfHostedOrganizationLicensesController(
ICurrentContext currentContext,
ISelfHostedGetOrganizationLicenseQuery selfHostedGetOrganizationLicenseQuery,
IOrganizationConnectionRepository organizationConnectionRepository,
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
IUserService userService)
{
_currentContext = currentContext;
_selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery;
_organizationConnectionRepository = organizationConnectionRepository;
_organizationService = organizationService;
_organizationRepository = organizationRepository;
_userService = userService;
}
[HttpPost("")]
public async Task<OrganizationResponseModel> PostLicenseAsync(OrganizationCreateLicenseRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(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);
}
[HttpPost("{id}")]
public async Task PostLicenseAsync(string id, LicenseRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))
{
throw new NotFoundException();
}
var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(HttpContext, model.License);
if (license == null)
{
throw new BadRequestException("Invalid license");
}
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
}
[HttpPost("{id}/sync")]
public async Task SyncLicenseAsync(string id)
{
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
if (organization == null)
{
throw new NotFoundException();
}
if (!await _currentContext.OrganizationOwner(organization.Id))
{
throw new NotFoundException();
}
var billingSyncConnection =
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
if (billingSyncConnection == null)
{
throw new NotFoundException("Unable to get Cloud Billing Sync connection");
}
var license =
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(organization, billingSyncConnection);
await _organizationService.UpdateLicenseAsync(organization.Id, license);
var config = billingSyncConnection.GetConfig<BillingSyncConfig>();
config.LastLicenseSync = DateTime.Now;
billingSyncConnection.SetConfig(config);
await _organizationConnectionRepository.ReplaceAsync(billingSyncConnection);
}
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.Utilities;
namespace Bit.Api.Models.Request.Organizations;
@ -17,7 +18,7 @@ public class OrganizationConnectionRequestModel
}
public class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : new()
public class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : IConnectionConfig
{
public T ParsedConfig { get; private set; }

View File

@ -17,6 +17,7 @@ public class OrganizationUserInviteRequestModel
[Required]
public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
@ -28,6 +29,7 @@ public class OrganizationUserInviteRequestModel
Emails = Emails,
Type = Type,
AccessAll = AccessAll,
AccessSecretsManager = AccessSecretsManager,
Collections = Collections?.Select(c => c.ToSelectionReadOnly()),
Groups = Groups,
Permissions = Permissions,
@ -73,6 +75,7 @@ public class OrganizationUserUpdateRequestModel
[Required]
public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<SelectionReadOnlyRequestModel> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
@ -85,6 +88,7 @@ public class OrganizationUserUpdateRequestModel
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
existingUser.AccessAll = AccessAll;
existingUser.AccessSecretsManager = AccessSecretsManager;
return existingUser;
}
}

View File

@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
}
@ -40,6 +41,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
UsesKeyConnector = organizationUser.UsesKeyConnector;
@ -50,6 +52,7 @@ public class OrganizationUserResponseModel : ResponseModel
public OrganizationUserType Type { get; set; }
public OrganizationUserStatusType Status { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; }
public bool ResetPasswordEnrolled { get; set; }
public bool UsesKeyConnector { get; set; }

View File

@ -52,6 +52,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
AccessSecretsManager = organization.AccessSecretsManager;
if (organization.SsoConfig != null)
{
@ -101,4 +102,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
public bool AccessSecretsManager { get; set; }
}

View File

@ -1,5 +1,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.Models.Response;
@ -39,5 +40,6 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName;
PlanProductType = StaticStore.GetPlan(organization.PlanType).Product;
}
}

View File

@ -40,7 +40,7 @@ public class AccessPoliciesController : Controller
[HttpGet("/projects/{id}/access-policies")]
public async Task<ProjectAccessPoliciesResponseModel> GetProjectAccessPoliciesAsync([FromRoute] Guid id)
{
var results = await _accessPolicyRepository.GetManyByProjectId(id);
var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id);
return new ProjectAccessPoliciesResponseModel(results);
}

View File

@ -7,61 +7,46 @@ using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
[Authorize("secrets")]
public class ProjectsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IProjectRepository _projectRepository;
private readonly ICreateProjectCommand _createProjectCommand;
private readonly IUpdateProjectCommand _updateProjectCommand;
private readonly IDeleteProjectCommand _deleteProjectCommand;
private readonly ICurrentContext _currentContext;
public ProjectsController(
ICurrentContext currentContext,
IUserService userService,
IProjectRepository projectRepository,
ICreateProjectCommand createProjectCommand,
IUpdateProjectCommand updateProjectCommand,
IDeleteProjectCommand deleteProjectCommand,
ICurrentContext currentContext)
IDeleteProjectCommand deleteProjectCommand)
{
_currentContext = currentContext;
_userService = userService;
_projectRepository = projectRepository;
_createProjectCommand = createProjectCommand;
_updateProjectCommand = updateProjectCommand;
_deleteProjectCommand = deleteProjectCommand;
_currentContext = currentContext;
}
[HttpPost("organizations/{organizationId}/projects")]
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
[HttpGet("organizations/{organizationId}/projects")]
public async Task<ListResponseModel<ProjectResponseModel>> ListByOrganizationAsync([FromRoute] Guid organizationId)
{
if (!await _currentContext.OrganizationUser(organizationId))
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
return new ProjectResponseModel(result);
}
[HttpPut("projects/{id}")]
public async Task<ProjectResponseModel> UpdateProjectAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest)
{
var userId = _userService.GetProperUserId(User).Value;
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId);
return new ProjectResponseModel(result);
}
[HttpGet("organizations/{organizationId}/projects")]
public async Task<ListResponseModel<ProjectResponseModel>> GetProjectsByOrganizationAsync(
[FromRoute] Guid organizationId)
{
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -72,8 +57,29 @@ public class ProjectsController : Controller
return new ListResponseModel<ProjectResponseModel>(responses);
}
[HttpPost("organizations/{organizationId}/projects")]
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
return new ProjectResponseModel(result);
}
[HttpPut("projects/{id}")]
public async Task<ProjectResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest)
{
var userId = _userService.GetProperUserId(User).Value;
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId);
return new ProjectResponseModel(result);
}
[HttpGet("projects/{id}")]
public async Task<ProjectResponseModel> GetProjectAsync([FromRoute] Guid id)
public async Task<ProjectResponseModel> GetAsync([FromRoute] Guid id)
{
var project = await _projectRepository.GetByIdAsync(id);
if (project == null)
@ -81,6 +87,11 @@ public class ProjectsController : Controller
throw new NotFoundException();
}
if (!_currentContext.AccessSecretsManager(project.OrganizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -101,7 +112,7 @@ public class ProjectsController : Controller
}
[HttpPost("projects/delete")]
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteProjectsAsync([FromBody] List<Guid> ids)
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteAsync([FromBody] List<Guid> ids)
{
var userId = _userService.GetProperUserId(User).Value;

View File

@ -1,6 +1,7 @@
using Bit.Api.Models.Response;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Api.SecretsManager.Models.Response;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
@ -13,30 +14,52 @@ namespace Bit.Api.SecretsManager.Controllers;
[Authorize("secrets")]
public class SecretsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand)
public SecretsController(
ICurrentContext currentContext,
ISecretRepository secretRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand)
{
_currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
}
[HttpGet("organizations/{organizationId}/secrets")]
public async Task<SecretWithProjectsListResponseModel> GetSecretsByOrganizationAsync([FromRoute] Guid organizationId)
public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync([FromRoute] Guid organizationId)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(organizationId);
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPost("organizations/{organizationId}/secrets")]
public async Task<SecretResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId));
return new SecretResponseModel(result);
}
[HttpGet("secrets/{id}")]
public async Task<SecretResponseModel> GetSecretAsync([FromRoute] Guid id)
public async Task<SecretResponseModel> GetAsync([FromRoute] Guid id)
{
var secret = await _secretRepository.GetByIdAsync(id);
if (secret == null)
@ -54,15 +77,8 @@ public class SecretsController : Controller
return new SecretWithProjectsListResponseModel(secrets);
}
[HttpPost("organizations/{organizationId}/secrets")]
public async Task<SecretResponseModel> CreateSecretAsync([FromRoute] Guid organizationId, [FromBody] SecretCreateRequestModel createRequest)
{
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId));
return new SecretResponseModel(result);
}
[HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
public async Task<SecretResponseModel> UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
return new SecretResponseModel(result);

View File

@ -8,43 +8,50 @@ using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.SecretsManager.Controllers;
[SecretsManager]
[Authorize("secrets")]
[Route("service-accounts")]
public class ServiceAccountsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IUserService _userService;
public ServiceAccountsController(
ICurrentContext currentContext,
IUserService userService,
IServiceAccountRepository serviceAccountRepository,
ICreateAccessTokenCommand createAccessTokenCommand,
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand,
ICurrentContext currentContext)
IUpdateServiceAccountCommand updateServiceAccountCommand)
{
_currentContext = currentContext;
_userService = userService;
_serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand;
_createAccessTokenCommand = createAccessTokenCommand;
_currentContext = currentContext;
}
[HttpGet("/organizations/{organizationId}/service-accounts")]
public async Task<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync(
public async Task<ListResponseModel<ServiceAccountResponseModel>> ListByOrganizationAsync(
[FromRoute] Guid organizationId)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
@ -57,20 +64,20 @@ public class ServiceAccountsController : Controller
}
[HttpPost("/organizations/{organizationId}/service-accounts")]
public async Task<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId,
public async Task<ServiceAccountResponseModel> CreateAsync([FromRoute] Guid organizationId,
[FromBody] ServiceAccountCreateRequestModel createRequest)
{
if (!await _currentContext.OrganizationUser(organizationId))
if (!_currentContext.AccessSecretsManager(organizationId))
{
throw new NotFoundException();
}
var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId));
var userId = _userService.GetProperUserId(User).Value;
var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);
return new ServiceAccountResponseModel(result);
}
[HttpPut("{id}")]
public async Task<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id,
public async Task<ServiceAccountResponseModel> UpdateAsync([FromRoute] Guid id,
[FromBody] ServiceAccountUpdateRequestModel updateRequest)
{
var userId = _userService.GetProperUserId(User).Value;
@ -89,6 +96,11 @@ public class ServiceAccountsController : Controller
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);

View File

@ -10,11 +10,6 @@ public class ProjectAccessPoliciesResponseModel : ResponseModel
public ProjectAccessPoliciesResponseModel(IEnumerable<BaseAccessPolicy> baseAccessPolicies)
: base(_objectName)
{
if (baseAccessPolicies == null)
{
return;
}
foreach (var baseAccessPolicy in baseAccessPolicies)
switch (baseAccessPolicy)
{

View File

@ -33,6 +33,9 @@ public class Startup
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Repositories
services.AddDatabaseRepositories(globalSettings);

View File

@ -1,6 +1,6 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Utilities;
namespace Bit.Core.Context;
@ -9,14 +9,16 @@ public class CurrentContentOrganization
{
public CurrentContentOrganization() { }
public CurrentContentOrganization(OrganizationUser orgUser)
public CurrentContentOrganization(OrganizationUserOrganizationDetails orgUser)
{
Id = orgUser.OrganizationId;
Type = orgUser.Type;
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager;
}
public Guid Id { get; set; }
public OrganizationUserType Type { get; set; }
public Permissions Permissions { get; set; }
public bool AccessSecretsManager { get; set; }
}

View File

@ -157,6 +157,10 @@ public class CurrentContext : ICurrentContext
private List<CurrentContentOrganization> GetOrganizations(Dictionary<string, IEnumerable<Claim>> claimsDict, bool orgApi)
{
var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess)
? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true)
: new Dictionary<string, bool>();
var organizations = new List<CurrentContentOrganization>();
if (claimsDict.ContainsKey(Claims.OrganizationOwner))
{
@ -164,7 +168,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Owner
Type = OrganizationUserType.Owner,
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
}));
}
else if (orgApi && OrganizationId.HasValue)
@ -172,7 +177,7 @@ public class CurrentContext : ICurrentContext
organizations.Add(new CurrentContentOrganization
{
Id = OrganizationId.Value,
Type = OrganizationUserType.Owner
Type = OrganizationUserType.Owner,
});
}
@ -182,7 +187,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Admin
Type = OrganizationUserType.Admin,
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
}));
}
@ -192,7 +198,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.User
Type = OrganizationUserType.User,
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
}));
}
@ -202,7 +209,8 @@ public class CurrentContext : ICurrentContext
new CurrentContentOrganization
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Manager
Type = OrganizationUserType.Manager,
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
}));
}
@ -213,7 +221,8 @@ public class CurrentContext : ICurrentContext
{
Id = new Guid(c.Value),
Type = OrganizationUserType.Custom,
Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict)
Permissions = SetOrganizationPermissionsFromClaims(c.Value, claimsDict),
AccessSecretsManager = accessSecretsManager.ContainsKey(c.Value),
}));
}
@ -434,12 +443,17 @@ public class CurrentContext : ICurrentContext
return po?.ProviderId;
}
public bool AccessSecretsManager(Guid orgId)
{
return Organizations?.Any(o => o.Id == orgId && o.AccessSecretsManager) ?? false;
}
public async Task<ICollection<CurrentContentOrganization>> OrganizationMembershipAsync(
IOrganizationUserRepository organizationUserRepository, Guid userId)
{
if (Organizations == null)
{
var userOrgs = await organizationUserRepository.GetManyByUserAsync(userId);
var userOrgs = await organizationUserRepository.GetManyDetailsByUserAsync(userId);
Organizations = userOrgs.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
.Select(ou => new CurrentContentOrganization(ou)).ToList();
}

View File

@ -68,4 +68,5 @@ public interface ICurrentContext
IProviderUserRepository providerUserRepository, Guid userId);
Task<Guid?> ProviderIdForOrg(Guid orgId);
bool AccessSecretsManager(Guid organizationId);
}

View File

@ -1,15 +1,16 @@
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.Utilities;
namespace Bit.Core.Entities;
public class OrganizationConnection<T> : OrganizationConnection where T : new()
public class OrganizationConnection<T> : OrganizationConnection where T : IConnectionConfig
{
public new T Config
{
get => base.GetConfig<T>();
set => base.SetConfig<T>(value);
set => base.SetConfig(value);
}
}
@ -26,7 +27,7 @@ public class OrganizationConnection : ITableObject<Guid>
Id = CoreHelpers.GenerateComb();
}
public T GetConfig<T>() where T : new()
public T GetConfig<T>() where T : IConnectionConfig
{
try
{
@ -38,8 +39,32 @@ public class OrganizationConnection : ITableObject<Guid>
}
}
public void SetConfig<T>(T config) where T : new()
public void SetConfig<T>(T config) where T : IConnectionConfig
{
Config = JsonSerializer.Serialize(config);
}
public bool Validate<T>(out string exception) where T : IConnectionConfig
{
if (!Enabled)
{
exception = $"Connection disabled for organization {OrganizationId}";
return false;
}
if (string.IsNullOrWhiteSpace(Config))
{
exception = $"No saved Connection config for organization {OrganizationId}";
return false;
}
var config = GetConfig<T>();
if (config == null)
{
exception = $"Error parsing Connection config for organization {OrganizationId}";
return false;
}
return config.Validate(out exception);
}
}

View File

@ -22,6 +22,7 @@ public class OrganizationUser : ITableObject<Guid>, IExternal
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public string Permissions { get; set; }
public bool AccessSecretsManager { get; set; }
public void SetNewId()
{

View File

@ -65,6 +65,10 @@ public class User : ITableObject<Guid>, ISubscriber, IStorable, IStorableSubscri
public bool UnknownDeviceVerificationEnabled { get; set; }
[MaxLength(7)]
public string AvatarColor { get; set; }
public DateTime? LastPasswordChangeDate { get; set; }
public DateTime? LastKdfChangeDate { get; set; }
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public void SetNewId()
{

View File

@ -6,6 +6,7 @@ public static class Claims
public const string SecurityStamp = "sstamp";
public const string Premium = "premium";
public const string Device = "device";
public const string OrganizationOwner = "orgowner";
public const string OrganizationAdmin = "orgadmin";
public const string OrganizationManager = "orgmanager";
@ -14,6 +15,8 @@ public static class Claims
public const string ProviderAdmin = "providerprovideradmin";
public const string ProviderServiceUser = "providerserviceuser";
public const string SecretsManagerAccess = "accesssecretsmanager";
// Service Account
public const string Organization = "organization";

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Api.OrganizationLicenses;
public class SelfHostedOrganizationLicenseRequestModel
{
public string LicenseKey { get; set; }
public string BillingSyncKey { get; set; }
}

View File

@ -8,6 +8,7 @@ public class OrganizationUserInvite
public IEnumerable<string> Emails { get; set; }
public Enums.OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public Permissions Permissions { get; set; }
public IEnumerable<CollectionAccessSelection> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
@ -19,6 +20,7 @@ public class OrganizationUserInvite
Emails = requestModel.Emails;
Type = requestModel.Type;
AccessAll = requestModel.AccessAll;
AccessSecretsManager = requestModel.AccessSecretsManager;
Collections = requestModel.Collections;
Groups = requestModel.Groups;
Permissions = requestModel.Permissions;

View File

@ -1,9 +1,10 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.Models.Data.Organizations.OrganizationConnections;
public class OrganizationConnectionData<T> where T : new()
public class OrganizationConnectionData<T> where T : IConnectionConfig
{
public Guid? Id { get; set; }
public OrganizationConnectionType Type { get; set; }

View File

@ -7,6 +7,7 @@ public class OrganizationUserInviteData
public IEnumerable<string> Emails { get; set; }
public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public IEnumerable<CollectionAccessSelection> Collections { get; set; }
public IEnumerable<Guid> Groups { get; set; }
public Permissions Permissions { get; set; }

View File

@ -41,4 +41,5 @@ public class OrganizationUserOrganizationDetails
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
public bool AccessSecretsManager { get; set; }
}

View File

@ -17,6 +17,7 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
public OrganizationUserStatusType Status { get; set; }
public OrganizationUserType Type { get; set; }
public bool AccessAll { get; set; }
public bool AccessSecretsManager { get; set; }
public string ExternalId { get; set; }
public string SsoExternalId { get; set; }
public string Permissions { get; set; }

View File

@ -34,4 +34,5 @@ public class ProviderUserOrganizationDetails
public Guid? ProviderId { get; set; }
public Guid? ProviderUserId { get; set; }
public string ProviderName { get; set; }
public Enums.PlanType PlanType { get; set; }
}

View File

@ -1,7 +1,20 @@
namespace Bit.Core.Models.OrganizationConnectionConfigs;
public class BillingSyncConfig
public class BillingSyncConfig : IConnectionConfig
{
public string BillingSyncKey { get; set; }
public Guid CloudOrganizationId { get; set; }
public DateTime? LastLicenseSync { get; set; }
public bool Validate(out string exception)
{
if (string.IsNullOrWhiteSpace(BillingSyncKey))
{
exception = "Failed to get Billing Sync Key";
return false;
}
exception = "";
return true;
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Models.OrganizationConnectionConfigs;
public interface IConnectionConfig
{
bool Validate(out string exception);
}

View File

@ -3,9 +3,21 @@ using Bit.Core.Enums;
namespace Bit.Core.Models.OrganizationConnectionConfigs;
public class ScimConfig
public class ScimConfig : IConnectionConfig
{
public bool Enabled { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public ScimProviderType? ScimProvider { get; set; }
public bool Validate(out string exception)
{
if (!Enabled)
{
exception = "Scim Config is disabled";
return false;
}
exception = "";
return true;
}
}

View File

@ -1,5 +1,6 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
@ -14,7 +15,7 @@ public class CreateOrganizationConnectionCommand : ICreateOrganizationConnection
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig
{
return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity());
}

View File

@ -1,9 +1,10 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface ICreateOrganizationConnectionCommand
{
Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;
}

View File

@ -1,9 +1,10 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface IUpdateOrganizationConnectionCommand
{
Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;
}

View File

@ -1,6 +1,6 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface IValidateBillingSyncKeyCommand
{

View File

@ -1,6 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
@ -15,7 +16,7 @@ public class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnection
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig
{
if (!connectionData.Id.HasValue)
{

View File

@ -1,20 +1,17 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections;
public class ValidateBillingSyncKeyCommand : IValidateBillingSyncKeyCommand
{
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationApiKeyRepository _apiKeyRepository;
public ValidateBillingSyncKeyCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_apiKeyRepository = organizationApiKeyRepository;
}

View File

@ -0,0 +1,38 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuery
{
private readonly IInstallationRepository _installationRepository;
private readonly IPaymentService _paymentService;
private readonly ILicensingService _licensingService;
public CloudGetOrganizationLicenseQuery(
IInstallationRepository installationRepository,
IPaymentService paymentService,
ILicensingService licensingService)
{
_installationRepository = installationRepository;
_paymentService = paymentService;
_licensingService = licensingService;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
int? version = null)
{
var installation = await _installationRepository.GetByIdAsync(installationId);
if (installation is not { Enabled: true })
{
throw new BadRequestException("Invalid installation id");
}
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
}
}

View File

@ -0,0 +1,15 @@
using Bit.Core.Entities;
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
public interface ICloudGetOrganizationLicenseQuery
{
Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
int? version = null);
}
public interface ISelfHostedGetOrganizationLicenseQuery
{
Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection);
}

View File

@ -0,0 +1,66 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.OrganizationLicenses;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class SelfHostedGetOrganizationLicenseQuery : BaseIdentityClientService, ISelfHostedGetOrganizationLicenseQuery
{
private readonly IGlobalSettings _globalSettings;
public SelfHostedGetOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger<SelfHostedGetOrganizationLicenseQuery> logger, ICurrentContext currentContext)
: base(
httpFactory,
globalSettings.Installation.ApiUri,
globalSettings.Installation.IdentityUri,
"api.licensing",
$"installation.{globalSettings.Installation.Id}",
globalSettings.Installation.Key,
logger)
{
_globalSettings = globalSettings;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection)
{
if (!_globalSettings.SelfHosted)
{
throw new BadRequestException("This action is only available for self-hosted.");
}
if (!_globalSettings.EnableCloudCommunication)
{
throw new BadRequestException("Cloud communication is disabled in global settings");
}
if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))
{
throw new BadRequestException(exception);
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
var cloudOrganizationId = billingSyncConfig.CloudOrganizationId;
var response = await SendAsync<SelfHostedOrganizationLicenseRequestModel, OrganizationLicense>(
HttpMethod.Get, $"licenses/organization/{cloudOrganizationId}", new SelfHostedOrganizationLicenseRequestModel()
{
BillingSyncKey = billingSyncConfig.BillingSyncKey,
LicenseKey = organization.LicenseKey,
}, true);
if (response == null)
{
_logger.LogDebug("Organization License sync failed for '{OrgId}'", organization.Id);
throw new BadRequestException("An error has occurred. Check your internet connection and ensure the billing token is correct.");
}
return response;
}
}

View File

@ -7,6 +7,8 @@ using Bit.Core.OrganizationFeatures.OrganizationCollections;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@ -32,6 +34,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationApiKeyCommandsQueries();
services.AddOrganizationCollectionCommands();
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandQueries();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@ -85,6 +88,12 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
}
private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
{
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
}
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>

View File

@ -48,20 +48,13 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel
{
throw new BadRequestException("Failed to sync instance with cloud - Cloud communication is disabled in global settings");
}
if (!billingSyncConnection.Enabled)
if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))
{
throw new BadRequestException($"Billing Sync Key disabled for organization {organizationId}");
}
if (string.IsNullOrWhiteSpace(billingSyncConnection.Config))
{
throw new BadRequestException($"No Billing Sync Key known for organization {organizationId}");
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
if (billingSyncConfig == null || string.IsNullOrWhiteSpace(billingSyncConfig.BillingSyncKey))
{
throw new BadRequestException($"Failed to get Billing Sync Key for organization {organizationId}");
throw new BadRequestException(exception);
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId))
.ToDictionary(i => i.SponsoringOrganizationUserId);
if (!organizationSponsorshipsDict.Any())

View File

@ -11,7 +11,7 @@ public interface IProviderUserRepository : IRepository<ProviderUser, Guid>
Task<ICollection<ProviderUser>> GetManyByUserAsync(Guid userId);
Task<ProviderUser> GetByProviderUserAsync(Guid providerId, Guid userId);
Task<ICollection<ProviderUser>> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null);
Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId);
Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status = null);
Task<ICollection<ProviderUserProviderDetails>> GetManyDetailsByUserAsync(Guid userId,
ProviderUserStatusType? status = null);
Task<IEnumerable<ProviderUserOrganizationDetails>> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null);

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
public interface ICreateServiceAccountCommand
{
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId);
}

View File

@ -8,7 +8,8 @@ public interface IAccessPolicyRepository
Task<List<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> baseAccessPolicies);
Task<bool> AccessPolicyExists(BaseAccessPolicy baseAccessPolicy);
Task<BaseAccessPolicy?> GetByIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>?> GetManyByProjectId(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id);
Task<IEnumerable<BaseAccessPolicy>> GetManyByGrantedServiceAccountIdAsync(Guid id);
Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy);
Task DeleteAsync(Guid id);
}

View File

@ -56,9 +56,6 @@ public interface IOrganizationService
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<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
int? version = null);
Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable<ImportedGroup> groups,
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
bool overwriteExisting);

View File

@ -2,6 +2,7 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Enums.Provider;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
@ -43,6 +44,8 @@ public class OrganizationService : IOrganizationService
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
private readonly ICurrentContext _currentContext;
private readonly ILogger<OrganizationService> _logger;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IProviderUserRepository _providerUserRepository;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -69,7 +72,9 @@ public class OrganizationService : IOrganizationService
IOrganizationApiKeyRepository organizationApiKeyRepository,
IOrganizationConnectionRepository organizationConnectionRepository,
ICurrentContext currentContext,
ILogger<OrganizationService> logger)
ILogger<OrganizationService> logger,
IProviderOrganizationRepository providerOrganizationRepository,
IProviderUserRepository providerUserRepository)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -96,6 +101,8 @@ public class OrganizationService : IOrganizationService
_organizationConnectionRepository = organizationConnectionRepository;
_currentContext = currentContext;
_logger = logger;
_providerOrganizationRepository = providerOrganizationRepository;
_providerUserRepository = providerUserRepository;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -1236,6 +1243,7 @@ public class OrganizationService : IOrganizationService
Type = invite.Type.Value,
Status = OrganizationUserStatusType.Invited,
AccessAll = invite.AccessAll,
AccessSecretsManager = invite.AccessSecretsManager,
ExternalId = externalId,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
@ -1637,8 +1645,19 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException(failureMessage);
}
var ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
var providerOrg = await this._providerOrganizationRepository.GetByOrganizationId(organization.Id);
IEnumerable<string> ownerEmails;
if (providerOrg != null)
{
ownerEmails = (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId, ProviderUserStatusType.Confirmed))
.Select(u => u.Email).Distinct();
}
else
{
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
}
var initialSeatCount = organization.Seats.Value;
await AdjustSeatsAsync(organization, seatsToAdd, prorationDate, ownerEmails);
@ -1910,30 +1929,6 @@ public class OrganizationService : IOrganizationService
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
}
public async Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId)
{
var organization = await GetOrgById(organizationId);
return await GenerateLicenseAsync(organization, installationId);
}
public async Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
int? version = null)
{
if (organization == null)
{
throw new NotFoundException();
}
var installation = await _installationRepository.GetByIdAsync(installationId);
if (installation == null || !installation.Enabled)
{
throw new BadRequestException("Invalid installation id");
}
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
IEnumerable<Guid> groups)

View File

@ -561,10 +561,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return result;
}
var now = DateTime.UtcNow;
user.Key = key;
user.Email = newEmail;
user.EmailVerified = true;
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastEmailChangeDate = now;
await _userRepository.ReplaceAsync(user);
if (user.Gateway == GatewayType.Stripe)
@ -618,7 +621,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return result;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastPasswordChangeDate = now;
user.Key = key;
user.MasterPasswordHint = passwordHint;
@ -845,7 +850,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return result;
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKdfChangeDate = now;
user.Key = key;
user.Kdf = kdf;
user.KdfIterations = kdfIterations;
@ -870,7 +877,9 @@ public class UserService : UserManager<User>, IUserService, IDisposable
if (await CheckPasswordAsync(user, masterPassword))
{
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString();
user.Key = key;
user.PrivateKey = privateKey;

View File

@ -692,6 +692,15 @@ public static class CoreHelpers
default:
break;
}
// Secrets Manager
foreach (var org in group)
{
if (org.AccessSecretsManager)
{
claims.Add(new KeyValuePair<string, string>(Claims.SecretsManagerAccess, org.Id.ToString()));
}
}
}
}

View File

@ -29,6 +29,9 @@ public class Startup
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Repositories
services.AddDatabaseRepositories(globalSettings);

View File

@ -25,6 +25,7 @@ public class ApiResources
Claims.OrganizationCustom,
Claims.ProviderAdmin,
Claims.ProviderServiceUser,
Claims.SecretsManagerAccess,
}),
new(ApiScopes.Internal, new[] { JwtClaimTypes.Subject }),
new(ApiScopes.ApiPush, new[] { JwtClaimTypes.Subject }),

View File

@ -59,7 +59,7 @@ public static class DapperHelpers
public static DataTable ToTvp(this IEnumerable<OrganizationUser> orgUsers)
{
var table = new DataTable();
table.SetTypeName("[dbo].[OrganizationUserType]");
table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func<OrganizationUser, object> getter)>
{
@ -76,6 +76,7 @@ public static class DapperHelpers
(nameof(OrganizationUser.RevisionDate), typeof(DateTime), ou => ou.RevisionDate),
(nameof(OrganizationUser.Permissions), typeof(string), ou => ou.Permissions),
(nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey),
(nameof(OrganizationUser.AccessSecretsManager), typeof(bool), ou => ou.AccessSecretsManager),
};
return orgUsers.BuildTable(table, columnData);

View File

@ -321,6 +321,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
}
cmd.Parameters.Add("@RevisionDate", SqlDbType.DateTime2).Value = user.RevisionDate;
cmd.Parameters.Add("@AccountRevisionDate", SqlDbType.DateTime2).Value = user.AccountRevisionDate;
cmd.Parameters.Add("@LastKeyRotationDate", SqlDbType.DateTime2).Value = user.LastKeyRotationDate;
cmd.ExecuteNonQuery();
}

View File

@ -405,7 +405,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
using (var connection = new SqlConnection(_marsConnectionString))
{
var results = await connection.ExecuteAsync(
$"[{Schema}].[{Table}_CreateMany]",
$"[{Schema}].[{Table}_CreateMany2]",
new { OrganizationUsersInput = orgUsersTVP },
commandType: CommandType.StoredProcedure);
}
@ -424,7 +424,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
using (var connection = new SqlConnection(_marsConnectionString))
{
var results = await connection.ExecuteAsync(
$"[{Schema}].[{Table}_UpdateMany]",
$"[{Schema}].[{Table}_UpdateMany2]",
new { OrganizationUsersInput = orgUsersTVP },
commandType: CommandType.StoredProcedure);
}

View File

@ -84,13 +84,13 @@ public class ProviderUserRepository : Repository<ProviderUser, Guid>, IProviderU
}
}
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId)
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<ProviderUserUserDetails>(
"[dbo].[ProviderUserUserDetails_ReadByProviderId]",
new { ProviderId = providerId },
new { ProviderId = providerId, Status = status },
commandType: CommandType.StoredProcedure);
return results.ToList();

View File

@ -128,7 +128,9 @@ public abstract class BaseEntityFrameworkRepository
entity.SecurityStamp = user.SecurityStamp;
entity.Key = user.Key;
entity.PrivateKey = user.PrivateKey;
entity.RevisionDate = DateTime.UtcNow;
entity.LastKeyRotationDate = user.LastKeyRotationDate;
entity.AccountRevisionDate = user.AccountRevisionDate;
entity.RevisionDate = user.RevisionDate;
await dbContext.SaveChangesAsync();
}
}

View File

@ -29,7 +29,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
var dbContext = GetDatabaseContext(scope);
if (cipher.OrganizationId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);
}
else if (cipher.UserId.HasValue)
{
@ -59,7 +59,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
await OrganizationUpdateStorage(cipherInfo.OrganizationId.Value);
}
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipherInfo.OrganizationId);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipherInfo.OrganizationId.Value);
}
else if (cipherInfo?.UserId != null)
{
@ -107,7 +107,16 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
null;
var entity = Mapper.Map<Cipher>((Core.Entities.Cipher)cipher);
await dbContext.AddAsync(entity);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.GetValueOrDefault());
if (cipher.OrganizationId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);
}
else if (cipher.UserId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);
}
await dbContext.SaveChangesAsync();
}
return cipher;
@ -458,7 +467,16 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
}
var mappedEntity = Mapper.Map<Cipher>((Core.Entities.Cipher)cipher);
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.GetValueOrDefault());
if (cipher.OrganizationId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);
}
else if (cipher.UserId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);
}
await dbContext.SaveChangesAsync();
}
}
@ -566,7 +584,15 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
}
}
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.GetValueOrDefault());
if (cipher.OrganizationId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);
}
else if (cipher.UserId.HasValue)
{
await dbContext.UserBumpAccountRevisionDateAsync(cipher.UserId.Value);
}
await dbContext.SaveChangesAsync();
return true;
}
@ -677,7 +703,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
if (attachment.OrganizationId.HasValue)
{
await OrganizationUpdateStorage(cipher.OrganizationId.Value);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId);
await dbContext.UserBumpAccountRevisionDateByCipherIdAsync(cipher.Id, cipher.OrganizationId.Value);
}
else if (attachment.UserId.HasValue)
{

View File

@ -34,7 +34,7 @@ public static class DatabaseContextExtensions
UpdateUserRevisionDate(users);
}
public static async Task UserBumpAccountRevisionDateByCipherIdAsync(this DatabaseContext context, Guid cipherId, Guid? organizationId)
public static async Task UserBumpAccountRevisionDateByCipherIdAsync(this DatabaseContext context, Guid cipherId, Guid organizationId)
{
var query = new UserBumpAccountRevisionDateByCipherIdQuery(cipherId, organizationId);
var users = await query.Run(context).ToListAsync();

View File

@ -103,7 +103,7 @@ public class ProviderUserRepository :
return await query.FirstOrDefaultAsync();
}
}
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId)
public async Task<ICollection<ProviderUserUserDetails>> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -113,17 +113,19 @@ public class ProviderUserRepository :
on pu.UserId equals u.Id into u_g
from u in u_g.DefaultIfEmpty()
select new { pu, u };
var data = await view.Where(e => e.pu.ProviderId == providerId).Select(e => new ProviderUserUserDetails
{
Id = e.pu.Id,
UserId = e.pu.UserId,
ProviderId = e.pu.ProviderId,
Name = e.u.Name,
Email = e.u.Email ?? e.pu.Email,
Status = e.pu.Status,
Type = e.pu.Type,
Permissions = e.pu.Permissions,
}).ToArrayAsync();
var data = await view
.Where(e => e.pu.ProviderId == providerId && (status == null || e.pu.Status == status))
.Select(e => new ProviderUserUserDetails
{
Id = e.pu.Id,
UserId = e.pu.UserId,
ProviderId = e.pu.ProviderId,
Name = e.u.Name,
Email = e.u.Email ?? e.pu.Email,
Status = e.pu.Status,
Type = e.pu.Type,
Permissions = e.pu.Permissions,
}).ToArrayAsync();
return data;
}
}

View File

@ -37,6 +37,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
Use2fa = o.Use2fa,
UseApi = o.UseApi,
UseResetPassword = o.UseResetPassword,
UseSecretsManager = o.UseSecretsManager,
SelfHost = o.SelfHost,
UsersGetPremium = o.UsersGetPremium,
UseCustomPermissions = o.UseCustomPermissions,
@ -58,7 +59,8 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
FamilySponsorshipFriendlyName = os.FriendlyName,
FamilySponsorshipLastSyncDate = os.LastSyncDate,
FamilySponsorshipToDelete = os.ToDelete,
FamilySponsorshipValidUntil = os.ValidUntil
FamilySponsorshipValidUntil = os.ValidUntil,
AccessSecretsManager = ou.AccessSecretsManager,
};
return query;
}

View File

@ -29,6 +29,7 @@ public class OrganizationUserUserDetailsViewQuery : IQuery<OrganizationUserUserD
Permissions = x.ou.Permissions,
ResetPasswordKey = x.ou.ResetPasswordKey,
UsesKeyConnector = x.u != null && x.u.UsesKeyConnector,
AccessSecretsManager = x.ou.AccessSecretsManager,
});
}
}

View File

@ -41,6 +41,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
PrivateKey = x.o.PrivateKey,
ProviderId = x.p.Id,
ProviderName = x.p.Name,
PlanType = x.o.PlanType
});
}
}

View File

@ -1,5 +1,4 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Enums;
using User = Bit.Infrastructure.EntityFramework.Models.User;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
@ -7,15 +6,9 @@ namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class UserBumpAccountRevisionDateByCipherIdQuery : IQuery<User>
{
private readonly Guid _cipherId;
private readonly Guid? _organizationId;
private readonly Guid _organizationId;
public UserBumpAccountRevisionDateByCipherIdQuery(Cipher cipher)
{
_cipherId = cipher.Id;
_organizationId = cipher.OrganizationId;
}
public UserBumpAccountRevisionDateByCipherIdQuery(Guid cipherId, Guid? organizationId)
public UserBumpAccountRevisionDateByCipherIdQuery(Guid cipherId, Guid organizationId)
{
_cipherId = cipherId;
_organizationId = organizationId;

View File

@ -13,7 +13,7 @@ public class HeartbeatHostedService : IHostedService, IDisposable
private CancellationTokenSource _cts;
public HeartbeatHostedService(
ILogger<AzureQueueHostedService> logger,
ILogger<HeartbeatHostedService> logger,
IHubContext<NotificationsHub> hubContext,
GlobalSettings globalSettings)
{
@ -49,7 +49,7 @@ public class HeartbeatHostedService : IHostedService, IDisposable
while (!cancellationToken.IsCancellationRequested)
{
await _hubContext.Clients.All.SendAsync("Heartbeat");
await Task.Delay(120000);
await Task.Delay(120000, cancellationToken);
}
_logger.LogWarning("Done with heartbeat.");
}

View File

@ -238,6 +238,7 @@
<Build Include="dbo\Stored Procedures\OrganizationUser_Activate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Create.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateMany.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateMany2.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_CreateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Deactivate.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_DeleteById.sql" />
@ -258,6 +259,7 @@
<Build Include="dbo\Stored Procedures\OrganizationUser_SelectKnownEmails.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_Update.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateMany.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateMany2.sql" />
<Build Include="dbo\Stored Procedures\OrganizationUser_UpdateWithCollections.sql" />
<Build Include="dbo\Stored Procedures\Organization_Create.sql" />
<Build Include="dbo\Stored Procedures\Organization_DeleteById.sql" />
@ -400,6 +402,7 @@
<Build Include="dbo\User Defined Types\GuidIdArray.sql" />
<Build Include="dbo\User Defined Types\OrganizationSponsorshipType.sql" />
<Build Include="dbo\User Defined Types\OrganizationUserType.sql" />
<Build Include="dbo\User Defined Types\OrganizationUserType2.sql" />
<Build Include="dbo\User Defined Types\SelectionReadOnlyArray.sql" />
<Build Include="dbo\User Defined Types\TwoGuidIdArray.sql" />
<Build Include="dbo\Views\AuthRequestView.sql" />

View File

@ -11,7 +11,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX)
@ResetPasswordKey VARCHAR(MAX),
@AccessSecretsManager BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -30,7 +31,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[Permissions],
[ResetPasswordKey]
[ResetPasswordKey],
[AccessSecretsManager]
)
VALUES
(
@ -46,6 +48,7 @@ BEGIN
@CreationDate,
@RevisionDate,
@Permissions,
@ResetPasswordKey
@ResetPasswordKey,
@AccessSecretsManager
)
END

View File

@ -0,0 +1,42 @@
CREATE PROCEDURE [dbo].[OrganizationUser_CreateMany2]
@OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OrganizationUser]
(
[Id],
[OrganizationId],
[UserId],
[Email],
[Key],
[Status],
[Type],
[AccessAll],
[ExternalId],
[CreationDate],
[RevisionDate],
[Permissions],
[ResetPasswordKey],
[AccessSecretsManager]
)
SELECT
OU.[Id],
OU.[OrganizationId],
OU.[UserId],
OU.[Email],
OU.[Key],
OU.[Status],
OU.[Type],
OU.[AccessAll],
OU.[ExternalId],
OU.[CreationDate],
OU.[RevisionDate],
OU.[Permissions],
OU.[ResetPasswordKey],
OU.[AccessSecretsManager]
FROM
@OrganizationUsersInput OU
END
GO

View File

@ -12,12 +12,13 @@
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY,
@AccessSecretsManager BIT = 0
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager
;WITH [AvailableCollectionsCTE] AS(
SELECT

View File

@ -11,7 +11,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX)
@ResetPasswordKey VARCHAR(MAX),
@AccessSecretsManager BIT = 0
AS
BEGIN
SET NOCOUNT ON
@ -30,7 +31,8 @@ BEGIN
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[Permissions] = @Permissions,
[ResetPasswordKey] = @ResetPasswordKey
[ResetPasswordKey] = @ResetPasswordKey,
[AccessSecretsManager] = @AccessSecretsManager
WHERE
[Id] = @Id

View File

@ -0,0 +1,34 @@
CREATE PROCEDURE [dbo].[OrganizationUser_UpdateMany2]
@OrganizationUsersInput [dbo].[OrganizationUserType2] READONLY
AS
BEGIN
SET NOCOUNT ON
UPDATE
OU
SET
[OrganizationId] = OUI.[OrganizationId],
[UserId] = OUI.[UserId],
[Email] = OUI.[Email],
[Key] = OUI.[Key],
[Status] = OUI.[Status],
[Type] = OUI.[Type],
[AccessAll] = OUI.[AccessAll],
[ExternalId] = OUI.[ExternalId],
[CreationDate] = OUI.[CreationDate],
[RevisionDate] = OUI.[RevisionDate],
[Permissions] = OUI.[Permissions],
[ResetPasswordKey] = OUI.[ResetPasswordKey],
[AccessSecretsManager] = OUI.[AccessSecretsManager]
FROM
[dbo].[OrganizationUser] OU
INNER JOIN
@OrganizationUsersInput OUI ON OU.Id = OUI.Id
EXEC [dbo].[User_BumpManyAccountRevisionDates]
(
SELECT UserId
FROM @OrganizationUsersInput
)
END
GO

View File

@ -12,12 +12,13 @@
@RevisionDate DATETIME2(7),
@Permissions NVARCHAR(MAX),
@ResetPasswordKey VARCHAR(MAX),
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY
@Collections AS [dbo].[SelectionReadOnlyArray] READONLY,
@AccessSecretsManager BIT = 0
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager
-- Update
UPDATE
[Target]

View File

@ -1,5 +1,6 @@
CREATE PROCEDURE [dbo].[ProviderUserUserDetails_ReadByProviderId]
@ProviderId UNIQUEIDENTIFIER
@ProviderId UNIQUEIDENTIFIER,
@Status TINYINT = NULL
AS
BEGIN
SET NOCOUNT ON
@ -10,4 +11,5 @@ BEGIN
[dbo].[ProviderUserUserDetailsView]
WHERE
[ProviderId] = @ProviderId
AND [Status] = COALESCE(@Status, [Status])
END

View File

@ -37,7 +37,11 @@
@FailedLoginCount INT = 0,
@LastFailedLoginDate DATETIME2(7),
@UnknownDeviceVerificationEnabled BIT = 1,
@AvatarColor VARCHAR(7) = NULL
@AvatarColor VARCHAR(7) = NULL,
@LastPasswordChangeDate DATETIME2(7) = NULL,
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -82,7 +86,11 @@ BEGIN
[UnknownDeviceVerificationEnabled],
[AvatarColor],
[KdfMemory],
[KdfParallelism]
[KdfParallelism],
[LastPasswordChangeDate],
[LastKdfChangeDate],
[LastKeyRotationDate],
[LastEmailChangeDate]
)
VALUES
(
@ -124,6 +132,10 @@ BEGIN
@UnknownDeviceVerificationEnabled,
@AvatarColor,
@KdfMemory,
@KdfParallelism
@KdfParallelism,
@LastPasswordChangeDate,
@LastKdfChangeDate,
@LastKeyRotationDate,
@LastEmailChangeDate
)
END

View File

@ -37,7 +37,11 @@
@FailedLoginCount INT,
@LastFailedLoginDate DATETIME2(7),
@UnknownDeviceVerificationEnabled BIT = 1,
@AvatarColor VARCHAR(7)
@AvatarColor VARCHAR(7),
@LastPasswordChangeDate DATETIME2(7) = NULL,
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -82,7 +86,11 @@ BEGIN
[FailedLoginCount] = @FailedLoginCount,
[LastFailedLoginDate] = @LastFailedLoginDate,
[UnknownDeviceVerificationEnabled] = @UnknownDeviceVerificationEnabled,
[AvatarColor] = @AvatarColor
[AvatarColor] = @AvatarColor,
[LastPasswordChangeDate] = @LastPasswordChangeDate,
[LastKdfChangeDate] = @LastKdfChangeDate,
[LastKeyRotationDate] = @LastKeyRotationDate,
[LastEmailChangeDate] = @LastEmailChangeDate
WHERE
[Id] = @Id
END

View File

@ -3,7 +3,9 @@
@SecurityStamp NVARCHAR(50),
@Key NVARCHAR(MAX),
@PrivateKey VARCHAR(MAX),
@RevisionDate DATETIME2(7)
@RevisionDate DATETIME2(7),
@AccountRevisionDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -15,7 +17,8 @@ BEGIN
[Key] = @Key,
[PrivateKey] = @PrivateKey,
[RevisionDate] = @RevisionDate,
[AccountRevisionDate] = @RevisionDate
[AccountRevisionDate] = ISNULL(@AccountRevisionDate, @RevisionDate),
[LastKeyRotationDate] = @LastKeyRotationDate
WHERE
[Id] = @Id
END

View File

@ -12,6 +12,7 @@
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
[Permissions] NVARCHAR (MAX) NULL,
[AccessSecretsManager] BIT NOT NULL CONSTRAINT [DF_OrganizationUser_SecretsManager] DEFAULT (0),
CONSTRAINT [PK_OrganizationUser] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationUser_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])

Some files were not shown because too many files have changed in this diff Show More