diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index aa43052df7..9845c2ed2a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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: |
diff --git a/.run/Full Server - Self-hosted.run.xml b/.run/Full Server - Self-hosted.run.xml
new file mode 100644
index 0000000000..671697cf94
--- /dev/null
+++ b/.run/Full Server - Self-hosted.run.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Full Server.run.xml b/.run/Full Server.run.xml
new file mode 100644
index 0000000000..2031666f7e
--- /dev/null
+++ b/.run/Full Server.run.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Min Server - Self-hosted.run.xml b/.run/Min Server - Self-hosted.run.xml
new file mode 100644
index 0000000000..ef0ca539d1
--- /dev/null
+++ b/.run/Min Server - Self-hosted.run.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Min Server.run.xml b/.run/Min Server.run.xml
new file mode 100644
index 0000000000..6e6cac80a3
--- /dev/null
+++ b/.run/Min Server.run.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs
index e8d179e799..66b525f654 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/AccessTokens/CreateAccessTokenCommand.cs
@@ -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);
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs
index 8451f1d40a..e975df93f7 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/DeleteProjectCommand.cs
@@ -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);
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs
index 74799547a0..06ed949c0f 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/UpdateProjectCommand.cs
@@ -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;
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs
index b04a8f911d..c1872717b7 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Secrets/DeleteSecretCommand.cs
@@ -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);
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs
index f3d3799472..687291d75a 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/CreateServiceAccountCommand.cs
@@ -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 CreateAsync(ServiceAccount serviceAccount)
+ public async Task 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 { accessPolicy });
+ return createdServiceAccount;
}
}
diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs
index efc77fc040..378f3cbc33 100644
--- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs
+++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/ServiceAccounts/UpdateServiceAccountCommand.cs
@@ -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;
diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs
index dd56e08fcd..1cf066190e 100644
--- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs
+++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/AccessPolicyRepository.cs
@@ -138,7 +138,7 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
- public async Task?> GetManyByProjectId(Guid id)
+ public async Task> 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> 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(ap),
GroupProjectAccessPolicy ap => Mapper.Map(ap),
ServiceAccountProjectAccessPolicy ap => Mapper.Map(ap),
+ UserServiceAccountAccessPolicy ap => Mapper.Map(ap),
+ GroupServiceAccountAccessPolicy ap => Mapper.Map(ap),
_ => throw new ArgumentException("Unsupported access policy type")
};
}
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs
index d9b2027c36..de34e77392 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/AccessTokens/CreateAccessTokenCommandTests.cs
@@ -36,7 +36,7 @@ public class CreateServiceAccountCommandTests
sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false);
- await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId));
+ await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId));
await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
@@ -49,6 +49,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true);
+ sutProvider.GetDependency().AccessSecretsManager(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
@@ -64,6 +65,7 @@ public class CreateServiceAccountCommandTests
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData);
+ sutProvider.GetDependency().AccessSecretsManager(saData.OrganizationId).Returns(true);
sutProvider.GetDependency().OrganizationAdmin(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs
index 3d93d39793..e48c2cf0af 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/DeleteProjectCommandTests.cs
@@ -28,7 +28,7 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
- public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List data, Guid userId,
+ public async Task Delete_OneIdNotFound_Throws_NotFoundException(List data, Guid userId,
SutProvider 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().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency().ClientType = ClientType.User;
sutProvider.GetDependency().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency().UserHasWriteAccessToProject(Arg.Any(), userId).Returns(true);
@@ -65,11 +66,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
- public async Task DeleteSecrets_User_No_Permission(List data, Guid userId, Guid organizationId,
+ public async Task Delete_User_No_Permission(List data, Guid userId, Guid organizationId,
SutProvider sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
+ sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency().ClientType = ClientType.User;
sutProvider.GetDependency().GetManyByIds(data).Returns(projects);
sutProvider.GetDependency().UserHasWriteAccessToProject(userId, userId).Returns(false);
@@ -86,11 +88,12 @@ public class DeleteProjectCommandTests
[Theory]
[BitAutoData]
- public async Task DeleteSecrets_OrganizationAdmin_Success(List data, Guid userId, Guid organizationId,
+ public async Task Delete_OrganizationAdmin_Success(List data, Guid userId, Guid organizationId,
SutProvider sutProvider)
{
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
+ sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true);
sutProvider.GetDependency().GetManyByIds(data).Returns(projects);
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs
index d85a44a911..8bacf0fc20 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/UpdateProjectCommandTests.cs
@@ -33,6 +33,7 @@ public class UpdateProjectCommandTests
public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider sutProvider)
{
sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project);
+ sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true);
sutProvider.GetDependency().OrganizationAdmin(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
@@ -51,8 +52,9 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(false);
+ sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true);
- await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(project, userId));
+ await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(project, userId));
await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@@ -63,6 +65,7 @@ public class UpdateProjectCommandTests
{
sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project);
sutProvider.GetDependency().UserHasWriteAccessToProject(project.Id, userId).Returns(true);
+ sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true);
var project2 = new Project { Id = project.Id, Name = "newName" };
var result = await sutProvider.Sut.UpdateAsync(project2, userId);
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs
index d5adec8827..cee70548e7 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Secrets/DeleteSecretCommandTests.cs
@@ -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().GetManyByIds(data).Returns(secrets);
+ sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true);
var results = await sutProvider.Sut.DeleteSecrets(data);
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/CreateServiceAccountCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/CreateServiceAccountCommandTests.cs
index 3c3e11a6a0..7acd315651 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/CreateServiceAccountCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/CreateServiceAccountCommandTests.cs
@@ -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 sutProvider)
+ Guid userId,
+ SutProvider sutProvider)
{
- await sutProvider.Sut.CreateAsync(data);
+ sutProvider.GetDependency()
+ .GetByOrganizationAsync(Arg.Any(), Arg.Any())
+ .Returns(new OrganizationUser() { Id = userId });
+
+ sutProvider.GetDependency()
+ .CreateAsync(Arg.Any())
+ .Returns(data);
+
+ await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs
index 0b75cb2a9a..3a06eac474 100644
--- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs
+++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/ServiceAccounts/UpdateServiceAccountCommandTests.cs
@@ -18,7 +18,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider sutProvider)
{
- var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId));
+ await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@@ -30,7 +30,7 @@ public class UpdateServiceAccountCommandTests
sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false);
- await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId));
+ await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
@@ -39,6 +39,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider sutProvider)
{
+ sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true);
@@ -54,6 +55,7 @@ public class UpdateServiceAccountCommandTests
public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider sutProvider)
{
sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data);
+ sutProvider.GetDependency().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency().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 sutProvider)
{
+ sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@@ -87,6 +90,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider)
{
+ sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
@@ -108,6 +112,7 @@ public class UpdateServiceAccountCommandTests
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider)
{
+ sutProvider.GetDependency().AccessSecretsManager(existingServiceAccount.OrganizationId).Returns(true);
sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
diff --git a/dev/migrate.ps1 b/dev/migrate.ps1
index 4c878a5d10..c1894342b9 100755
--- a/dev/migrate.ps1
+++ b/dev/migrate.ps1
@@ -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
diff --git a/docker-unified/Dockerfile b/docker-unified/Dockerfile
index 0814ec9f05..110ca7991b 100644
--- a/docker-unified/Dockerfile
+++ b/docker-unified/Dockerfile
@@ -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
diff --git a/docker-unified/hbs/nginx-config.hbs b/docker-unified/hbs/nginx-config.hbs
index d405bf5ac9..5b57673f1c 100644
--- a/docker-unified/hbs/nginx-config.hbs
+++ b/docker-unified/hbs/nginx-config.hbs
@@ -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")}}
diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs
index 9bd6189b3e..b18864d371 100644
--- a/src/Admin/Controllers/ToolsController.cs
+++ b/src/Admin/Controllers/ToolsController.cs
@@ -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);
diff --git a/src/Api/Controllers/LicensesController.cs b/src/Api/Controllers/LicensesController.cs
index 63ed824795..f5391fddc2 100644
--- a/src/Api/Controllers/LicensesController.cs
+++ b/src/Api/Controllers/LicensesController.cs
@@ -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;
}
+ ///
+ /// Used by self-hosted installations to get an updated license file
+ ///
[HttpGet("organization/{id}")]
- public async Task GetOrganization(string id, [FromQuery] string key)
+ public async Task 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;
}
}
diff --git a/src/Api/Controllers/OrganizationConnectionsController.cs b/src/Api/Controllers/OrganizationConnectionsController.cs
index 13260b7cca..b7329a14fa 100644
--- a/src/Api/Controllers/OrganizationConnectionsController.cs
+++ b/src/Api/Controllers/OrganizationConnectionsController.cs
@@ -191,7 +191,7 @@ public class OrganizationConnectionsController : Controller
Guid? organizationConnectionId,
OrganizationConnectionRequestModel model,
Func, Task> validateAction = null)
- where T : new()
+ where T : IConnectionConfig
{
var typedModel = new OrganizationConnectionRequestModel(model);
if (validateAction != null)
diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs
index fc5d38db1c..d9715d07c3 100644
--- a/src/Api/Controllers/OrganizationSponsorshipsController.cs
+++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs
@@ -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;
diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs
index bb7c3a63a2..baf0dbfaaf 100644
--- a/src/Api/Controllers/OrganizationsController.cs
+++ b/src/Api/Controllers/OrganizationsController.cs
@@ -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 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)
diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs
new file mode 100644
index 0000000000..92cf50ae41
--- /dev/null
+++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs
@@ -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 PostLicenseAsync(OrganizationCreateLicenseRequestModel model)
+ {
+ var user = await _userService.GetUserByPrincipalAsync(User);
+ if (user == null)
+ {
+ throw new UnauthorizedAccessException();
+ }
+
+ var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License);
+ if (license == null)
+ {
+ throw new BadRequestException("Invalid license");
+ }
+
+ var result = await _organizationService.SignUpAsync(license, user, model.Key,
+ model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
+ return new OrganizationResponseModel(result.Item1);
+ }
+
+ [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(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();
+ config.LastLicenseSync = DateTime.Now;
+ billingSyncConnection.SetConfig(config);
+ await _organizationConnectionRepository.ReplaceAsync(billingSyncConnection);
+ }
+}
diff --git a/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs
index 9dbc9ca0a0..96a444c27a 100644
--- a/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs
+++ b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs
@@ -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 : OrganizationConnectionRequestModel where T : new()
+public class OrganizationConnectionRequestModel : OrganizationConnectionRequestModel where T : IConnectionConfig
{
public T ParsedConfig { get; private set; }
diff --git a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs
index a09012d792..9a8ab0192c 100644
--- a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs
+++ b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs
@@ -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 Collections { get; set; }
public IEnumerable 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 Collections { get; set; }
public IEnumerable Groups { get; set; }
@@ -85,6 +88,7 @@ public class OrganizationUserUpdateRequestModel
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
existingUser.AccessAll = AccessAll;
+ existingUser.AccessSecretsManager = AccessSecretsManager;
return existingUser;
}
}
diff --git a/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs
index 6c83a76940..4c7424744c 100644
--- a/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs
+++ b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs
@@ -23,6 +23,7 @@ public class OrganizationUserResponseModel : ResponseModel
Type = organizationUser.Type;
Status = organizationUser.Status;
AccessAll = organizationUser.AccessAll;
+ AccessSecretsManager = organizationUser.AccessSecretsManager;
Permissions = CoreHelpers.LoadClassFromJsonData(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(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; }
diff --git a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
index d2b98099ed..94ef68c263 100644
--- a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
+++ b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
@@ -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; }
}
diff --git a/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs
index 88c41d99c0..1d508cae17 100644
--- a/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs
+++ b/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs
@@ -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;
}
}
diff --git a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs
index 493fbf5602..d2d3a4f28b 100644
--- a/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs
+++ b/src/Api/SecretsManager/Controllers/AccessPoliciesController.cs
@@ -40,7 +40,7 @@ public class AccessPoliciesController : Controller
[HttpGet("/projects/{id}/access-policies")]
public async Task GetProjectAccessPoliciesAsync([FromRoute] Guid id)
{
- var results = await _accessPolicyRepository.GetManyByProjectId(id);
+ var results = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(id);
return new ProjectAccessPoliciesResponseModel(results);
}
diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs
index d8ef5a00f7..437079d87d 100644
--- a/src/Api/SecretsManager/Controllers/ProjectsController.cs
+++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs
@@ -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 CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
+ [HttpGet("organizations/{organizationId}/projects")]
+ public async Task> 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 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> 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(responses);
}
+ [HttpPost("organizations/{organizationId}/projects")]
+ public async Task 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 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 GetProjectAsync([FromRoute] Guid id)
+ public async Task 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> BulkDeleteProjectsAsync([FromBody] List ids)
+ public async Task> BulkDeleteAsync([FromBody] List ids)
{
var userId = _userService.GetProperUserId(User).Value;
diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs
index a66680f63b..3ddf4699d7 100644
--- a/src/Api/SecretsManager/Controllers/SecretsController.cs
+++ b/src/Api/SecretsManager/Controllers/SecretsController.cs
@@ -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 GetSecretsByOrganizationAsync([FromRoute] Guid organizationId)
+ public async Task 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 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 GetSecretAsync([FromRoute] Guid id)
+ public async Task 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 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 UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
+ public async Task UpdateAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{
var result = await _updateSecretCommand.UpdateAsync(updateRequest.ToSecret(id));
return new SecretResponseModel(result);
diff --git a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
index 6456fa21e3..175db2a1f2 100644
--- a/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
+++ b/src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
@@ -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> GetServiceAccountsByOrganizationAsync(
+ public async Task> 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 CreateServiceAccountAsync([FromRoute] Guid organizationId,
+ public async Task 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 UpdateServiceAccountAsync([FromRoute] Guid id,
+ public async Task 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);
diff --git a/src/Api/SecretsManager/Models/Response/ProjectAccessPoliciesResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectAccessPoliciesResponseModel.cs
index c7170e1367..f03411cd39 100644
--- a/src/Api/SecretsManager/Models/Response/ProjectAccessPoliciesResponseModel.cs
+++ b/src/Api/SecretsManager/Models/Response/ProjectAccessPoliciesResponseModel.cs
@@ -10,11 +10,6 @@ public class ProjectAccessPoliciesResponseModel : ResponseModel
public ProjectAccessPoliciesResponseModel(IEnumerable baseAccessPolicies)
: base(_objectName)
{
- if (baseAccessPolicies == null)
- {
- return;
- }
-
foreach (var baseAccessPolicy in baseAccessPolicies)
switch (baseAccessPolicy)
{
diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs
index 2ad7d0ce7e..caf2235698 100644
--- a/src/Billing/Startup.cs
+++ b/src/Billing/Startup.cs
@@ -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);
diff --git a/src/Core/Context/CurrentContentOrganization.cs b/src/Core/Context/CurrentContentOrganization.cs
index 040c1ece49..b21598a035 100644
--- a/src/Core/Context/CurrentContentOrganization.cs
+++ b/src/Core/Context/CurrentContentOrganization.cs
@@ -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(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; }
}
diff --git a/src/Core/Context/CurrentContext.cs b/src/Core/Context/CurrentContext.cs
index 68e4246d04..4411509cd5 100644
--- a/src/Core/Context/CurrentContext.cs
+++ b/src/Core/Context/CurrentContext.cs
@@ -157,6 +157,10 @@ public class CurrentContext : ICurrentContext
private List GetOrganizations(Dictionary> claimsDict, bool orgApi)
{
+ var accessSecretsManager = claimsDict.ContainsKey(Claims.SecretsManagerAccess)
+ ? claimsDict[Claims.SecretsManagerAccess].ToDictionary(s => s.Value, _ => true)
+ : new Dictionary();
+
var organizations = new List();
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> 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();
}
diff --git a/src/Core/Context/ICurrentContext.cs b/src/Core/Context/ICurrentContext.cs
index d5ea350602..a78757d090 100644
--- a/src/Core/Context/ICurrentContext.cs
+++ b/src/Core/Context/ICurrentContext.cs
@@ -68,4 +68,5 @@ public interface ICurrentContext
IProviderUserRepository providerUserRepository, Guid userId);
Task ProviderIdForOrg(Guid orgId);
+ bool AccessSecretsManager(Guid organizationId);
}
diff --git a/src/Core/Entities/OrganizationConnection.cs b/src/Core/Entities/OrganizationConnection.cs
index cc07177384..5b466fb4a6 100644
--- a/src/Core/Entities/OrganizationConnection.cs
+++ b/src/Core/Entities/OrganizationConnection.cs
@@ -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 : OrganizationConnection where T : new()
+public class OrganizationConnection : OrganizationConnection where T : IConnectionConfig
{
public new T Config
{
get => base.GetConfig();
- set => base.SetConfig(value);
+ set => base.SetConfig(value);
}
}
@@ -26,7 +27,7 @@ public class OrganizationConnection : ITableObject
Id = CoreHelpers.GenerateComb();
}
- public T GetConfig() where T : new()
+ public T GetConfig() where T : IConnectionConfig
{
try
{
@@ -38,8 +39,32 @@ public class OrganizationConnection : ITableObject
}
}
- public void SetConfig(T config) where T : new()
+ public void SetConfig(T config) where T : IConnectionConfig
{
Config = JsonSerializer.Serialize(config);
}
+
+ public bool Validate(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();
+ if (config == null)
+ {
+ exception = $"Error parsing Connection config for organization {OrganizationId}";
+ return false;
+ }
+
+ return config.Validate(out exception);
+ }
}
diff --git a/src/Core/Entities/OrganizationUser.cs b/src/Core/Entities/OrganizationUser.cs
index ee1bdc15d4..9e2efb2626 100644
--- a/src/Core/Entities/OrganizationUser.cs
+++ b/src/Core/Entities/OrganizationUser.cs
@@ -22,6 +22,7 @@ public class OrganizationUser : ITableObject, 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()
{
diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs
index 65af18c449..3b59729c14 100644
--- a/src/Core/Entities/User.cs
+++ b/src/Core/Entities/User.cs
@@ -65,6 +65,10 @@ public class User : ITableObject, 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()
{
diff --git a/src/Core/Identity/Claims.cs b/src/Core/Identity/Claims.cs
index b56b23ad86..318f0b4009 100644
--- a/src/Core/Identity/Claims.cs
+++ b/src/Core/Identity/Claims.cs
@@ -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";
diff --git a/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs
new file mode 100644
index 0000000000..365d88877e
--- /dev/null
+++ b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs
@@ -0,0 +1,7 @@
+namespace Bit.Core.Models.Api.OrganizationLicenses;
+
+public class SelfHostedOrganizationLicenseRequestModel
+{
+ public string LicenseKey { get; set; }
+ public string BillingSyncKey { get; set; }
+}
diff --git a/src/Core/Models/Business/OrganizationUserInvite.cs b/src/Core/Models/Business/OrganizationUserInvite.cs
index 78edfb267e..7102a5f9ef 100644
--- a/src/Core/Models/Business/OrganizationUserInvite.cs
+++ b/src/Core/Models/Business/OrganizationUserInvite.cs
@@ -8,6 +8,7 @@ public class OrganizationUserInvite
public IEnumerable 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 Collections { get; set; }
public IEnumerable 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;
diff --git a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs
index 3a3edaed45..7a9aa77110 100644
--- a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs
+++ b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs
@@ -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 where T : new()
+public class OrganizationConnectionData where T : IConnectionConfig
{
public Guid? Id { get; set; }
public OrganizationConnectionType Type { get; set; }
diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs
index 887b64e963..f8789fe5d5 100644
--- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs
+++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs
@@ -7,6 +7,7 @@ public class OrganizationUserInviteData
public IEnumerable Emails { get; set; }
public OrganizationUserType? Type { get; set; }
public bool AccessAll { get; set; }
+ public bool AccessSecretsManager { get; set; }
public IEnumerable Collections { get; set; }
public IEnumerable Groups { get; set; }
public Permissions Permissions { get; set; }
diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs
index e104dd6213..32b7003700 100644
--- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs
+++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs
@@ -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; }
}
diff --git a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
index b155118161..74e06182bf 100644
--- a/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
+++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
@@ -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; }
diff --git a/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
index e121962e6a..2d06fc4296 100644
--- a/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
+++ b/src/Core/Models/Data/Provider/ProviderUserOrganizationDetails.cs
@@ -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; }
}
diff --git a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs
index 204e165d05..07f07093d2 100644
--- a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs
+++ b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs
@@ -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;
+ }
}
diff --git a/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs
new file mode 100644
index 0000000000..9b02c359e4
--- /dev/null
+++ b/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs
@@ -0,0 +1,6 @@
+namespace Bit.Core.Models.OrganizationConnectionConfigs;
+
+public interface IConnectionConfig
+{
+ bool Validate(out string exception);
+}
diff --git a/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs
index 63a1606cb2..8a4fcb4e8c 100644
--- a/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs
+++ b/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs
@@ -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;
+ }
}
diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs
index e3f308bc57..6c019001c0 100644
--- a/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs
@@ -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 CreateAsync(OrganizationConnectionData connectionData) where T : new()
+ public async Task CreateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig
{
return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity());
}
diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs
index b31920b10a..a97be09c3b 100644
--- a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs
@@ -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 CreateAsync(OrganizationConnectionData connectionData) where T : new();
+ Task CreateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig;
}
diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs
index 742e89c970..b245b89693 100644
--- a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs
@@ -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 UpdateAsync(OrganizationConnectionData connectionData) where T : new();
+ Task UpdateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig;
}
diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs
similarity index 64%
rename from src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs
rename to src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs
index 53e926903f..9fb979548a 100644
--- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs
@@ -1,6 +1,6 @@
using Bit.Core.Entities;
-namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
+namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface IValidateBillingSyncKeyCommand
{
diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs
index 0d872b6f1f..3e64fd47ab 100644
--- a/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs
@@ -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 UpdateAsync(OrganizationConnectionData connectionData) where T : new()
+ public async Task UpdateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig
{
if (!connectionData.Id.HasValue)
{
diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs
similarity index 70%
rename from src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs
rename to src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs
index 19c4398a70..a764bbcf23 100644
--- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs
@@ -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;
}
diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs
new file mode 100644
index 0000000000..ff8a6d34fb
--- /dev/null
+++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs
@@ -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 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);
+ }
+}
diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs
new file mode 100644
index 0000000000..2c66833e63
--- /dev/null
+++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs
@@ -0,0 +1,15 @@
+using Bit.Core.Entities;
+using Bit.Core.Models.Business;
+
+namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
+
+public interface ICloudGetOrganizationLicenseQuery
+{
+ Task GetLicenseAsync(Organization organization, Guid installationId,
+ int? version = null);
+}
+
+public interface ISelfHostedGetOrganizationLicenseQuery
+{
+ Task GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection);
+}
diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs
new file mode 100644
index 0000000000..84ae0a27db
--- /dev/null
+++ b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs
@@ -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 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 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(out var exception))
+ {
+ throw new BadRequestException(exception);
+ }
+
+ var billingSyncConfig = billingSyncConnection.GetConfig();
+ var cloudOrganizationId = billingSyncConfig.CloudOrganizationId;
+
+ var response = await SendAsync(
+ 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;
+ }
+}
diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
index 18c2f44dc9..b8e2a775e2 100644
--- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
+++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
@@ -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();
}
+ private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ }
+
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton>(serviceProvider =>
diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs
index eed143838e..0d22b53bad 100644
--- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs
@@ -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(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();
- 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();
var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId))
.ToDictionary(i => i.SponsoringOrganizationUserId);
if (!organizationSponsorshipsDict.Any())
diff --git a/src/Core/Repositories/IProviderUserRepository.cs b/src/Core/Repositories/IProviderUserRepository.cs
index 4a5db368ee..ba920a575d 100644
--- a/src/Core/Repositories/IProviderUserRepository.cs
+++ b/src/Core/Repositories/IProviderUserRepository.cs
@@ -11,7 +11,7 @@ public interface IProviderUserRepository : IRepository
Task> GetManyByUserAsync(Guid userId);
Task GetByProviderUserAsync(Guid providerId, Guid userId);
Task> GetManyByProviderAsync(Guid providerId, ProviderUserType? type = null);
- Task> GetManyDetailsByProviderAsync(Guid providerId);
+ Task> GetManyDetailsByProviderAsync(Guid providerId, ProviderUserStatusType? status = null);
Task> GetManyDetailsByUserAsync(Guid userId,
ProviderUserStatusType? status = null);
Task> GetManyOrganizationDetailsByUserAsync(Guid userId, ProviderUserStatusType? status = null);
diff --git a/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/ICreateServiceAccountCommand.cs b/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/ICreateServiceAccountCommand.cs
index 42eaf6824f..48e368b842 100644
--- a/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/ICreateServiceAccountCommand.cs
+++ b/src/Core/SecretsManager/Commands/ServiceAccounts/Interfaces/ICreateServiceAccountCommand.cs
@@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
public interface ICreateServiceAccountCommand
{
- Task CreateAsync(ServiceAccount serviceAccount);
+ Task CreateAsync(ServiceAccount serviceAccount, Guid userId);
}
diff --git a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs
index 3450fdc800..dbe05074fa 100644
--- a/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs
+++ b/src/Core/SecretsManager/Repositories/IAccessPolicyRepository.cs
@@ -8,7 +8,8 @@ public interface IAccessPolicyRepository
Task> CreateManyAsync(List baseAccessPolicies);
Task AccessPolicyExists(BaseAccessPolicy baseAccessPolicy);
Task GetByIdAsync(Guid id);
- Task?> GetManyByProjectId(Guid id);
+ Task> GetManyByGrantedProjectIdAsync(Guid id);
+ Task> GetManyByGrantedServiceAccountIdAsync(Guid id);
Task ReplaceAsync(BaseAccessPolicy baseAccessPolicy);
Task DeleteAsync(Guid id);
}
diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs
index f3f3f16139..5383c1a3e3 100644
--- a/src/Core/Services/IOrganizationService.cs
+++ b/src/Core/Services/IOrganizationService.cs
@@ -56,9 +56,6 @@ public interface IOrganizationService
IEnumerable organizationUserIds, Guid? deletingUserId);
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds, Guid? loggedInUserId);
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
- Task GenerateLicenseAsync(Guid organizationId, Guid installationId);
- Task GenerateLicenseAsync(Organization organization, Guid installationId,
- int? version = null);
Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable groups,
IEnumerable newUsers, IEnumerable removeUserExternalIds,
bool overwriteExisting);
diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs
index f2b6f8b520..b2564a9432 100644
--- a/src/Core/Services/Implementations/OrganizationService.cs
+++ b/src/Core/Services/Implementations/OrganizationService.cs
@@ -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 _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 logger)
+ ILogger 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 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 GenerateLicenseAsync(Guid organizationId, Guid installationId)
- {
- var organization = await GetOrgById(organizationId);
- return await GenerateLicenseAsync(organization, installationId);
- }
-
- public async Task 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 InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections,
IEnumerable groups)
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index fed83f0a0c..3045797490 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -561,10 +561,13 @@ public class UserService : UserManager, 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, 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, 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, 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;
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index b925550d77..27e4551611 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -692,6 +692,15 @@ public static class CoreHelpers
default:
break;
}
+
+ // Secrets Manager
+ foreach (var org in group)
+ {
+ if (org.AccessSecretsManager)
+ {
+ claims.Add(new KeyValuePair(Claims.SecretsManagerAccess, org.Id.ToString()));
+ }
+ }
}
}
diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs
index 9aee5425cd..50147fcda5 100644
--- a/src/Events/Startup.cs
+++ b/src/Events/Startup.cs
@@ -29,6 +29,9 @@ public class Startup
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
+ // Data Protection
+ services.AddCustomDataProtectionServices(Environment, globalSettings);
+
// Repositories
services.AddDatabaseRepositories(globalSettings);
diff --git a/src/Identity/IdentityServer/ApiResources.cs b/src/Identity/IdentityServer/ApiResources.cs
index 5d212f99bf..d23c06d7db 100644
--- a/src/Identity/IdentityServer/ApiResources.cs
+++ b/src/Identity/IdentityServer/ApiResources.cs
@@ -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 }),
diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs
index bef1f39741..9f40e56000 100644
--- a/src/Infrastructure.Dapper/DapperHelpers.cs
+++ b/src/Infrastructure.Dapper/DapperHelpers.cs
@@ -59,7 +59,7 @@ public static class DapperHelpers
public static DataTable ToTvp(this IEnumerable orgUsers)
{
var table = new DataTable();
- table.SetTypeName("[dbo].[OrganizationUserType]");
+ table.SetTypeName("[dbo].[OrganizationUserType2]");
var columnData = new List<(string name, Type type, Func 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);
diff --git a/src/Infrastructure.Dapper/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Repositories/CipherRepository.cs
index 19c895d9fd..8045aaf660 100644
--- a/src/Infrastructure.Dapper/Repositories/CipherRepository.cs
+++ b/src/Infrastructure.Dapper/Repositories/CipherRepository.cs
@@ -321,6 +321,8 @@ public class CipherRepository : Repository, 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();
}
diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs
index 60c6c204c7..ef3d5bfbbd 100644
--- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs
+++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs
@@ -405,7 +405,7 @@ public class OrganizationUserRepository : Repository, 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, IO
using (var connection = new SqlConnection(_marsConnectionString))
{
var results = await connection.ExecuteAsync(
- $"[{Schema}].[{Table}_UpdateMany]",
+ $"[{Schema}].[{Table}_UpdateMany2]",
new { OrganizationUsersInput = orgUsersTVP },
commandType: CommandType.StoredProcedure);
}
diff --git a/src/Infrastructure.Dapper/Repositories/ProviderUserRepository.cs b/src/Infrastructure.Dapper/Repositories/ProviderUserRepository.cs
index 0e1138e146..a0e2b69896 100644
--- a/src/Infrastructure.Dapper/Repositories/ProviderUserRepository.cs
+++ b/src/Infrastructure.Dapper/Repositories/ProviderUserRepository.cs
@@ -84,13 +84,13 @@ public class ProviderUserRepository : Repository, IProviderU
}
}
- public async Task> GetManyDetailsByProviderAsync(Guid providerId)
+ public async Task