1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-17 02:01:53 +01:00

Merge branch 'main' into auth/pm-6631/handle-webauthn-creation-exception

This commit is contained in:
Todd Martin 2024-05-14 14:42:21 -04:00
commit bad8e6b988
No known key found for this signature in database
GPG Key ID: 663E7AF5C839BC8F
464 changed files with 42498 additions and 7511 deletions

View File

@ -40,19 +40,26 @@
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]
},
{
"matchPackageNames": ["bootstrap", "del", "gulp"],
"matchUpdateTypes": ["major"],
"description": "Lock bootstrap, del, and gulp major versions due to ASP.NET conflicts",
"enabled": false
},
{
"matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"DuoUniversal",
"Fido2.AspNet",
"Duende.IdentityServer",
"Microsoft.Azure.Cosmos",
"Microsoft.Azure.Cosmos.Table",
"Microsoft.Extensions.Caching.StackExchangeRedis",
"Microsoft.Extensions.Identity.Stores",
"Otp.NET",

View File

@ -540,36 +540,11 @@ jobs:
steps:
- name: Check if any job failed
if: |
github.ref == 'refs/heads/main'
(github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc'
env:
LINT_STATUS: ${{ needs.lint.result }}
TESTING_STATUS: ${{ needs.testing.result }}
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
UPLOAD_STATUS: ${{ needs.upload.result }}
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
run: |
if [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$TESTING_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
exit 1
elif [ "$UPLOAD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_MSSQLMIGRATORUTILITY_STATUS" = "failure" ]; then
exit 1
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
exit 1
elif [ "$TRIGGER_K8S_DEPLOY_STATUS" = "failure" ]; then
exit 1
fi
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7

View File

@ -5,36 +5,26 @@ on:
pull_request:
types: [closed]
env:
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
jobs:
build-docker:
name: Remove branch-specific Docker images
runs-on: ubuntu-22.04
steps:
########## ACR ##########
- name: Log in to Azure - QA Subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Log in to Azure ACR
run: az acr login -n bitwardenqa
- name: Log in to Azure - production subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Log in to Azure ACR
run: az acr login -n bitwardenprod
run: az acr login -n $_AZ_REGISTRY --only-show-errors
########## Remove Docker images ##########
- name: Remove the Docker image from ACR
env:
REF: ${{ github.event.pull_request.head.ref }}
REGISTRIES: |
registries:
- bitwardenprod
- bitwardenqa
SERVICES: |
services:
- Admin
@ -54,24 +44,21 @@ jobs:
run: |
for SERVICE in $(echo "${{ env.SERVICES }}" | yq e ".services[]" - )
do
for REGISTRY in $( echo "${{ env.REGISTRIES }}" | yq e ".registries[]" - )
do
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
SERVICE_NAME=$(echo $SERVICE | awk '{print tolower($0)}')
IMAGE_TAG=$(echo "${REF}" | sed "s#/#-#g") # slash safe branch name
echo "[*] Checking if remote exists: $REGISTRY.azurecr.io/$SERVICE_NAME:$IMAGE_TAG"
TAG_EXISTS=$(
az acr repository show-tags --name $REGISTRY --repository $SERVICE_NAME \
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
)
echo "[*] Checking if remote exists: $_AZ_REGISTRY/$SERVICE_NAME:$IMAGE_TAG"
TAG_EXISTS=$(
az acr repository show-tags --name $_AZ_REGISTRY --repository $SERVICE_NAME \
| jq --arg $TAG "$IMAGE_TAG" -e '. | any(. == "$TAG")'
)
if [[ "$TAG_EXISTS" == "true" ]]; then
echo "[*] Tag exists. Removing tag"
az acr repository delete --name $REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
else
echo "[*] Tag does not exist. No action needed"
fi
done
if [[ "$TAG_EXISTS" == "true" ]]; then
echo "[*] Tag exists. Removing tag"
az acr repository delete --name $_AZ_REGISTRY --image $SERVICE_NAME:$IMAGE_TAG --yes
else
echo "[*] Tag does not exist. No action needed"
fi
done
- name: Log out of Docker

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

@ -0,0 +1,53 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

View File

@ -69,20 +69,15 @@ jobs:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
needs:
- purge
needs: [purge]
steps:
- name: Check if any job failed
if: |
github.ref == 'refs/heads/main'
(github.ref == 'refs/heads/main'
|| github.ref == 'refs/heads/rc'
|| github.ref == 'refs/heads/hotfix-rc'
env:
PURGE_STATUS: ${{ needs.purge.result }}
run: |
if [ "$PURGE_STATUS" = "failure" ]; then
exit 1
fi
|| github.ref == 'refs/heads/hotfix-rc')
&& contains(needs.*.result, 'failure')
run: exit 1
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7

View File

@ -7,47 +7,63 @@ on:
- "main"
- "rc"
- "hotfix-rc"
pull_request:
permissions: read-all
pull_request_target:
types: [opened, synchronize]
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
env:
INCREMENTAL: "${{ github.event_name == 'pull_request' && '--sast-incremental' || '' }}"
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1

View File

@ -57,9 +57,9 @@ jobs:
run: sleep 15s
- name: Migrate SQL Server
working-directory: "dev"
run: "./migrate.ps1"
shell: pwsh
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
env:
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Migrate MySQL
working-directory: "util/MySqlMigrations"
@ -147,9 +147,9 @@ jobs:
shell: pwsh
- name: Migrate
working-directory: "dev"
run: "./migrate.ps1"
shell: pwsh
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
env:
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
- name: Diff .sqlproj to migrations
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True

View File

@ -1,13 +1,12 @@
---
name: Bump version
run-name: Bump version to ${{ inputs.version_number }}
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New version (example: '2024.1.0')"
required: true
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
@ -16,22 +15,16 @@ on:
jobs:
bump_version:
name: Bump
name: Bump Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
- name: Validate version input
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
version: ${{ inputs.version_number_override }}
- name: Check out branch
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
@ -48,6 +41,20 @@ jobs:
exit 1
fi
- name: Log in to Azure - CI subscription
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase,
github-pat-bitwarden-devops-bot-repo-scope"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
with:
@ -56,22 +63,35 @@ jobs:
git_user_signingkey: true
git_commit_gpgsign: true
- name: Set up Git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Install xmllint
run: sudo apt install -y libxml2-utils
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify input version
env:
NEW_VERSION: ${{ inputs.version_number }}
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
@ -87,16 +107,37 @@ jobs:
exit 1
fi
- name: Bump version props
- name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump version props - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
version: ${{ inputs.version_number }}
file_path: "Directory.Build.props"
version: ${{ inputs.version_number_override }}
- name: Set up Git
- name: Bump version props - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed
id: version-changed
@ -110,7 +151,7 @@ jobs:
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
@ -124,7 +165,7 @@ jobs:
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ inputs.version_number }}"
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
@ -140,38 +181,43 @@ jobs:
- [X] Other
## Objective
Automated version bump to ${{ inputs.version_number }}")
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cut_rc:
name: Cut RC branch
needs: bump_version
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Check out branch
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: main
- name: Install xmllint
run: sudo apt install -y libxml2-utils
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated
env:
NEW_VERSION: ${{ inputs.version_number }}
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do

56
.vscode/launch.json vendored
View File

@ -38,6 +38,7 @@
"configurations": [
"run-Admin",
"run-API",
"run-Events",
"run-EventsProcessor",
"run-Identity",
"run-Sso",
@ -58,6 +59,7 @@
"configurations": [
"run-Admin-SelfHost",
"run-API-SelfHost",
"run-Events-SelfHost",
"run-EventsProcessor-SelfHost",
"run-Identity-SelfHost",
"run-Sso-SelfHost",
@ -76,6 +78,7 @@
"configurations": [
"run-Admin-SelfHost",
"run-API-SelfHost",
"run-Events-SelfHost",
"run-EventsProcessor-SelfHost",
"run-Identity-SelfHost",
],
@ -120,6 +123,17 @@
},
"preLaunchTask": "buildBilling",
},
{
"name": "Events",
"configurations": [
"run-Events"
],
"presentation": {
"hidden": false,
"group": "cloud",
},
"preLaunchTask": "buildEvents",
},
{
"name": "Events Processor",
"configurations": [
@ -341,6 +355,25 @@
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-Events",
"presentation": {
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Events",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-EventsProcessor",
"presentation": {
@ -505,6 +538,27 @@
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-Events-SelfHost",
"presentation": {
"hidden": true,
},
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/Events/bin/Debug/net8.0/Events.dll",
"args": [],
"cwd": "${workspaceFolder}/src/Events",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:46274",
"developSelfHosted": "true",
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "run-EventsProcessor-SelfHost",
"presentation": {
@ -519,7 +573,7 @@
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:46274",
"ASPNETCORE_URLS": "http://localhost:54103",
"developSelfHosted": "true",
},
"sourceFileMap": {

12
.vscode/tasks.json vendored
View File

@ -96,6 +96,18 @@
],
"problemMatcher": "$msCompile"
},
{
"label": "buildEvents",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Events/Events.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "buildEventsProcessor",
"command": "dotnet",

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2024.2.3</Version>
<Version>2024.5.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -1,10 +1,15 @@
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.AdminConsole.Providers;
@ -14,20 +19,26 @@ public class CreateProviderCommand : ICreateProviderCommand
private readonly IProviderUserRepository _providerUserRepository;
private readonly IProviderService _providerService;
private readonly IUserRepository _userRepository;
private readonly IProviderPlanRepository _providerPlanRepository;
private readonly IFeatureService _featureService;
public CreateProviderCommand(
IProviderRepository providerRepository,
IProviderUserRepository providerUserRepository,
IProviderService providerService,
IUserRepository userRepository)
IUserRepository userRepository,
IProviderPlanRepository providerPlanRepository,
IFeatureService featureService)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
_providerService = providerService;
_userRepository = userRepository;
_providerPlanRepository = providerPlanRepository;
_featureService = featureService;
}
public async Task CreateMspAsync(Provider provider, string ownerEmail)
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
{
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
if (owner == null)
@ -35,6 +46,13 @@ public class CreateProviderCommand : ICreateProviderCommand
throw new BadRequestException("Invalid owner. Owner must be an existing Bitwarden user.");
}
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (isConsolidatedBillingEnabled)
{
provider.Gateway = GatewayType.Stripe;
}
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Pending);
var providerUser = new ProviderUser
@ -44,6 +62,21 @@ public class CreateProviderCommand : ICreateProviderCommand
Type = ProviderUserType.ProviderAdmin,
Status = ProviderUserStatusType.Confirmed,
};
if (isConsolidatedBillingEnabled)
{
var providerPlans = new List<ProviderPlan>
{
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
};
foreach (var providerPlan in providerPlans)
{
await _providerPlanRepository.CreateAsync(providerPlan);
}
}
await _providerUserRepository.CreateAsync(providerUser);
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
}
@ -60,4 +93,16 @@ public class CreateProviderCommand : ICreateProviderCommand
provider.UseEvents = true;
await _providerRepository.CreateAsync(provider);
}
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
{
return new ProviderPlan
{
ProviderId = providerId,
PlanType = planType,
SeatMinimum = seatMinimum,
PurchasedSeats = 0,
AllocatedSeats = 0
};
}
}

View File

@ -1,11 +1,17 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
using Stripe;
@ -20,6 +26,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IOrganizationService _organizationService;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IStripeAdapter _stripeAdapter;
private readonly IScaleSeatsCommand _scaleSeatsCommand;
private readonly IFeatureService _featureService;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
@ -28,7 +36,9 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IOrganizationRepository organizationRepository,
IOrganizationService organizationService,
IProviderOrganizationRepository providerOrganizationRepository,
IStripeAdapter stripeAdapter)
IStripeAdapter stripeAdapter,
IScaleSeatsCommand scaleSeatsCommand,
IFeatureService featureService)
{
_eventService = eventService;
_logger = logger;
@ -37,6 +47,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_organizationService = organizationService;
_providerOrganizationRepository = providerOrganizationRepository;
_stripeAdapter = stripeAdapter;
_scaleSeatsCommand = scaleSeatsCommand;
_featureService = featureService;
}
public async Task RemoveOrganizationFromProvider(
@ -65,8 +77,35 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
organization.BillingEmail = organizationOwnerEmails.MinBy(email => email);
await ResetOrganizationBillingAsync(organization, provider, organizationOwnerEmails);
await _organizationRepository.ReplaceAsync(organization);
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
/// <summary>
/// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled
/// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because
/// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly,
/// we email the organization owners letting them know they need to add a new payment method.
/// </summary>
private async Task ResetOrganizationBillingAsync(
Organization organization,
Provider provider,
IEnumerable<string> organizationOwnerEmails)
{
if (!organization.IsStripeEnabled())
{
return;
}
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
var customerUpdateOptions = new CustomerUpdateOptions
{
Coupon = string.Empty,
@ -75,24 +114,47 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, customerUpdateOptions);
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
if (isConsolidatedBillingEnabled && provider.Status == ProviderStatusType.Billable)
{
CollectionMethod = "send_invoice",
DaysUntilDue = 30
};
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
Customer = organization.GatewayCustomerId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary<string, string>
{
{ "organizationId", organization.Id.ToString() }
},
OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
};
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
organization.GatewaySubscriptionId = subscription.Id;
await _scaleSeatsCommand.ScalePasswordManagerSeats(provider, organization.PlanType,
-(organization.Seats ?? 0));
}
else
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
CollectionMethod = "send_invoice",
DaysUntilDue = 30
};
await _stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId, subscriptionUpdateOptions);
}
await _mailService.SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
organizationOwnerEmails);
await _providerOrganizationRepository.DeleteAsync(providerOrganization);
await _eventService.LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
}

View File

@ -4,8 +4,10 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -15,8 +17,10 @@ using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.DataProtection;
using Stripe;
namespace Bit.Commercial.Core.AdminConsole.Services;
@ -37,13 +41,18 @@ public class ProviderService : IProviderService
private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext;
private readonly IStripeAdapter _stripeAdapter;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
private readonly IApplicationCacheService _applicationCacheService;
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
IUserService userService, IOrganizationService organizationService, IMailService mailService,
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
ICurrentContext currentContext, IStripeAdapter stripeAdapter)
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
IApplicationCacheService applicationCacheService)
{
_providerRepository = providerRepository;
_providerUserRepository = providerUserRepository;
@ -58,6 +67,9 @@ public class ProviderService : IProviderService
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
_currentContext = currentContext;
_stripeAdapter = stripeAdapter;
_featureService = featureService;
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
_applicationCacheService = applicationCacheService;
}
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
@ -257,7 +269,7 @@ public class ProviderService : IProviderService
await _providerUserRepository.ReplaceAsync(providerUser);
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);
result.Add(Tuple.Create(providerUser, ""));
}
catch (BadRequestException e)
@ -331,7 +343,7 @@ public class ProviderService : IProviderService
var email = user == null ? providerUser.Email : user.Email;
if (!string.IsNullOrWhiteSpace(email))
{
await _mailService.SendProviderUserRemoved(provider.Name, email);
await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
}
result.Add(Tuple.Create(providerUser, ""));
@ -359,6 +371,7 @@ public class ProviderService : IProviderService
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
ThrowOnInvalidPlanType(organization.PlanType);
if (organization.UseSecretsManager)
@ -374,8 +387,22 @@ public class ProviderService : IProviderService
Key = key,
};
await ApplyProviderPriceRateAsync(organizationId, providerId);
var provider = await _providerRepository.GetByIdAsync(providerId);
await ApplyProviderPriceRateAsync(organization, provider);
await _providerOrganizationRepository.CreateAsync(providerOrganization);
organization.BillingEmail = provider.BillingEmail;
await _organizationRepository.ReplaceAsync(organization);
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Email = provider.BillingEmail
});
}
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
}
@ -400,16 +427,14 @@ public class ProviderService : IProviderService
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
}
private async Task ApplyProviderPriceRateAsync(Guid organizationId, Guid providerId)
private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
{
return;
}
var organization = await _organizationRepository.GetByIdAsync(organizationId);
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
var extractedPlanType = PlanTypeMappings(organization);
if (subscriptionItem != null)
@ -494,9 +519,15 @@ public class ProviderService : IProviderService
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
{
ThrowOnInvalidPlanType(organizationSignup.Plan);
var provider = await _providerRepository.GetByIdAsync(providerId);
var (organization, _, defaultCollection) = await _organizationService.SignUpAsync(organizationSignup, true);
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled);
var (organization, _, defaultCollection) = consolidatedBillingEnabled
? await _organizationService.SignupClientAsync(organizationSignup)
: await _organizationService.SignUpAsync(organizationSignup, true);
var providerOrganization = new ProviderOrganization
{
@ -581,12 +612,50 @@ public class ProviderService : IProviderService
}
}
public async Task InitiateDeleteAsync(Provider provider, string providerAdminEmail)
{
if (string.IsNullOrWhiteSpace(provider.Name))
{
throw new BadRequestException("Provider name not found.");
}
var providerAdmin = await _userRepository.GetByEmailAsync(providerAdminEmail);
if (providerAdmin == null)
{
throw new BadRequestException("Provider admin not found.");
}
var providerAdminOrgUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id);
if (providerAdminOrgUser == null || providerAdminOrgUser.Status != ProviderUserStatusType.Confirmed ||
providerAdminOrgUser.Type != ProviderUserType.ProviderAdmin)
{
throw new BadRequestException("Org admin not found.");
}
var token = _providerDeleteTokenDataFactory.Protect(new ProviderDeleteTokenable(provider, 1));
await _mailService.SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, token);
}
public async Task DeleteAsync(Provider provider, string token)
{
if (!_providerDeleteTokenDataFactory.TryUnprotect(token, out var data) || !data.IsValid(provider))
{
throw new BadRequestException("Invalid token.");
}
await DeleteAsync(provider);
}
public async Task DeleteAsync(Provider provider)
{
await _providerRepository.DeleteAsync(provider);
await _applicationCacheService.DeleteProviderAbilityAsync(provider.Id);
}
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
{
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
var token = _dataProtector.Protect(
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);
}
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
@ -598,8 +667,13 @@ public class ProviderService : IProviderService
return confirmedOwnersIds.Except(providerUserIds).Any();
}
private void ThrowOnInvalidPlanType(PlanType requestedType)
private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false)
{
if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
{
throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
}
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
{
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");

View File

@ -1,267 +0,0 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class AccessPolicyAuthorizationHandler : AuthorizationHandler<AccessPolicyOperationRequirement, BaseAccessPolicy>
{
private readonly ICurrentContext _currentContext;
private readonly IAccessClientQuery _accessClientQuery;
private readonly IGroupRepository _groupRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public AccessPolicyAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IGroupRepository groupRepository,
IOrganizationUserRepository organizationUserRepository,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_groupRepository = groupRepository;
_organizationUserRepository = organizationUserRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement,
BaseAccessPolicy resource)
{
switch (requirement)
{
case not null when requirement == AccessPolicyOperations.Create:
await CanCreateAccessPolicyAsync(context, requirement, resource);
break;
case not null when requirement == AccessPolicyOperations.Update:
await CanUpdateAccessPolicyAsync(context, requirement, resource);
break;
case not null when requirement == AccessPolicyOperations.Delete:
await CanDeleteAccessPolicyAsync(context, requirement, resource);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanCreateAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
switch (resource)
{
case UserProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case GroupProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case ServiceAccountProjectAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case UserServiceAccountAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
case GroupServiceAccountAccessPolicy ap:
await CanCreateAsync(context, requirement, ap);
break;
default:
throw new ArgumentException("Unsupported access policy type provided.");
}
}
private async Task CanUpdateAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
var access = await GetAccessPolicyAccessAsync(context, resource);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanDeleteAccessPolicyAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, BaseAccessPolicy resource)
{
var access = await GetAccessPolicyAccessAsync(context, resource);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, UserProjectAccessPolicy resource)
{
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
if (user.OrganizationId != resource.GrantedProject?.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, GroupProjectAccessPolicy resource)
{
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
if (group.OrganizationId != resource.GrantedProject?.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedProject!.OrganizationId, resource.GrantedProjectId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, ServiceAccountProjectAccessPolicy resource)
{
var projectOrganizationId = resource.GrantedProject?.OrganizationId;
var serviceAccountOrgId = resource.ServiceAccount?.OrganizationId;
if (projectOrganizationId == null)
{
var project = await _projectRepository.GetByIdAsync(resource.GrantedProjectId!.Value);
projectOrganizationId = project?.OrganizationId;
}
if (serviceAccountOrgId == null)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(resource.ServiceAccountId!.Value);
serviceAccountOrgId = serviceAccount?.OrganizationId;
}
if (!serviceAccountOrgId.HasValue || !projectOrganizationId.HasValue ||
serviceAccountOrgId != projectOrganizationId)
{
return;
}
var access = await GetAccessAsync(context, projectOrganizationId.Value, resource.GrantedProjectId,
resource.ServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, UserServiceAccountAccessPolicy resource)
{
var user = await _organizationUserRepository.GetByIdAsync(resource.OrganizationUserId!.Value);
if (user.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task CanCreateAsync(AuthorizationHandlerContext context,
AccessPolicyOperationRequirement requirement, GroupServiceAccountAccessPolicy resource)
{
var group = await _groupRepository.GetByIdAsync(resource.GroupId!.Value);
if (group.OrganizationId != resource.GrantedServiceAccount!.OrganizationId)
{
return;
}
var access = await GetAccessAsync(context, resource.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: resource.GrantedServiceAccountId);
if (access.Write)
{
context.Succeed(requirement);
}
}
private async Task<(bool Read, bool Write)> GetAccessPolicyAccessAsync(AuthorizationHandlerContext context,
BaseAccessPolicy resource) =>
resource switch
{
UserProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
GroupProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
ServiceAccountProjectAccessPolicy ap => await GetAccessAsync(context, ap.GrantedProject!.OrganizationId,
ap.GrantedProjectId),
UserServiceAccountAccessPolicy ap => await GetAccessAsync(context, ap.GrantedServiceAccount!.OrganizationId,
serviceAccountIdToCheck: ap.GrantedServiceAccountId),
GroupServiceAccountAccessPolicy ap => await GetAccessAsync(context,
ap.GrantedServiceAccount!.OrganizationId, serviceAccountIdToCheck: ap.GrantedServiceAccountId),
_ => throw new ArgumentException("Unsupported access policy type provided."),
};
private async Task<(bool Read, bool Write)> GetAccessAsync(AuthorizationHandlerContext context,
Guid organizationId, Guid? projectIdToCheck = null,
Guid? serviceAccountIdToCheck = null)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
return (false, false);
}
var (accessClient, userId) = await _accessClientQuery.GetAccessClientAsync(context.User, organizationId);
// Only users and admins should be able to manipulate access policies
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return (false, false);
}
if (projectIdToCheck.HasValue && serviceAccountIdToCheck.HasValue)
{
var projectAccess =
await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
var serviceAccountAccess =
await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
accessClient);
return (
projectAccess.Read && serviceAccountAccess.Read,
projectAccess.Write && serviceAccountAccess.Write);
}
if (projectIdToCheck.HasValue)
{
return await _projectRepository.AccessToProjectAsync(projectIdToCheck.Value, userId, accessClient);
}
if (serviceAccountIdToCheck.HasValue)
{
return await _serviceAccountRepository.AccessToServiceAccountAsync(serviceAccountIdToCheck.Value, userId,
accessClient);
}
throw new ArgumentException("No ID to check provided.");
}
}

View File

@ -0,0 +1,107 @@
#nullable enable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandler : AuthorizationHandler<
ProjectServiceAccountsAccessPoliciesOperationRequirement,
ProjectServiceAccountsAccessPoliciesUpdates>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ProjectServiceAccountsAccessPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ProjectServiceAccountsAccessPoliciesOperations.Updates:
await CanUpdateAsync(context, requirement, resource, accessClient,
userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
ProjectServiceAccountsAccessPoliciesOperationRequirement requirement,
ProjectServiceAccountsAccessPoliciesUpdates resource,
AccessClientType accessClient, Guid userId)
{
var access =
await _projectRepository.AccessToProjectAsync(resource.ProjectId, userId,
accessClient);
if (!access.Write)
{
return;
}
var serviceAccountIds = resource.ServiceAccountAccessPolicyUpdates.Select(update =>
update.AccessPolicy.ServiceAccountId!.Value).ToList();
var inSameOrganization =
await _serviceAccountRepository.ServiceAccountsAreInOrganizationAsync(serviceAccountIds,
resource.OrganizationId);
if (!inSameOrganization)
{
return;
}
// Users can only create access policies for service accounts they have access to.
// User can delete and update any service account access policy if they have write access to the project.
var serviceAccountIdsToCheck = resource.ServiceAccountAccessPolicyUpdates
.Where(update => update.Operation == AccessPolicyOperation.Create).Select(update =>
update.AccessPolicy.ServiceAccountId!.Value).ToList();
if (serviceAccountIdsToCheck.Count == 0)
{
context.Succeed(requirement);
return;
}
var serviceAccountsAccess =
await _serviceAccountRepository.AccessToServiceAccountsAsync(serviceAccountIdsToCheck, userId,
accessClient);
if (serviceAccountsAccess.Count == serviceAccountIdsToCheck.Count &&
serviceAccountsAccess.All(a => a.Value.Write))
{
context.Succeed(requirement);
}
}
}

View File

@ -0,0 +1,88 @@
#nullable enable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
public class ServiceAccountGrantedPoliciesAuthorizationHandler : AuthorizationHandler<
ServiceAccountGrantedPoliciesOperationRequirement,
ServiceAccountGrantedPoliciesUpdates>
{
private readonly IAccessClientQuery _accessClientQuery;
private readonly ICurrentContext _currentContext;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public ServiceAccountGrantedPoliciesAuthorizationHandler(ICurrentContext currentContext,
IAccessClientQuery accessClientQuery,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_currentContext = currentContext;
_accessClientQuery = accessClientQuery;
_serviceAccountRepository = serviceAccountRepository;
_projectRepository = projectRepository;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement,
ServiceAccountGrantedPoliciesUpdates resource)
{
if (!_currentContext.AccessSecretsManager(resource.OrganizationId))
{
return;
}
// Only users and admins should be able to manipulate access policies
var (accessClient, userId) =
await _accessClientQuery.GetAccessClientAsync(context.User, resource.OrganizationId);
if (accessClient != AccessClientType.User && accessClient != AccessClientType.NoAccessCheck)
{
return;
}
switch (requirement)
{
case not null when requirement == ServiceAccountGrantedPoliciesOperations.Updates:
await CanUpdateAsync(context, requirement, resource, accessClient,
userId);
break;
default:
throw new ArgumentException("Unsupported operation requirement type provided.",
nameof(requirement));
}
}
private async Task CanUpdateAsync(AuthorizationHandlerContext context,
ServiceAccountGrantedPoliciesOperationRequirement requirement, ServiceAccountGrantedPoliciesUpdates resource,
AccessClientType accessClient, Guid userId)
{
var access =
await _serviceAccountRepository.AccessToServiceAccountAsync(resource.ServiceAccountId, userId,
accessClient);
if (access.Write)
{
var projectIdsToCheck = resource.ProjectGrantedPolicyUpdates.Select(update =>
update.AccessPolicy.GrantedProjectId!.Value).ToList();
var sameOrganization =
await _projectRepository.ProjectsAreInOrganization(projectIdsToCheck, resource.OrganizationId);
if (!sameOrganization)
{
return;
}
var projectsAccess =
await _projectRepository.AccessToProjectsAsync(projectIdsToCheck, userId, accessClient);
if (projectsAccess.Count == projectIdsToCheck.Count && projectsAccess.All(a => a.Value.Write))
{
context.Succeed(requirement);
}
}
}
}

View File

@ -1,33 +0,0 @@
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class CreateAccessPoliciesCommand : ICreateAccessPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public CreateAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<IEnumerable<BaseAccessPolicy>> CreateManyAsync(List<BaseAccessPolicy> accessPolicies)
{
await CheckAccessPoliciesDoNotExistAsync(accessPolicies);
return await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
private async Task CheckAccessPoliciesDoNotExistAsync(List<BaseAccessPolicy> accessPolicies)
{
foreach (var accessPolicy in accessPolicies)
{
if (await _accessPolicyRepository.AccessPolicyExists(accessPolicy))
{
throw new BadRequestException("Resource already exists");
}
}
}
}

View File

@ -1,19 +0,0 @@
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class DeleteAccessPolicyCommand : IDeleteAccessPolicyCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public DeleteAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task DeleteAsync(Guid id)
{
await _accessPolicyRepository.DeleteAsync(id);
}
}

View File

@ -1,31 +0,0 @@
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateAccessPolicyCommand : IUpdateAccessPolicyCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateAccessPolicyCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<BaseAccessPolicy> UpdateAsync(Guid id, bool read, bool write)
{
var accessPolicy = await _accessPolicyRepository.GetByIdAsync(id);
if (accessPolicy == null)
{
throw new NotFoundException();
}
accessPolicy.Read = read;
accessPolicy.Write = write;
accessPolicy.RevisionDate = DateTime.UtcNow;
await _accessPolicyRepository.ReplaceAsync(accessPolicy);
return accessPolicy;
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateProjectServiceAccountsAccessPoliciesCommand : IUpdateProjectServiceAccountsAccessPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateProjectServiceAccountsAccessPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task UpdateAsync(ProjectServiceAccountsAccessPoliciesUpdates accessPoliciesUpdates)
{
if (!accessPoliciesUpdates.ServiceAccountAccessPolicyUpdates.Any())
{
return;
}
await _accessPolicyRepository.UpdateProjectServiceAccountsAccessPoliciesAsync(accessPoliciesUpdates);
}
}

View File

@ -0,0 +1,26 @@
#nullable enable
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
public class UpdateServiceAccountGrantedPoliciesCommand : IUpdateServiceAccountGrantedPoliciesCommand
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public UpdateServiceAccountGrantedPoliciesCommand(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task UpdateAsync(ServiceAccountGrantedPoliciesUpdates grantedPoliciesUpdates)
{
if (!grantedPoliciesUpdates.ProjectGrantedPolicyUpdates.Any())
{
return;
}
await _accessPolicyRepository.UpdateServiceAccountGrantedPoliciesAsync(grantedPoliciesUpdates);
}
}

View File

@ -0,0 +1,44 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class ProjectServiceAccountsAccessPoliciesUpdatesQuery : IProjectServiceAccountsAccessPoliciesUpdatesQuery
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public ProjectServiceAccountsAccessPoliciesUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<ProjectServiceAccountsAccessPoliciesUpdates> GetAsync(
ProjectServiceAccountsAccessPolicies projectServiceAccountsAccessPolicies)
{
var currentPolicies =
await _accessPolicyRepository.GetProjectServiceAccountsAccessPoliciesAsync(
projectServiceAccountsAccessPolicies.ProjectId);
if (currentPolicies == null)
{
return new ProjectServiceAccountsAccessPoliciesUpdates
{
ProjectId = projectServiceAccountsAccessPolicies.ProjectId,
OrganizationId = projectServiceAccountsAccessPolicies.OrganizationId,
ServiceAccountAccessPolicyUpdates =
projectServiceAccountsAccessPolicies.ServiceAccountAccessPolicies.Select(p =>
new ServiceAccountProjectAccessPolicyUpdate
{
Operation = AccessPolicyOperation.Create,
AccessPolicy = p
})
};
}
return currentPolicies.GetPolicyUpdates(projectServiceAccountsAccessPolicies);
}
}

View File

@ -0,0 +1,41 @@
#nullable enable
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
public class ServiceAccountGrantedPolicyUpdatesQuery : IServiceAccountGrantedPolicyUpdatesQuery
{
private readonly IAccessPolicyRepository _accessPolicyRepository;
public ServiceAccountGrantedPolicyUpdatesQuery(IAccessPolicyRepository accessPolicyRepository)
{
_accessPolicyRepository = accessPolicyRepository;
}
public async Task<ServiceAccountGrantedPoliciesUpdates> GetAsync(
ServiceAccountGrantedPolicies grantedPolicies)
{
var currentPolicies =
await _accessPolicyRepository.GetServiceAccountGrantedPoliciesAsync(grantedPolicies.ServiceAccountId);
if (currentPolicies == null)
{
return new ServiceAccountGrantedPoliciesUpdates
{
ServiceAccountId = grantedPolicies.ServiceAccountId,
OrganizationId = grantedPolicies.OrganizationId,
ProjectGrantedPolicyUpdates = grantedPolicies.ProjectGrantedPolicies.Select(p =>
new ServiceAccountProjectAccessPolicyUpdate
{
Operation = AccessPolicyOperation.Create,
AccessPolicy = p
})
};
}
return currentPolicies.GetPolicyUpdates(grantedPolicies);
}
}

View File

@ -0,0 +1,50 @@
#nullable enable
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Repositories;
namespace Bit.Commercial.Core.SecretsManager.Queries.Secrets;
public class SecretsSyncQuery : ISecretsSyncQuery
{
private readonly ISecretRepository _secretRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public SecretsSyncQuery(
ISecretRepository secretRepository,
IServiceAccountRepository serviceAccountRepository)
{
_secretRepository = secretRepository;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetAsync(SecretsSyncRequest syncRequest)
{
if (syncRequest.LastSyncedDate == null)
{
return await GetSecretsAsync(syncRequest);
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(syncRequest.ServiceAccountId);
if (serviceAccount == null)
{
throw new NotFoundException();
}
if (syncRequest.LastSyncedDate.Value <= serviceAccount.RevisionDate)
{
return await GetSecretsAsync(syncRequest);
}
return (HasChanges: false, null);
}
private async Task<(bool HasChanges, IEnumerable<Secret>? Secrets)> GetSecretsAsync(SecretsSyncRequest syncRequest)
{
var secrets = await _secretRepository.GetManyByOrganizationIdAsync(syncRequest.OrganizationId,
syncRequest.ServiceAccountId, syncRequest.AccessClientType);
return (HasChanges: true, Secrets: secrets);
}
}

View File

@ -12,6 +12,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Trash;
using Bit.Commercial.Core.SecretsManager.Queries;
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
@ -23,6 +24,7 @@ using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Bit.Core.SecretsManager.Queries.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Queries.Secrets.Interfaces;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
@ -36,13 +38,17 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IAuthorizationHandler, ProjectAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, SecretAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountPeopleAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ServiceAccountGrantedPoliciesAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, ProjectServiceAccountsAccessPoliciesAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
services.AddScoped<ISameOrganizationQuery, SameOrganizationQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<IServiceAccountGrantedPolicyUpdatesQuery, ServiceAccountGrantedPolicyUpdatesQuery>();
services.AddScoped<ISecretsSyncQuery, SecretsSyncQuery>();
services.AddScoped<IProjectServiceAccountsAccessPoliciesUpdatesQuery, ProjectServiceAccountsAccessPoliciesUpdatesQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
@ -55,11 +61,10 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<ICountNewServiceAccountSlotsRequiredQuery, CountNewServiceAccountSlotsRequiredQuery>();
services.AddScoped<IRevokeAccessTokensCommand, RevokeAccessTokensCommand>();
services.AddScoped<ICreateAccessTokenCommand, CreateAccessTokenCommand>();
services.AddScoped<ICreateAccessPoliciesCommand, CreateAccessPoliciesCommand>();
services.AddScoped<IUpdateAccessPolicyCommand, UpdateAccessPolicyCommand>();
services.AddScoped<IDeleteAccessPolicyCommand, DeleteAccessPolicyCommand>();
services.AddScoped<IImportCommand, ImportCommand>();
services.AddScoped<IEmptyTrashCommand, EmptyTrashCommand>();
services.AddScoped<IRestoreTrashCommand, RestoreTrashCommand>();
services.AddScoped<IUpdateServiceAccountGrantedPoliciesCommand, UpdateServiceAccountGrantedPoliciesCommand>();
services.AddScoped<IUpdateProjectServiceAccountsAccessPoliciesCommand, UpdateProjectServiceAccountsAccessPoliciesCommand>();
}
}

View File

@ -1,7 +1,8 @@
using System.Linq.Expressions;
using AutoMapper;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Discriminators;
@ -19,16 +20,13 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
}
private static Expression<Func<ServiceAccountProjectAccessPolicy, bool>> UserHasWriteAccessToProject(Guid userId) =>
policy =>
policy.GrantedProject.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
policy.GrantedProject.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
public async Task<List<Core.SecretsManager.Entities.BaseAccessPolicy>> CreateManyAsync(List<Core.SecretsManager.Entities.BaseAccessPolicy> baseAccessPolicies)
{
using var scope = ServiceScopeFactory.CreateScope();
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = new List<Guid>();
foreach (var baseAccessPolicy in baseAccessPolicies)
{
baseAccessPolicy.SetNewId();
@ -64,160 +62,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
{
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
serviceAccountIds.Add(entity.ServiceAccountId!.Value);
break;
}
}
}
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return baseAccessPolicies;
}
public async Task<bool> AccessPolicyExists(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
switch (baseAccessPolicy)
{
case Core.SecretsManager.Entities.UserProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.UserProjectAccessPolicy
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.GroupProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.GroupProjectAccessPolicy
.Where(c => c.GroupId == accessPolicy.GroupId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
{
var policy = await dbContext.ServiceAccountProjectAccessPolicy
.Where(c => c.ServiceAccountId == accessPolicy.ServiceAccountId &&
c.GrantedProjectId == accessPolicy.GrantedProjectId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.UserServiceAccountAccessPolicy accessPolicy:
{
var policy = await dbContext.UserServiceAccountAccessPolicy
.Where(c => c.OrganizationUserId == accessPolicy.OrganizationUserId &&
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
.FirstOrDefaultAsync();
return policy != null;
}
case Core.SecretsManager.Entities.GroupServiceAccountAccessPolicy accessPolicy:
{
var policy = await dbContext.GroupServiceAccountAccessPolicy
.Where(c => c.GroupId == accessPolicy.GroupId &&
c.GrantedServiceAccountId == accessPolicy.GrantedServiceAccountId)
.FirstOrDefaultAsync();
return policy != null;
}
default:
throw new ArgumentException("Unsupported access policy type provided.", nameof(baseAccessPolicy));
}
}
public async Task<Core.SecretsManager.Entities.BaseAccessPolicy?> GetByIdAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.Where(ap => ap.Id == id)
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((UserProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
.Include(ap => ((GroupProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).GrantedProject)
.Include(ap => ((UserServiceAccountAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((UserServiceAccountAccessPolicy)ap).GrantedServiceAccount)
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).Group)
.Include(ap => ((GroupServiceAccountAccessPolicy)ap).GrantedServiceAccount)
.FirstOrDefaultAsync();
return entity == null ? null : MapToCore(entity);
}
public async Task ReplaceAsync(Core.SecretsManager.Entities.BaseAccessPolicy baseAccessPolicy)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.FindAsync(baseAccessPolicy.Id);
if (entity != null)
{
dbContext.AccessPolicies.Attach(entity);
entity.Write = baseAccessPolicy.Write;
entity.Read = baseAccessPolicy.Read;
entity.RevisionDate = baseAccessPolicy.RevisionDate;
await dbContext.SaveChangesAsync();
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByGrantedProjectIdAsync(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.AccessPolicies.Where(ap =>
((UserProjectAccessPolicy)ap).GrantedProjectId == id ||
((GroupProjectAccessPolicy)ap).GrantedProjectId == id ||
((ServiceAccountProjectAccessPolicy)ap).GrantedProjectId == id)
.Include(ap => ((UserProjectAccessPolicy)ap).OrganizationUser.User)
.Include(ap => ((GroupProjectAccessPolicy)ap).Group)
.Include(ap => ((ServiceAccountProjectAccessPolicy)ap).ServiceAccount)
.Select(ap => new
{
ap,
CurrentUserInGroup = ap is GroupProjectAccessPolicy &&
((GroupProjectAccessPolicy)ap).Group.GroupUsers.Any(g =>
g.OrganizationUser.User.Id == userId),
})
.ToListAsync();
return entities.Select(e => MapToCore(e.ap, e.CurrentUserInGroup));
}
public async Task DeleteAsync(Guid id)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var entity = await dbContext.AccessPolicies.FindAsync(id);
if (entity != null)
{
dbContext.Remove(entity);
await dbContext.SaveChangesAsync();
}
}
public async Task<IEnumerable<Core.SecretsManager.Entities.BaseAccessPolicy>> GetManyByServiceAccountIdAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccountProjectAccessPolicy.Where(ap =>
ap.ServiceAccountId == id);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var entities = await query
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
return entities.Select(MapToCore);
}
public async Task<PeopleGrantees> GetPeopleGranteesAsync(Guid organizationId, Guid currentUserId)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -405,6 +268,133 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return await GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);
}
public async Task<ServiceAccountGrantedPolicies?> GetServiceAccountGrantedPoliciesAsync(Guid serviceAccountId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
if (entities.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPolicies(serviceAccountId, entities.Select(MapToCore).ToList());
}
public async Task<ServiceAccountGrantedPoliciesPermissionDetails?>
GetServiceAccountGrantedPoliciesPermissionDetailsAsync(Guid serviceAccountId, Guid userId,
AccessClientType accessClientType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var accessPolicyQuery = dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == serviceAccountId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject);
var accessPoliciesPermissionDetails =
await ToPermissionDetails(accessPolicyQuery, userId, accessClientType).ToListAsync();
if (accessPoliciesPermissionDetails.Count == 0)
{
return null;
}
return new ServiceAccountGrantedPoliciesPermissionDetails
{
ServiceAccountId = serviceAccountId,
OrganizationId = accessPoliciesPermissionDetails.First().AccessPolicy.GrantedProject!.OrganizationId,
ProjectGrantedPolicies = accessPoliciesPermissionDetails
};
}
public async Task UpdateServiceAccountGrantedPoliciesAsync(ServiceAccountGrantedPoliciesUpdates updates)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.ServiceAccountId == updates.ServiceAccountId)
.ToListAsync();
if (currentAccessPolicies.Count != 0)
{
var projectIdsToDelete = updates.ProjectGrantedPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
var policiesToDelete = currentAccessPolicies
.Where(entity => projectIdsToDelete.Contains(entity.GrantedProjectId!.Value))
.ToList();
dbContext.RemoveRange(policiesToDelete);
}
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
updates.ProjectGrantedPolicyUpdates.Where(pu => pu.Operation != AccessPolicyOperation.Delete).ToList());
await UpdateServiceAccountRevisionAsync(dbContext, updates.ServiceAccountId);
await dbContext.SaveChangesAsync();
}
public async Task<ProjectServiceAccountsAccessPolicies?> GetProjectServiceAccountsAccessPoliciesAsync(Guid projectId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var entities = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.GrantedProjectId == projectId)
.Include(ap => ap.ServiceAccount)
.Include(ap => ap.GrantedProject)
.ToListAsync();
if (entities.Count == 0)
{
return null;
}
return new ProjectServiceAccountsAccessPolicies(projectId, entities.Select(MapToCore).ToList());
}
public async Task UpdateProjectServiceAccountsAccessPoliciesAsync(
ProjectServiceAccountsAccessPoliciesUpdates updates)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var currentAccessPolicies = await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => ap.GrantedProjectId == updates.ProjectId)
.ToListAsync();
if (currentAccessPolicies.Count != 0)
{
var serviceAccountIdsToDelete = updates.ServiceAccountAccessPolicyUpdates
.Where(pu => pu.Operation == AccessPolicyOperation.Delete)
.Select(pu => pu.AccessPolicy.ServiceAccountId!.Value)
.ToList();
var accessPolicyIdsToDelete = currentAccessPolicies
.Where(entity => serviceAccountIdsToDelete.Contains(entity.ServiceAccountId!.Value))
.Select(ap => ap.Id)
.ToList();
await dbContext.ServiceAccountProjectAccessPolicy
.Where(ap => accessPolicyIdsToDelete.Contains(ap.Id))
.ExecuteDeleteAsync();
}
await UpsertServiceAccountProjectPoliciesAsync(dbContext, currentAccessPolicies,
updates.ServiceAccountAccessPolicyUpdates.Where(update => update.Operation != AccessPolicyOperation.Delete)
.ToList());
var effectedServiceAccountIds = updates.ServiceAccountAccessPolicyUpdates
.Select(sa => sa.AccessPolicy.ServiceAccountId!.Value).ToList();
await UpdateServiceAccountsRevisionAsync(dbContext, effectedServiceAccountIds);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
private static async Task UpsertPeoplePoliciesAsync(DatabaseContext dbContext,
List<BaseAccessPolicy> policies, IReadOnlyCollection<AccessPolicy> userPolicyEntities,
IReadOnlyCollection<AccessPolicy> groupPolicyEntities)
@ -440,6 +430,37 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
}
}
private async Task UpsertServiceAccountProjectPoliciesAsync(DatabaseContext dbContext,
IReadOnlyCollection<ServiceAccountProjectAccessPolicy> currentPolices,
List<ServiceAccountProjectAccessPolicyUpdate> policyUpdates)
{
var currentDate = DateTime.UtcNow;
foreach (var policyUpdate in policyUpdates)
{
var updatedEntity = MapToEntity(policyUpdate.AccessPolicy);
var currentEntity = currentPolices.FirstOrDefault(e =>
e.GrantedProjectId == policyUpdate.AccessPolicy.GrantedProjectId!.Value &&
e.ServiceAccountId == policyUpdate.AccessPolicy.ServiceAccountId!.Value);
switch (policyUpdate.Operation)
{
case AccessPolicyOperation.Create when currentEntity == null:
updatedEntity.SetNewId();
await dbContext.AddAsync(updatedEntity);
break;
case AccessPolicyOperation.Update when currentEntity != null:
dbContext.AccessPolicies.Attach(currentEntity);
currentEntity.Read = updatedEntity.Read;
currentEntity.Write = updatedEntity.Write;
currentEntity.RevisionDate = currentDate;
break;
default:
throw new InvalidOperationException("Policy updates failed due to unexpected state.");
}
}
}
private Core.SecretsManager.Entities.BaseAccessPolicy MapToCore(
BaseAccessPolicy baseAccessPolicyEntity) =>
baseAccessPolicyEntity switch
@ -494,4 +515,51 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
return MapToCore(baseAccessPolicyEntity);
}
}
private IQueryable<ServiceAccountProjectAccessPolicyPermissionDetails> ToPermissionDetails(
IQueryable<ServiceAccountProjectAccessPolicy>
query, Guid userId, AccessClientType accessClientType)
{
var permissionDetails = accessClientType switch
{
AccessClientType.NoAccessCheck => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission = true
}),
AccessClientType.User => query.Select(ap => new ServiceAccountProjectAccessPolicyPermissionDetails
{
AccessPolicy =
Mapper.Map<Core.SecretsManager.Entities.ServiceAccountProjectAccessPolicy>(ap),
HasPermission =
(ap.GrantedProject.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.GrantedProject.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write))) &&
(ap.ServiceAccount.UserAccessPolicies.Any(p => p.OrganizationUser.UserId == userId && p.Write) ||
ap.ServiceAccount.GroupAccessPolicies.Any(p =>
p.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && p.Write)))
}),
_ => throw new ArgumentOutOfRangeException(nameof(accessClientType), accessClientType, null)
};
return permissionDetails;
}
private static async Task UpdateServiceAccountRevisionAsync(DatabaseContext dbContext, Guid serviceAccountId)
{
var entity = await dbContext.ServiceAccount.FindAsync(serviceAccountId);
if (entity != null)
{
entity.RevisionDate = DateTime.UtcNow;
}
}
private static async Task UpdateServiceAccountsRevisionAsync(DatabaseContext dbContext, List<Guid> serviceAccountIds)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
}

View File

@ -70,23 +70,43 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var utcNow = DateTime.UtcNow;
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projects = dbContext.Project
.Where(c => ids.Contains(c.Id))
.Include(p => p.Secrets);
await projects.ForEachAsync(project =>
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var serviceAccountIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var secretIds = await dbContext.Project
.Where(p => ids.Contains(p.Id))
.Include(p => p.Secrets)
.SelectMany(p => p.Secrets.Select(s => s.Id))
.Distinct()
.ToListAsync();
var utcNow = DateTime.UtcNow;
if (serviceAccountIds.Count > 0)
{
foreach (var projectSecret in project.Secrets)
{
projectSecret.RevisionDate = utcNow;
}
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(sa => sa.RevisionDate, utcNow));
}
dbContext.Remove(project);
});
if (secretIds.Count > 0)
{
await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow));
}
await dbContext.SaveChangesAsync();
await dbContext.Project.Where(p => ids.Contains(p.Id)).ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Project>> GetManyWithSecretsByIds(IEnumerable<Guid> ids)
@ -120,27 +140,8 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
var projectQuery = dbContext.Project
.Where(s => s.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => projectQuery.Select(p => new
{
Read = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read)
|| p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => projectQuery.Select(p => new
{
Read = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
Write = p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write),
}),
_ => projectQuery.Select(_ => new { Read = false, Write = false }),
};
var policy = await query.FirstOrDefaultAsync();
var accessQuery = BuildProjectAccessQuery(projectQuery, userId, accessType);
var policy = await accessQuery.FirstOrDefaultAsync();
return policy == null ? (false, false) : (policy.Read, policy.Write);
}
@ -154,6 +155,46 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
return projectIds.Count == results.Count;
}
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(
IEnumerable<Guid> projectIds,
Guid userId,
AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var projectsQuery = dbContext.Project.Where(p => projectIds.Contains(p.Id));
var accessQuery = BuildProjectAccessQuery(projectsQuery, userId, accessType);
return await accessQuery.ToDictionaryAsync(pa => pa.Id, pa => (pa.Read, pa.Write));
}
private record ProjectAccess(Guid Id, bool Read, bool Write);
private static IQueryable<ProjectAccess> BuildProjectAccessQuery(IQueryable<Project> projectQuery, Guid userId,
AccessClientType accessType) =>
accessType switch
{
AccessClientType.NoAccessCheck => projectQuery.Select(p => new ProjectAccess(p.Id, true, true)),
AccessClientType.User => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
)),
AccessClientType.ServiceAccount => projectQuery.Select(p => new ProjectAccess
(
p.Id,
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read),
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)
)),
_ => projectQuery.Select(p => new ProjectAccess(p.Id, false, false))
};
private IQueryable<ProjectPermissionDetails> ProjectToPermissionDetails(IQueryable<Project> query, Guid userId, AccessClientType accessType)
{
var projects = accessType switch
@ -199,8 +240,4 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
private static Expression<Func<Project, bool>> ServiceAccountHasWriteAccessToProject(Guid serviceAccountId) => p =>
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Write);
}

View File

@ -43,7 +43,28 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.Secret
.Include(c => c.Projects)
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null);
query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)),
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToSecret(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null)
};
var secrets = await query.OrderBy(c => c.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -82,7 +103,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByOrganizationIdInTrashAsync(Guid organizationId)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdInTrashAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -103,7 +124,7 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
}
}
public async Task<IEnumerable<SecretPermissionDetails>> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
@ -115,106 +136,124 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return await secrets.ToListAsync();
}
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(Core.SecretsManager.Entities.Secret secret)
public override async Task<Core.SecretsManager.Entities.Secret> CreateAsync(
Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
secret.SetNewId();
var entity = Mapper.Map<Secret>(secret);
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
secret.SetNewId();
var entity = Mapper.Map<Secret>(secret);
if (secret.Projects?.Count > 0)
await using var transaction = await dbContext.Database.BeginTransactionAsync();
if (secret.Projects?.Count > 0)
{
foreach (var project in entity.Projects)
{
foreach (var p in entity.Projects)
{
dbContext.Attach(p);
}
dbContext.Attach(project);
}
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
secret.Id = entity.Id;
return secret;
var projectIds = entity.Projects.Select(p => p.Id).ToList();
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await dbContext.AddAsync(entity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
secret.Id = entity.Id;
return secret;
}
public async Task<Core.SecretsManager.Entities.Secret> UpdateAsync(Core.SecretsManager.Entities.Secret secret)
{
using (var scope = ServiceScopeFactory.CreateScope())
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var entity = await dbContext.Secret
.Include(s => s.Projects)
.FirstAsync(s => s.Id == secret.Id);
var projectsToRemove = entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)).ToList();
var projectsToAdd = mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)).ToList();
foreach (var p in projectsToRemove)
{
var dbContext = GetDatabaseContext(scope);
var mappedEntity = Mapper.Map<Secret>(secret);
var entity = await dbContext.Secret
.Include("Projects")
.FirstAsync(s => s.Id == secret.Id);
foreach (var p in entity.Projects.Where(p => mappedEntity.Projects.All(mp => mp.Id != p.Id)))
{
entity.Projects.Remove(p);
}
// Add new relationships
foreach (var project in mappedEntity.Projects.Where(p => entity.Projects.All(ep => ep.Id != p.Id)))
{
var p = dbContext.AttachToOrGet<Project>(_ => _.Id == project.Id, () => project);
entity.Projects.Add(p);
}
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.SaveChangesAsync();
entity.Projects.Remove(p);
}
foreach (var project in projectsToAdd)
{
var p = dbContext.AttachToOrGet<Project>(x => x.Id == project.Id, () => project);
entity.Projects.Add(p);
}
var projectIds = projectsToRemove.Select(p => p.Id).Concat(projectsToAdd.Select(p => p.Id)).ToList();
if (projectIds.Count > 0)
{
await UpdateServiceAccountRevisionsByProjectIdsAsync(dbContext, projectIds);
}
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, [entity.Id]);
dbContext.Entry(entity).CurrentValues.SetValues(mappedEntity);
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return secret;
}
public async Task SoftDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = utcNow;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, utcNow));
await transaction.CommitAsync();
}
public async Task HardDeleteManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
dbContext.Remove(secret);
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteDeleteAsync();
await transaction.CommitAsync();
}
public async Task RestoreManyByIdAsync(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.DeletedDate = null;
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
await using var transaction = await dbContext.Database.BeginTransactionAsync();
var secretIds = ids.ToList();
await UpdateServiceAccountRevisionsBySecretIdsAsync(dbContext, secretIds);
var utcNow = DateTime.UtcNow;
await dbContext.Secret.Where(c => secretIds.Contains(c.Id))
.ExecuteUpdateAsync(setters =>
setters.SetProperty(s => s.RevisionDate, utcNow)
.SetProperty(s => s.DeletedDate, (DateTime?)null));
await transaction.CommitAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> ImportAsync(IEnumerable<Core.SecretsManager.Entities.Secret> secrets)
@ -248,24 +287,6 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
return secrets;
}
public async Task UpdateRevisionDates(IEnumerable<Guid> ids)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var utcNow = DateTime.UtcNow;
var secrets = dbContext.Secret.Where(s => ids.Contains(s.Id));
await secrets.ForEachAsync(secret =>
{
dbContext.Attach(secret);
secret.RevisionDate = utcNow;
});
await dbContext.SaveChangesAsync();
}
}
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -357,4 +378,60 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
p.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)));
private static async Task UpdateServiceAccountRevisionsByProjectIdsAsync(DatabaseContext dbContext,
List<Guid> projectIds)
{
if (projectIds.Count == 0)
{
return;
}
var serviceAccountIds = await dbContext.Project.Where(p => projectIds.Contains(p.Id))
.Include(p => p.ServiceAccountAccessPolicies)
.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsBySecretIdsAsync(DatabaseContext dbContext,
List<Guid> secretIds)
{
if (secretIds.Count == 0)
{
return;
}
var projectAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s =>
s.Projects.SelectMany(p => p.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value)))
.Distinct()
.ToListAsync();
var directAccessServiceAccountIds = await dbContext.Secret
.Where(s => secretIds.Contains(s.Id))
.SelectMany(s => s.ServiceAccountAccessPolicies.Select(ap => ap.ServiceAccountId!.Value))
.Distinct()
.ToListAsync();
var serviceAccountIds =
directAccessServiceAccountIds.Concat(projectAccessServiceAccountIds).Distinct().ToList();
await UpdateServiceAccountRevisionsAsync(dbContext, serviceAccountIds);
}
private static async Task UpdateServiceAccountRevisionsAsync(DatabaseContext dbContext,
List<Guid> serviceAccountIds)
{
if (serviceAccountIds.Count > 0)
{
var utcNow = DateTime.UtcNow;
await dbContext.ServiceAccount
.Where(sa => serviceAccountIds.Contains(sa.Id))
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
}
}
}

View File

@ -43,28 +43,6 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
return Mapper.Map<List<Core.SecretsManager.Entities.ServiceAccount>>(serviceAccounts);
}
public async Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasReadAccessToServiceAccount(userId));
return await query.AnyAsync();
}
public async Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasWriteAccessToServiceAccount(userId));
return await query.AnyAsync();
}
public async Task<IEnumerable<Core.SecretsManager.Entities.ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
@ -112,30 +90,29 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
public async Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId,
AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccount = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var serviceAccountQuery = dbContext.ServiceAccount.Where(sa => sa.Id == id);
var query = accessType switch
{
AccessClientType.NoAccessCheck => serviceAccount.Select(_ => new { Read = true, Write = true }),
AccessClientType.User => serviceAccount.Select(sa => new
{
Read = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
Write = sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)),
}),
AccessClientType.ServiceAccount => serviceAccount.Select(_ => new { Read = false, Write = false }),
_ => serviceAccount.Select(_ => new { Read = false, Write = false }),
};
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountQuery, userId, accessType);
var access = await accessQuery.FirstOrDefaultAsync();
var policy = await query.FirstOrDefaultAsync();
return access == null ? (false, false) : (access.Read, access.Write);
}
return policy == null ? (false, false) : (policy.Read, policy.Write);
public async Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(
IEnumerable<Guid> ids,
Guid userId,
AccessClientType accessType)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var serviceAccountsQuery = dbContext.ServiceAccount.Where(p => ids.Contains(p.Id));
var accessQuery = BuildServiceAccountAccessQuery(serviceAccountsQuery, userId, accessType);
return await accessQuery.ToDictionaryAsync(access => access.Id, access => (access.Read, access.Write));
}
public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId)
@ -148,6 +125,15 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
}
}
public async Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId)
{
await using var scope = ServiceScopeFactory.CreateAsyncScope();
var dbContext = GetDatabaseContext(scope);
var result = await dbContext.ServiceAccount.CountAsync(sa =>
sa.OrganizationId == organizationId && serviceAccountIds.Contains(sa.Id));
return serviceAccountIds.Count == result;
}
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
@ -186,6 +172,27 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
return results;
}
private record ServiceAccountAccess(Guid Id, bool Read, bool Write);
private static IQueryable<ServiceAccountAccess> BuildServiceAccountAccessQuery(IQueryable<ServiceAccount> serviceAccountQuery, Guid userId,
AccessClientType accessType) =>
accessType switch
{
AccessClientType.NoAccessCheck => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, true, true)),
AccessClientType.User => serviceAccountQuery.Select(sa => new ServiceAccountAccess
(
sa.Id,
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read)),
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))
)),
AccessClientType.ServiceAccount => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false)),
_ => serviceAccountQuery.Select(sa => new ServiceAccountAccess(sa.Id, false, false))
};
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));

View File

@ -30,10 +30,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"sentry": {
"dsn": "SECRET"
},
@ -58,6 +54,5 @@
"region": "SECRET"
}
},
"scimSettings": {
}
"scimSettings": {}
}

View File

@ -19,13 +19,13 @@ using Bit.Core.Utilities;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using Duende.IdentityServer;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Services;
using Duende.IdentityServer.Stores;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
using DIM = Duende.IdentityServer.Models;
namespace Bit.Sso.Controllers;
@ -483,7 +483,7 @@ public class AccountController : Controller
if (orgUser.Status == OrganizationUserStatusType.Invited)
{
// Org User is invited - they must manually accept the invite via email and authenticate with MP
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.DisplayName()));
}
// Accepted or Confirmed - create SSO link and return;
@ -516,7 +516,7 @@ public class AccountController : Controller
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
}
_logger.LogInformation(e, "SSO auto provisioning failed");
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
}
}
}
@ -703,8 +703,10 @@ public class AccountController : Controller
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
{
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
if (providerSupportsSignout)
var provider = HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await provider.GetHandlerAsync(HttpContext, idp);
if (handler is IAuthenticationSignOutHandler)
{
if (logoutId == null)
{

View File

@ -8,7 +8,7 @@
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Sso-SelfHost' " />
<ItemGroup>
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.22" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.9.2" />
</ItemGroup>

View File

@ -6,7 +6,7 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
using Bit.Sso.Utilities;
using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Services;
using Microsoft.IdentityModel.Logging;
using Stripe;
@ -108,7 +108,7 @@ public class Startup
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
app.Use(async (ctx, next) =>
{
ctx.SetIdentityServerOrigin($"{uri.Scheme}://{uri.Host}");
ctx.RequestServices.GetRequiredService<IServerUrls>().Origin = $"{uri.Scheme}://{uri.Host}";
await next();
});
}

View File

@ -31,10 +31,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"

View File

@ -9,15 +9,15 @@
"version": "0.0.0",
"license": "-",
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7"
"sass": "1.75.0"
}
},
"node_modules/@nodelib/fs.scandir": {
@ -598,17 +598,23 @@
}
},
"node_modules/bootstrap": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
"integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.0"
"popper.js": "^1.16.1"
}
},
"node_modules/brace-expansion": {
@ -1028,9 +1034,9 @@
}
},
"node_modules/del": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
"dev": true,
"dependencies": {
"globby": "^11.0.1",
@ -2383,9 +2389,9 @@
}
},
"node_modules/jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -3946,9 +3952,9 @@
}
},
"node_modules/sass": {
"version": "1.49.7",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz",
"integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==",
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -3959,7 +3965,7 @@
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
}
},
"node_modules/sass/node_modules/anymatch": {
@ -3976,12 +3982,15 @@
}
},
"node_modules/sass/node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sass/node_modules/braces": {
@ -3997,16 +4006,10 @@
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -4019,6 +4022,9 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}

View File

@ -8,14 +8,14 @@
"build": "gulp build"
},
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7"
"sass": "1.75.0"
}
}

View File

@ -22,7 +22,7 @@ public class CreateProviderCommandTests
provider.Type = ProviderType.Msp;
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CreateMspAsync(provider, default));
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
Assert.Contains("Invalid owner.", exception.Message);
}
@ -34,7 +34,7 @@ public class CreateProviderCommandTests
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(user.Email).Returns(user);
await sutProvider.Sut.CreateMspAsync(provider, user.Email);
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);

View File

@ -1,7 +1,10 @@
using Bit.Commercial.Core.AdminConsole.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -11,6 +14,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
using IMailService = Bit.Core.Services.IMailService;
namespace Bit.Commercial.Core.Test.AdminConsole.ProviderFeatures;
@ -81,12 +85,15 @@ public class RemoveOrganizationFromProviderCommandTests
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations(
public async Task RemoveOrganizationFromProvider_NoStripeObjects_MakesCorrectInvocations(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
organization.GatewayCustomerId = null;
organization.GatewaySubscriptionId = null;
providerOrganization.ProviderId = provider.Id;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
@ -108,19 +115,125 @@ public class RemoveOrganizationFromProviderCommandTests
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
await stripeAdapter.DidNotReceiveWithAnyArgs().CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
await stripeAdapter.DidNotReceiveWithAnyArgs().SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs().SendProviderUpdatePaymentMethod(
Arg.Any<Guid>(),
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<IEnumerable<string>>());
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_MakesCorrectInvocations__FeatureFlagOff(
Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(true);
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
{
Id = "S-1",
CurrentPeriodEnd = DateTime.Today.AddDays(10),
});
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.BillingEmail == "a@example.com"));
await stripeAdapter.Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Coupon == string.Empty && options.Email == "a@gmail.com"));
await stripeAdapter.Received(1).SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
options => options.CollectionMethod == "send_invoice" && options.DaysUntilDue == 30));
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@gmail.com") && emails.Contains("b@gmail.com")));
Arg.Is<IEnumerable<string>>(emails => emails.Contains("a@example.com") && emails.Contains("b@example.com")));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);
await sutProvider.GetDependency<IEventService>().Received(1).LogProviderOrganizationEventAsync(
providerOrganization,
EventType.ProviderOrganization_Removed);
}
[Theory, BitAutoData]
public async Task RemoveOrganizationFromProvider_CreatesSubscriptionAndScalesSeats_FeatureFlagON(Provider provider,
ProviderOrganization providerOrganization,
Organization organization,
SutProvider<RemoveOrganizationFromProviderCommand> sutProvider)
{
providerOrganization.ProviderId = provider.Id;
provider.Status = ProviderStatusType.Billable;
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false)
.Returns(true);
var organizationOwnerEmails = new List<string> { "a@example.com", "b@example.com" };
organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns(organizationOwnerEmails);
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
{
Id = "S-1",
CurrentPeriodEnd = DateTime.Today.AddDays(10),
});
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);
await stripeAdapter.Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
options => options.Coupon == string.Empty && options.Email == "a@example.com"));
await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(c =>
c.Customer == organization.GatewayCustomerId &&
c.CollectionMethod == "send_invoice" &&
c.DaysUntilDue == 30 &&
c.Items.Count == 1
));
await sutProvider.GetDependency<IScaleSeatsCommand>().Received(1)
.ScalePasswordManagerSeats(provider, organization.PlanType, -(int)organization.Seats);
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(
org => org.Id == organization.Id && org.BillingEmail == "a@example.com" &&
org.GatewaySubscriptionId == "S-1"));
await sutProvider.GetDependency<IMailService>().Received(1).SendProviderUpdatePaymentMethod(
organization.Id,
organization.Name,
provider.Name,
Arg.Is<IEnumerable<string>>(emails =>
emails.Contains("a@example.com") && emails.Contains("b@example.com")));
await sutProvider.GetDependency<IProviderOrganizationRepository>().Received(1)
.DeleteAsync(providerOrganization);

View File

@ -1,9 +1,11 @@
using Bit.Commercial.Core.AdminConsole.Services;
using Bit.Commercial.Core.Test.AdminConsole.AutoFixture;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -13,6 +15,7 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -458,17 +461,112 @@ public class ProviderServiceTests
{
organization.PlanType = PlanType.EnterpriseAnnually;
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await organizationRepository.Received(1)
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
provider.Type = ProviderType.Msp;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
{
var newCreationDate = new DateTime(2023, 11, 5);
BackdateProviderCreationDate(provider, newCreationDate);
provider.Type = ProviderType.Msp;
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Plan = "Enterprise (Annually)";
var expectedPlanType = PlanType.EnterpriseAnnually2020;
var expectedPlanId = "2020-enterprise-org-seat-annually";
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await providerOrganizationRepository.Received(1)
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key));
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
providerOrganization.ProviderId == provider.Id &&
providerOrganization.OrganizationId == organization.Id &&
providerOrganization.Key == key),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
}
[Theory, BitAutoData]
@ -543,6 +641,85 @@ public class ProviderServiceTests
t.First().Item2 == null));
}
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvalidPlanType_ThrowsBadRequestException(
Provider provider,
OrganizationSignup organizationSignup,
Organization organization,
string clientOwnerEmail,
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
organizationSignup.Plan = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user));
await providerOrganizationRepository.DidNotReceiveWithAnyArgs().CreateAsync(default);
}
[Theory, OrganizationCustomize(FlexibleCollections = false), BitAutoData]
public async Task CreateOrganizationAsync_ConsolidatedBillingEnabled_InvokeSignupClientAsync(
Provider provider,
OrganizationSignup organizationSignup,
Organization organization,
string clientOwnerEmail,
User user,
SutProvider<ProviderService> sutProvider)
{
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true);
provider.Type = ProviderType.Msp;
provider.Status = ProviderStatusType.Billable;
organizationSignup.Plan = PlanType.EnterpriseMonthly;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
sutProvider.GetDependency<IOrganizationService>().SignupClientAsync(organizationSignup)
.Returns((organization, null as OrganizationUser, new Collection()));
var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user);
await providerOrganizationRepository.Received(1).CreateAsync(Arg.Is<ProviderOrganization>(
po =>
po.ProviderId == provider.Id &&
po.OrganizationId == organization.Id));
await sutProvider.GetDependency<IEventService>()
.Received()
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Created);
await sutProvider.GetDependency<IOrganizationService>()
.Received()
.InviteUsersAsync(
organization.Id,
user.Id,
Arg.Is<IEnumerable<(OrganizationUserInvite, string)>>(
t =>
t.Count() == 1 &&
t.First().Item1.Emails.Count() == 1 &&
t.First().Item1.Emails.First() == clientOwnerEmail &&
t.First().Item1.Type == OrganizationUserType.Owner &&
t.First().Item1.AccessAll &&
!t.First().Item1.Collections.Any() &&
t.First().Item2 == null));
}
[Theory, OrganizationCustomize(FlexibleCollections = true), BitAutoData]
public async Task CreateOrganizationAsync_WithFlexibleCollections_SetsAccessAllToFalse
(Provider provider, OrganizationSignup organizationSignup, Organization organization, string clientOwnerEmail,
@ -577,62 +754,92 @@ public class ProviderServiceTests
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
public async Task Delete_Success(Provider provider, SutProvider<ProviderService> sutProvider)
{
provider.Type = ProviderType.Msp;
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
await sutProvider.Sut.DeleteAsync(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
var expectedPlanType = PlanType.EnterpriseAnnually;
organization.PlanType = PlanType.EnterpriseAnnually;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
EventType.ProviderOrganization_Added);
Assert.Equal(organization.PlanType, expectedPlanType);
await providerRepository.Received().DeleteAsync(provider);
await applicationCacheService.Received().DeleteProviderAbilityAsync(provider.Id);
}
[Theory, BitAutoData]
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
SutProvider<ProviderService> sutProvider)
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderNameIsEmpty(string providerAdminEmail, SutProvider<ProviderService> sutProvider)
{
var newCreationDate = new DateTime(2023, 11, 5);
BackdateProviderCreationDate(provider, newCreationDate);
provider.Type = ProviderType.Msp;
var provider = new Provider { Name = "" };
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
}
organization.PlanType = PlanType.EnterpriseAnnually;
organization.Plan = "Enterprise (Annually)";
[Theory, BitAutoData]
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminNotFound(Provider provider, SutProvider<ProviderService> sutProvider)
{
var providerAdminEmail = "nonexistent@example.com";
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(null));
var expectedPlanType = PlanType.EnterpriseAnnually2020;
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
}
var expectedPlanId = "2020-enterprise-org-seat-annually";
[Theory, BitAutoData]
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminStatusIsNotConfirmed(
Provider provider
, User providerAdmin
, ProviderUser providerUser
, SutProvider<ProviderService> sutProvider)
{
var providerAdminEmail = "nonexistent@example.com";
providerUser.Status = ProviderUserStatusType.Confirmed;
providerUser.Type = ProviderUserType.ServiceUser;
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
.Returns(GetSubscription(organization.GatewaySubscriptionId));
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
Assert.Contains("Org admin not found.", exception.Message);
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
}
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
await sutProvider.GetDependency<IEventService>()
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
EventType.ProviderOrganization_Added);
[Theory, BitAutoData]
public async Task InitiateDeleteAsync_SendsInitiateDeleteProviderEmail(Provider provider, User providerAdmin
, ProviderUser providerUser, SutProvider<ProviderService> sutProvider)
{
var providerAdminEmail = providerAdmin.Email;
providerUser.Status = ProviderUserStatusType.Confirmed;
providerUser.Type = ProviderUserType.ProviderAdmin;
Assert.Equal(organization.PlanType, expectedPlanType);
var userRepository = sutProvider.GetDependency<IUserRepository>();
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
var mailService = sutProvider.GetDependency<IMailService>();
await sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail);
await mailService.Received().SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidToken(Provider provider, string invalidToken
, SutProvider<ProviderService> sutProvider)
{
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
providerDeleteTokenDataFactory.TryUnprotect(invalidToken, out Arg.Any<ProviderDeleteTokenable>()).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, invalidToken));
}
[Theory, BitAutoData]
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidTokenData(Provider provider, string validToken
, SutProvider<ProviderService> sutProvider)
{
var validTokenData = new ProviderDeleteTokenable();
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
providerDeleteTokenDataFactory.TryUnprotect(validToken, out validTokenData).Returns(false);
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, validToken));
}
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>

View File

@ -1,763 +0,0 @@
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Commercial.Core.Test.SecretsManager.Enums;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class AccessPolicyAuthorizationHandlerTests
{
private static void SetupCurrentUserPermission(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
PermissionType permissionType, Guid organizationId, Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
switch (permissionType)
{
case PermissionType.RunAsAdmin:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(AccessClientType.NoAccessCheck, userId));
break;
case PermissionType.RunAsUserWithPermission:
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(AccessClientType.User, userId));
break;
default:
throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null);
}
}
private static BaseAccessPolicy CreatePolicy(AccessPolicyType accessPolicyType, Project grantedProject,
ServiceAccount grantedServiceAccount, Guid? serviceAccountId = null)
{
switch (accessPolicyType)
{
case AccessPolicyType.UserProjectAccessPolicy:
return
new UserProjectAccessPolicy
{
Id = Guid.NewGuid(),
OrganizationUserId = Guid.NewGuid(),
Read = true,
Write = true,
GrantedProjectId = grantedProject.Id,
GrantedProject = grantedProject,
};
case AccessPolicyType.GroupProjectAccessPolicy:
return
new GroupProjectAccessPolicy
{
Id = Guid.NewGuid(),
GroupId = Guid.NewGuid(),
GrantedProjectId = grantedProject.Id,
Read = true,
Write = true,
GrantedProject = grantedProject,
};
case AccessPolicyType.ServiceAccountProjectAccessPolicy:
return new ServiceAccountProjectAccessPolicy
{
Id = Guid.NewGuid(),
ServiceAccountId = serviceAccountId,
GrantedProjectId = grantedProject.Id,
Read = true,
Write = true,
GrantedProject = grantedProject,
};
case AccessPolicyType.UserServiceAccountAccessPolicy:
return
new UserServiceAccountAccessPolicy
{
Id = Guid.NewGuid(),
OrganizationUserId = Guid.NewGuid(),
Read = true,
Write = true,
GrantedServiceAccountId = grantedServiceAccount.Id,
GrantedServiceAccount = grantedServiceAccount,
};
case AccessPolicyType.GroupServiceAccountAccessPolicy:
return new GroupServiceAccountAccessPolicy
{
Id = Guid.NewGuid(),
GroupId = Guid.NewGuid(),
GrantedServiceAccountId = grantedServiceAccount.Id,
GrantedServiceAccount = grantedServiceAccount,
Read = true,
Write = true,
};
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicyType), accessPolicyType, null);
}
}
private static void SetupMockAccess(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid userId, BaseAccessPolicy accessPolicy, bool read, bool write)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case GroupProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case UserServiceAccountAccessPolicy ap:
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case GroupServiceAccountAccessPolicy ap:
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.GrantedServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
case ServiceAccountProjectAccessPolicy ap:
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(ap.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(ap.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((read, write));
break;
}
}
private static void SetupOrganizationMismatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
BaseAccessPolicy accessPolicy)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = Guid.NewGuid()
});
break;
case GroupProjectAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
break;
case UserServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = Guid.NewGuid()
});
break;
case GroupServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = Guid.NewGuid() });
break;
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
}
}
private static void SetupOrganizationMatch(SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
BaseAccessPolicy accessPolicy, Guid organizationId)
{
switch (accessPolicy)
{
case UserProjectAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = organizationId
});
break;
case GroupProjectAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
break;
case UserServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(resource.OrganizationUserId!.Value)
.Returns(new OrganizationUser
{
Id = resource.OrganizationUserId!.Value,
OrganizationId = organizationId
});
break;
case GroupServiceAccountAccessPolicy resource:
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(resource.GroupId!.Value)
.Returns(new Group { Id = resource.GroupId!.Value, OrganizationId = organizationId });
break;
default:
throw new ArgumentOutOfRangeException(nameof(accessPolicy), accessPolicy, null);
}
}
[Fact]
public void AccessPolicyOperations_OnlyPublicStatic()
{
var publicStaticFields = typeof(AccessPolicyOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(AccessPolicyOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedAccessPolicyOperationRequirement_Throws(
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, UserProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new AccessPolicyOperationRequirement();
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_OrgMismatch_DoesNotSucceed(
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMismatch(sutProvider, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_AccessToSecretsManagerFalse_DoesNotSucceed(
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMatch(sutProvider, resource, organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanCreate_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
SetupOrganizationMatch(sutProvider, resource, organizationId);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, Guid.NewGuid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanCreate_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType,
bool read, bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Guid userId,
Guid serviceAccountId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount, serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupOrganizationMatch(sutProvider, resource, organizationId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_TargetsDontExist_DoesNotSucceed(bool projectExists,
bool serviceAccountExists,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
Project mockProject, ServiceAccount mockServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.GrantedProject = null;
resource.ServiceAccount = null;
if (projectExists)
{
resource.GrantedProject = null;
mockProject.Id = resource.GrantedProjectId!.Value;
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
.Returns(mockProject);
}
if (serviceAccountExists)
{
resource.ServiceAccount = null;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
.Returns(mockServiceAccount);
}
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
[BitAutoData(true, true)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_OrgMismatch_DoesNotSucceed(bool fetchProject,
bool fetchSa,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
Project mockProject, ServiceAccount mockServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
if (fetchProject)
{
resource.GrantedProject = null;
mockProject.Id = resource.GrantedProjectId!.Value;
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(resource.GrantedProjectId!.Value)
.Returns(mockProject);
}
if (fetchSa)
{
resource.ServiceAccount = null;
mockServiceAccount.Id = resource.ServiceAccountId!.Value;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(resource.ServiceAccountId!.Value)
.Returns(mockServiceAccount);
}
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessToSecretsManagerFalse_DoesNotSucceed(
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.GrantedProject!.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.ServiceAccount!.OrganizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(PermissionType.RunAsAdmin, true, true, true, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, false, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, false, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, false, true, true, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, false, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, false, true, true, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, false, true, true)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, false, false)]
[BitAutoData(PermissionType.RunAsUserWithPermission, true, true, true, true, true)]
public async Task CanCreate_ServiceAccountProjectAccessPolicy_AccessCheck(PermissionType permissionType,
bool projectRead,
bool projectWrite, bool saRead, bool saWrite, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider, ServiceAccountProjectAccessPolicy resource,
ClaimsPrincipal claimsPrincipal, Guid userId)
{
var requirement = AccessPolicyOperations.Create;
resource.ServiceAccount!.OrganizationId = resource.GrantedProject!.OrganizationId;
SetupCurrentUserPermission(sutProvider, permissionType, resource.GrantedProject!.OrganizationId, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.GrantedProjectId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((projectRead, projectWrite));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId!.Value, userId, Arg.Any<AccessClientType>())
.Returns((saRead, saWrite));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanUpdate_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanUpdate_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanUpdate_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType, bool read,
bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
{
var requirement = AccessPolicyOperations.Update;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanDelete_AccessToSecretsManagerFalse_DoesNotSucceed(AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.ServiceAccount, AccessPolicyType.GroupServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.ServiceAccountProjectAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.UserServiceAccountAccessPolicy)]
[BitAutoData(AccessClientType.Organization, AccessPolicyType.GroupServiceAccountAccessPolicy)]
public async Task CanDelete_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType clientType,
AccessPolicyType accessPolicyType,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount);
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, organizationId).ReturnsForAnyArgs(
(clientType, new Guid()));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.ServiceAccountProjectAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.UserServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsAdmin, true, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, false, true, true)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, false, false)]
[BitAutoData(AccessPolicyType.GroupServiceAccountAccessPolicy, PermissionType.RunAsUserWithPermission, true, true, true)]
public async Task CanDelete_AccessCheck(
AccessPolicyType accessPolicyType,
PermissionType permissionType,
bool read, bool write, bool expected,
SutProvider<AccessPolicyAuthorizationHandler> sutProvider,
Guid organizationId,
Project mockGrantedProject,
ServiceAccount mockGrantedServiceAccount,
ClaimsPrincipal claimsPrincipal, Guid userId, Guid serviceAccountId)
{
var requirement = AccessPolicyOperations.Delete;
mockGrantedProject.OrganizationId = organizationId;
mockGrantedServiceAccount.OrganizationId = organizationId;
var resource = CreatePolicy(accessPolicyType, mockGrantedProject, mockGrantedServiceAccount,
serviceAccountId);
SetupCurrentUserPermission(sutProvider, permissionType, organizationId, userId);
SetupMockAccess(sutProvider, userId, resource, read, write);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.Equal(expected, authzContext.HasSucceeded);
}
}

View File

@ -0,0 +1,342 @@
#nullable enable
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ProjectServiceAccountsAccessPoliciesAuthorizationHandlerTests
{
[Fact]
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ProjectServiceAccountsAccessPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedProjectServiceAccountsPoliciesOperationRequirement_Throws(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ProjectServiceAccountsAccessPoliciesOperationRequirement();
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
[BitAutoData(AccessClientType.User, false, false)]
[BitAutoData(AccessClientType.User, true, false)]
public async Task Handler_UserHasNoWriteAccessToProject_DoesNotSucceed(
AccessClientType accessClientType,
bool projectReadAccess,
bool projectWriteAccess,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
.Returns((projectReadAccess, projectWriteAccess));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_ServiceAccountsInDifferentOrganization_DoesNotSucceed(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToProject_NoCreatesRequested_Success(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = RemoveAllCreates(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasNoAccessToCreateServiceAccounts_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
accessResult[accessResult.First().Key] = (true, true);
accessResult.Remove(accessResult.Last().Key);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToSomeCreateServiceAccounts_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (false, false));
accessResult[accessResult.First().Key] = (true, true);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToAllCreateServiceAccounts_Success(
AccessClientType accessClientType,
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ProjectServiceAccountsAccessPoliciesOperations.Updates;
resource = AddServiceAccountCreateUpdate(resource);
SetupServiceAccountsAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = resource.ServiceAccountAccessPolicyUpdates
.Where(x => x.Operation == AccessPolicyOperation.Create)
.Select(x => x.AccessPolicy.ServiceAccountId!.Value)
.ToDictionary(id => id, _ => (true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
private static void SetupUserSubstitutes(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs((accessClientType, userId));
}
private static void SetupServiceAccountsAccessTest(
SutProvider<ProjectServiceAccountsAccessPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ProjectServiceAccountsAccessPoliciesUpdates resource,
Guid userId = new())
{
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectAsync(resource.ProjectId, userId, accessClientType)
.Returns((true, true));
sutProvider.GetDependency<IServiceAccountRepository>()
.ServiceAccountsAreInOrganizationAsync(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
}
private static ProjectServiceAccountsAccessPoliciesUpdates AddServiceAccountCreateUpdate(
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
resource.ServiceAccountAccessPolicyUpdates = resource.ServiceAccountAccessPolicyUpdates.Append(
new ServiceAccountProjectAccessPolicyUpdate
{
AccessPolicy = new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = Guid.NewGuid(),
GrantedProjectId = resource.ProjectId,
Read = true,
Write = true
}
});
return resource;
}
private static ProjectServiceAccountsAccessPoliciesUpdates RemoveAllCreates(
ProjectServiceAccountsAccessPoliciesUpdates resource)
{
resource.ServiceAccountAccessPolicyUpdates =
resource.ServiceAccountAccessPolicyUpdates.Where(x => x.Operation != AccessPolicyOperation.Create);
return resource;
}
}

View File

@ -0,0 +1,273 @@
#nullable enable
using System.Reflection;
using System.Security.Claims;
using Bit.Commercial.Core.SecretsManager.AuthorizationHandlers.AccessPolicies;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.AuthorizationRequirements;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.AuthorizationHandlers.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ServiceAccountGrantedPoliciesAuthorizationHandlerTests
{
[Fact]
public void ServiceAccountGrantedPoliciesOperations_OnlyPublicStatic()
{
var publicStaticFields =
typeof(ServiceAccountGrantedPoliciesOperations).GetFields(BindingFlags.Public | BindingFlags.Static);
var allFields = typeof(ServiceAccountGrantedPoliciesOperations).GetFields();
Assert.Equal(publicStaticFields.Length, allFields.Length);
}
[Theory]
[BitAutoData]
public async Task Handler_AccessSecretsManagerFalse_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.ServiceAccount)]
[BitAutoData(AccessClientType.Organization)]
public async Task Handler_UnsupportedClientTypes_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_UnsupportedServiceAccountGrantedPoliciesOperationRequirement_Throws(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
ClaimsPrincipal claimsPrincipal)
{
var requirement = new ServiceAccountGrantedPoliciesOperationRequirement();
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(authzContext));
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck, false, false)]
[BitAutoData(AccessClientType.NoAccessCheck, true, false)]
[BitAutoData(AccessClientType.User, false, false)]
[BitAutoData(AccessClientType.User, true, false)]
public async Task Handler_UserHasNoWriteAccessToServiceAccount_DoesNotSucceed(
AccessClientType accessClientType,
bool saReadAccess,
bool saWriteAccess,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((saReadAccess, saWriteAccess));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData]
public async Task Handler_GrantedProjectsInDifferentOrganization_DoesNotSucceed(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
SetupUserSubstitutes(sutProvider, AccessClientType.NoAccessCheck, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, AccessClientType.NoAccessCheck)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(false);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasNoAccessToGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (false, false)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToSomeGrantedProjects_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult[projectIds.First()] = (true, true);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_AccessResultsPartial_DoesNotSucceed(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
var accessResult = projectIds.ToDictionary(projectId => projectId, _ => (false, false));
accessResult.Remove(projectIds.First());
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(accessResult);
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.False(authzContext.HasSucceeded);
}
[Theory]
[BitAutoData(AccessClientType.NoAccessCheck)]
[BitAutoData(AccessClientType.User)]
public async Task Handler_UserHasAccessToAllGrantedProjects_Success(
AccessClientType accessClientType,
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId,
ClaimsPrincipal claimsPrincipal)
{
var requirement = ServiceAccountGrantedPoliciesOperations.Updates;
var projectIds = SetupProjectAccessTest(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IProjectRepository>()
.AccessToProjectsAsync(Arg.Any<List<Guid>>(), userId, accessClientType)
.Returns(projectIds.ToDictionary(projectId => projectId, _ => (true, true)));
var authzContext = new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement },
claimsPrincipal, resource);
await sutProvider.Sut.HandleAsync(authzContext);
Assert.True(authzContext.HasSucceeded);
}
private static void SetupUserSubstitutes(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(resource.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IAccessClientQuery>().GetAccessClientAsync(default, resource.OrganizationId)
.ReturnsForAnyArgs((accessClientType, userId));
}
private static List<Guid> SetupProjectAccessTest(
SutProvider<ServiceAccountGrantedPoliciesAuthorizationHandler> sutProvider,
AccessClientType accessClientType,
ServiceAccountGrantedPoliciesUpdates resource,
Guid userId = new())
{
SetupUserSubstitutes(sutProvider, accessClientType, resource, userId);
sutProvider.GetDependency<IServiceAccountRepository>()
.AccessToServiceAccountAsync(resource.ServiceAccountId, userId, accessClientType)
.Returns((true, true));
sutProvider.GetDependency<IProjectRepository>()
.ProjectsAreInOrganization(Arg.Any<List<Guid>>(), resource.OrganizationId)
.Returns(true);
return resource.ProjectGrantedPolicyUpdates
.Select(pu => pu.AccessPolicy.GrantedProjectId!.Value)
.ToList();
}
}

View File

@ -1,136 +0,0 @@
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class CreateAccessPoliciesCommandTests
{
private static List<BaseAccessPolicy> MakeGrantedProjectAccessPolicies(Guid grantedProjectId, List<UserProjectAccessPolicy> userProjectAccessPolicies,
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies)
{
var data = new List<BaseAccessPolicy>();
foreach (var ap in userProjectAccessPolicies)
{
ap.GrantedProjectId = grantedProjectId;
ap.GrantedProject = null;
ap.User = null;
}
foreach (var ap in groupProjectAccessPolicies)
{
ap.GrantedProjectId = grantedProjectId;
ap.GrantedProject = null;
ap.Group = null;
}
foreach (var ap in serviceAccountProjectAccessPolicies)
{
ap.GrantedProjectId = grantedProjectId;
ap.GrantedProject = null;
ap.ServiceAccount = null;
}
data.AddRange(userProjectAccessPolicies);
data.AddRange(groupProjectAccessPolicies);
data.AddRange(serviceAccountProjectAccessPolicies);
return data;
}
private static List<BaseAccessPolicy> MakeGrantedServiceAccountAccessPolicies(Guid grantedServiceAccountId, List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies)
{
var data = new List<BaseAccessPolicy>();
foreach (var ap in userServiceAccountAccessPolicies)
{
ap.GrantedServiceAccountId = grantedServiceAccountId;
ap.GrantedServiceAccount = null;
ap.User = null;
}
foreach (var ap in groupServiceAccountAccessPolicies)
{
ap.GrantedServiceAccountId = grantedServiceAccountId;
ap.GrantedServiceAccount = null;
ap.Group = null;
}
data.AddRange(userServiceAccountAccessPolicies);
data.AddRange(groupServiceAccountAccessPolicies);
return data;
}
[Theory]
[BitAutoData]
public async Task CreateMany_AlreadyExists_Throws_BadRequestException(
Project project,
ServiceAccount serviceAccount,
List<UserProjectAccessPolicy> userProjectAccessPolicies,
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies,
List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies,
SutProvider<CreateAccessPoliciesCommand> sutProvider)
{
var data = MakeGrantedProjectAccessPolicies(project.Id, userProjectAccessPolicies, groupProjectAccessPolicies,
serviceAccountProjectAccessPolicies);
var saData = MakeGrantedServiceAccountAccessPolicies(serviceAccount.Id, userServiceAccountAccessPolicies, groupServiceAccountAccessPolicies);
data = data.Concat(saData).ToList();
sutProvider.GetDependency<IAccessPolicyRepository>().AccessPolicyExists(Arg.Any<BaseAccessPolicy>())
.Returns(true);
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateManyAsync(data));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().CreateManyAsync(default!);
}
[Theory]
[BitAutoData]
public async Task CreateMany_ClearsReferences(SutProvider<CreateAccessPoliciesCommand> sutProvider, Guid projectId)
{
var userProjectAp = new UserProjectAccessPolicy
{
GrantedProjectId = projectId,
OrganizationUserId = new Guid(),
};
var data = new List<BaseAccessPolicy>() { userProjectAp, };
userProjectAp.GrantedProject = new Project() { Id = new Guid() };
var expectedCall = new List<BaseAccessPolicy>() { userProjectAp, };
await sutProvider.Sut.CreateManyAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedCall)));
}
[Theory]
[BitAutoData]
public async Task CreateMany_Success(
Project project,
ServiceAccount serviceAccount,
List<UserProjectAccessPolicy> userProjectAccessPolicies,
List<GroupProjectAccessPolicy> groupProjectAccessPolicies,
List<ServiceAccountProjectAccessPolicy> serviceAccountProjectAccessPolicies,
List<UserServiceAccountAccessPolicy> userServiceAccountAccessPolicies,
List<GroupServiceAccountAccessPolicy> groupServiceAccountAccessPolicies,
SutProvider<CreateAccessPoliciesCommand> sutProvider)
{
var data = MakeGrantedProjectAccessPolicies(project.Id, userProjectAccessPolicies, groupProjectAccessPolicies,
serviceAccountProjectAccessPolicies);
var saData = MakeGrantedServiceAccountAccessPolicies(serviceAccount.Id, userServiceAccountAccessPolicies, groupServiceAccountAccessPolicies);
data = data.Concat(saData).ToList();
await sutProvider.Sut.CreateManyAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
}

View File

@ -1,25 +0,0 @@
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class DeleteAccessPolicyCommandTests
{
[Theory]
[BitAutoData]
public async Task DeleteAsync_Success(SutProvider<DeleteAccessPolicyCommand> sutProvider, Guid data)
{
await sutProvider.Sut.DeleteAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.DeleteAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
}

View File

@ -1,44 +0,0 @@
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateAccessPolicyCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotExist_ThrowsNotFound(Guid data, bool read, bool write,
SutProvider<UpdateAccessPolicyCommand> sutProvider)
{
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, read, write));
await sutProvider.GetDependency<IAccessPolicyRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<BaseAccessPolicy>());
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_Success(Guid data, bool read, bool write, UserProjectAccessPolicy accessPolicy,
SutProvider<UpdateAccessPolicyCommand> sutProvider)
{
accessPolicy.Id = data;
sutProvider.GetDependency<IAccessPolicyRepository>().GetByIdAsync(data).Returns(accessPolicy);
var result = await sutProvider.Sut.UpdateAsync(data, read, write);
await sutProvider.GetDependency<IAccessPolicyRepository>().Received(1)
.ReplaceAsync(Arg.Any<BaseAccessPolicy>());
AssertHelper.AssertRecent(result.RevisionDate);
Assert.Equal(read, result.Read);
Assert.Equal(write, result.Write);
}
}

View File

@ -0,0 +1,42 @@
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data.AccessPolicyUpdates;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateProjectServiceAccountsAccessPoliciesCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates data)
{
data.ServiceAccountAccessPolicyUpdates = [];
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_HasUpdates_CallsRepository(
SutProvider<UpdateProjectServiceAccountsAccessPoliciesCommand> sutProvider,
ProjectServiceAccountsAccessPoliciesUpdates data)
{
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.Received(1)
.UpdateProjectServiceAccountsAccessPoliciesAsync(Arg.Any<ProjectServiceAccountsAccessPoliciesUpdates>());
}
}

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Commands.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Commands.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class UpdateServiceAccountGrantedPoliciesCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_NoUpdates_DoesNotCallRepository(
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
ServiceAccountGrantedPoliciesUpdates data)
{
data.ProjectGrantedPolicyUpdates = [];
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_HasUpdates_CallsRepository(
SutProvider<UpdateServiceAccountGrantedPoliciesCommand> sutProvider,
ServiceAccountGrantedPoliciesUpdates data)
{
await sutProvider.Sut.UpdateAsync(data);
await sutProvider.GetDependency<IAccessPolicyRepository>()
.Received(1)
.UpdateServiceAccountGrantedPoliciesAsync(Arg.Any<ServiceAccountGrantedPoliciesUpdates>());
}
}

View File

@ -0,0 +1,86 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ProjectServiceAccountsAccessPoliciesUpdatesQueryTests
{
[Theory]
[BitAutoData]
public async Task GetAsync_NoCurrentAccessPolicies_ReturnsAllCreates(
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
ProjectServiceAccountsAccessPolicies data)
{
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
.ReturnsNullForAnyArgs();
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ProjectId, result.ProjectId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Equal(data.ServiceAccountAccessPolicies.Count(), result.ServiceAccountAccessPolicyUpdates.Count());
Assert.All(result.ServiceAccountAccessPolicyUpdates, p =>
{
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
Assert.Contains(data.ServiceAccountAccessPolicies, x => x == p.AccessPolicy);
});
}
[Theory]
[BitAutoData]
public async Task GetAsync_CurrentAccessPolicies_ReturnsChanges(
SutProvider<ProjectServiceAccountsAccessPoliciesUpdatesQuery> sutProvider,
ProjectServiceAccountsAccessPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
{
foreach (var policy in data.ServiceAccountAccessPolicies)
{
policy.GrantedProjectId = data.ProjectId;
}
currentPolicyToDelete.GrantedProjectId = data.ProjectId;
var updatePolicy = new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = data.ServiceAccountAccessPolicies.First().ServiceAccountId,
GrantedProjectId = data.ProjectId,
Read = !data.ServiceAccountAccessPolicies.First().Read,
Write = !data.ServiceAccountAccessPolicies.First().Write
};
var currentPolicies = new ProjectServiceAccountsAccessPolicies
{
ProjectId = data.ProjectId,
OrganizationId = data.OrganizationId,
ServiceAccountAccessPolicies = [updatePolicy, currentPolicyToDelete]
};
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetProjectServiceAccountsAccessPoliciesAsync(data.ProjectId)
.ReturnsForAnyArgs(currentPolicies);
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ProjectId, result.ProjectId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
Assert.Single(result.ServiceAccountAccessPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Update &&
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
Assert.Equal(result.ServiceAccountAccessPolicyUpdates.Count() - 2,
result.ServiceAccountAccessPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
}
}

View File

@ -0,0 +1,86 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.AccessPolicies;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Enums.AccessPolicies;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.AccessPolicies;
[SutProviderCustomize]
[ProjectCustomize]
public class ServiceAccountGrantedPolicyUpdatesQueryTests
{
[Theory]
[BitAutoData]
public async Task GetAsync_NoCurrentGrantedPolicies_ReturnsAllCreates(
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
ServiceAccountGrantedPolicies data)
{
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
.ReturnsNullForAnyArgs();
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Equal(data.ProjectGrantedPolicies.Count(), result.ProjectGrantedPolicyUpdates.Count());
Assert.All(result.ProjectGrantedPolicyUpdates, p =>
{
Assert.Equal(AccessPolicyOperation.Create, p.Operation);
Assert.Contains(data.ProjectGrantedPolicies, x => x == p.AccessPolicy);
});
}
[Theory]
[BitAutoData]
public async Task GetAsync_CurrentGrantedPolicies_ReturnsChanges(
SutProvider<ServiceAccountGrantedPolicyUpdatesQuery> sutProvider,
ServiceAccountGrantedPolicies data, ServiceAccountProjectAccessPolicy currentPolicyToDelete)
{
foreach (var grantedPolicy in data.ProjectGrantedPolicies)
{
grantedPolicy.ServiceAccountId = data.ServiceAccountId;
}
currentPolicyToDelete.ServiceAccountId = data.ServiceAccountId;
var updatePolicy = new ServiceAccountProjectAccessPolicy
{
ServiceAccountId = data.ServiceAccountId,
GrantedProjectId = data.ProjectGrantedPolicies.First().GrantedProjectId,
Read = !data.ProjectGrantedPolicies.First().Read,
Write = !data.ProjectGrantedPolicies.First().Write
};
var currentPolicies = new ServiceAccountGrantedPolicies
{
ServiceAccountId = data.ServiceAccountId,
OrganizationId = data.OrganizationId,
ProjectGrantedPolicies = [updatePolicy, currentPolicyToDelete]
};
sutProvider.GetDependency<IAccessPolicyRepository>()
.GetServiceAccountGrantedPoliciesAsync(data.ServiceAccountId)
.ReturnsForAnyArgs(currentPolicies);
var result = await sutProvider.Sut.GetAsync(data);
Assert.Equal(data.ServiceAccountId, result.ServiceAccountId);
Assert.Equal(data.OrganizationId, result.OrganizationId);
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Delete && x.AccessPolicy == currentPolicyToDelete));
Assert.Single(result.ProjectGrantedPolicyUpdates.Where(x =>
x.Operation == AccessPolicyOperation.Update &&
x.AccessPolicy.GrantedProjectId == updatePolicy.GrantedProjectId));
Assert.Equal(result.ProjectGrantedPolicyUpdates.Count() - 2,
result.ProjectGrantedPolicyUpdates.Count(x => x.Operation == AccessPolicyOperation.Create));
}
}

View File

@ -0,0 +1,96 @@
#nullable enable
using Bit.Commercial.Core.SecretsManager.Queries.Secrets;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Secrets;
[SutProviderCustomize]
public class SecretsSyncQueryTests
{
[Theory, BitAutoData]
public async Task GetAsync_NullLastSyncedDate_ReturnsHasChanges(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data)
{
data.LastSyncedDate = null;
var result = await sutProvider.Sut.GetAsync(data);
Assert.True(result.HasChanges);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
Arg.Is(data.ServiceAccountId),
Arg.Is(data.AccessClientType));
}
[Theory, BitAutoData]
public async Task GetAsync_HasLastSyncedDateServiceAccountNotFound_Throws(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data)
{
data.LastSyncedDate = DateTime.UtcNow;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns((ServiceAccount?)null);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(data));
await sutProvider.GetDependency<ISecretRepository>()
.DidNotReceiveWithAnyArgs()
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task GetAsync_HasLastSyncedDateServiceAccountWithLaterOrEqualRevisionDate_ReturnsChanges(
bool datesEqual,
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data,
ServiceAccount serviceAccount)
{
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
serviceAccount.Id = data.ServiceAccountId;
serviceAccount.RevisionDate = datesEqual ? data.LastSyncedDate.Value : data.LastSyncedDate.Value.AddSeconds(600);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns(serviceAccount);
var result = await sutProvider.Sut.GetAsync(data);
Assert.True(result.HasChanges);
await sutProvider.GetDependency<ISecretRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(data.OrganizationId),
Arg.Is(data.ServiceAccountId),
Arg.Is(data.AccessClientType));
}
[Theory, BitAutoData]
public async Task GetAsync_HasLastSyncedDateServiceAccountWithEarlierRevisionDate_ReturnsNoChanges(
SutProvider<SecretsSyncQuery> sutProvider,
SecretsSyncRequest data,
ServiceAccount serviceAccount)
{
data.LastSyncedDate = DateTime.UtcNow.AddDays(-1);
serviceAccount.Id = data.ServiceAccountId;
serviceAccount.RevisionDate = data.LastSyncedDate.Value.AddDays(-2);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.ServiceAccountId)
.Returns(serviceAccount);
var result = await sutProvider.Sut.GetAsync(data);
Assert.False(result.HasChanges);
Assert.Null(result.Secrets);
await sutProvider.GetDependency<ISecretRepository>()
.DidNotReceiveWithAnyArgs()
.GetManyByOrganizationIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
}
}

View File

@ -54,7 +54,7 @@ services:
- postgres
mysql:
image: mysql:8
image: mysql:8.0
container_name: bw-mysql
ports:
- "3306:3306"

View File

@ -1,12 +1,12 @@
#!/bin/bash
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
#
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
#
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
# The best workaround seems to be adding tiny delay like so:
sleep 0.1;
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
LAST_MIGRATION_FILE="/mnt/data/last_migration"
SERVER='mssql'
DATABASE="vault_dev"
USER="SA"
@ -16,58 +16,33 @@ while getopts "s" arg; do
case $arg in
s)
echo "Running for self-host environment"
LAST_MIGRATION_FILE="/mnt/data/last_self_host_migration"
DATABASE="vault_dev_self_host"
;;
esac
done
if [ ! -f "$LAST_MIGRATION_FILE" ]; then
echo "No migration file, nothing to migrate to a database store"
exit 1
else
LAST_MIGRATION=$(cat $LAST_MIGRATION_FILE)
rm $LAST_MIGRATION_FILE
fi
[ -z "$LAST_MIGRATION" ]
PERFORM_MIGRATION=$?
# Create database if it does not already exist
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
QUERY="IF OBJECT_ID('[$DATABASE].[dbo].[Migration]') IS NULL AND OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NOT NULL
BEGIN
CREATE DATABASE migrations_$DATABASE;
END;
-- Create [database].dbo.Migration with the schema expected by MsSqlMigratorUtility
SET ANSI_NULLS ON;
SET QUOTED_IDENTIFIER ON;
CREATE TABLE [$DATABASE].[dbo].[Migration](
[Id] [int] IDENTITY(1,1) NOT NULL,
[ScriptName] [nvarchar](255) NOT NULL,
[Applied] [datetime] NOT NULL
) ON [PRIMARY];
ALTER TABLE [$DATABASE].[dbo].[Migration] ADD CONSTRAINT [PK_Migration_Id] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
-- Copy across old data
INSERT INTO [$DATABASE].[dbo].[Migration] (ScriptName, Applied)
SELECT CONCAT('Bit.Migrator.DbScripts.', [Filename]), CreationDate
FROM [migrations_$DATABASE].[dbo].[migrations];
END
"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
QUERY="IF OBJECT_ID('[dbo].[migrations_$DATABASE]') IS NULL
BEGIN
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
[Id] INT IDENTITY(1,1) PRIMARY KEY,
[Filename] NVARCHAR(MAX) NOT NULL,
[CreationDate] DATETIME2 (7) NULL,
);
END;"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
record_migration () {
echo "recording $1"
local file=$(basename $1)
echo $file
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
}
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
if (( PERFORM_MIGRATION == 0 )); then
echo "Still need to migrate $f"
else
record_migration $f
if [ "$LAST_MIGRATION" == "$f" ]; then
PERFORM_MIGRATION=0
fi
fi
done;

View File

@ -1,94 +0,0 @@
#!/bin/bash
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
# The best workaround seems to be adding tiny delay like so:
sleep 0.1;
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
SERVER='mssql'
DATABASE="vault_dev"
USER="SA"
PASSWD=$MSSQL_PASSWORD
while getopts "sp" arg; do
case $arg in
s)
echo "Running for self-host environment"
DATABASE="vault_dev_self_host"
;;
p)
echo "Running for pipeline"
MIGRATE_DIRECTORY=$MSSQL_MIGRATIONS_DIRECTORY
SERVER=$MSSQL_HOST
DATABASE=$MSSQL_DATABASE
USER=$MSSQL_USER
PASSWD=$MSSQL_PASS
esac
done
# Create databases if they do not already exist
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DATABASE')
BEGIN
CREATE DATABASE $DATABASE;
END;
GO
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
BEGIN
CREATE DATABASE migrations_$DATABASE;
END;
GO
"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
echo "Return code: $?"
# Create migrations table if it does not already exist
QUERY="IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
BEGIN
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
[Id] INT IDENTITY(1,1) PRIMARY KEY,
[Filename] NVARCHAR(MAX) NOT NULL,
[CreationDate] DATETIME2 (7) NULL,
);
END;
GO
"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$QUERY"
echo "Return code: $?"
should_migrate () {
local file=$(basename $1)
local query="SELECT * FROM [migrations] WHERE [Filename] = '$file'"
local result=$(/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query")
if [[ "$result" =~ .*"$file".* ]]; then
return 1;
else
return 0;
fi
}
record_migration () {
echo "recording $1"
local file=$(basename $1)
echo $file
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
}
migrate () {
local file=$1
echo "Performing $file"
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d $DATABASE -U $USER -P $PASSWD -I -i $file
}
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
BASENAME=$(basename $f)
if should_migrate $f == 1 ; then
migrate $f
record_migration $f
else
echo "Skipping $f, $BASENAME"
fi
done;

View File

@ -2,20 +2,20 @@
# Creates the vault_dev database, and runs all the migrations.
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
# the mssql-tools container which runs under x86_64. We should monitor this
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
# the mssql-tools container which runs under x86_64.
param(
[switch]$all = $false,
[switch]$postgres = $false,
[switch]$mysql = $false,
[switch]$mssql = $false,
[switch]$sqlite = $false,
[switch]$selfhost = $false,
[switch]$pipeline = $false
[switch]$all,
[switch]$postgres,
[switch]$mysql,
[switch]$mssql,
[switch]$sqlite,
[switch]$selfhost
)
# Abort on any error
$ErrorActionPreference = "Stop"
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
$mssql = $true;
}
@ -29,22 +29,27 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
}
if ($all -or $mssql) {
if ($selfhost) {
$migrationArgs = "-s"
} elseif ($pipeline) {
$migrationArgs = "-p"
function Get-UserSecrets {
return dotnet user-secrets list --json --project ../src/Api | ConvertFrom-Json
}
Write-Host "Starting Microsoft SQL Server Migrations"
docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
-v "$(pwd)/.data/mssql:/mnt/data" `
--env-file .env `
--network=bitwardenserver_default `
--rm `
mcr.microsoft.com/mssql-tools `
/mnt/helpers/run_migrations.sh $migrationArgs
if ($selfhost) {
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
$envName = "self-host"
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
./migrate_migration_record.ps1 -s
} else {
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
$envName = "cloud"
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
./migrate_migration_record.ps1
}
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
}
$currentDir = Get-Location

View File

@ -1,15 +1,13 @@
#!/usr/bin/env pwsh
# This script need only be run once
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
#
# This is a migration script for updating recording the last migration run
# in a file to recording migrations in a database table. It will create a
# migrations_vault table and store all of the previously run migrations as
# indicated by a last_migrations file. It will then delete this file.
# This is a migration script to move data from [migrations_vault_dev].[dbo].[migrations] (used by our custom
# migrator script) to [vault_dev].[dbo].[Migration] (used by MsSqlMigratorUtility). It is safe to run multiple
# times because it will not perform any migration if it detects that the new table is already present.
# This will be deleted after a few months after everyone has (presumably) migrated to the new schema.
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
# the mssql-tools container which runs under x86_64. We should monitor this
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
# the mssql-tools container which runs under x86_64.
docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" `

View File

@ -24,8 +24,9 @@ $projects = @{
Icons = "../src/Icons"
Identity = "../src/Identity"
Notifications = "../src/Notifications"
Sso = "../bitwarden_license/src/Sso"
Scim = "../bitwarden_license/src/Scim"
Sso = "../bitwarden_license/src/Sso"
Scim = "../bitwarden_license/src/Scim"
IntegrationTests = "../test/Infrastructure.IntegrationTest"
}
foreach ($key in $projects.keys) {

View File

@ -1,11 +1,14 @@
using Bit.Admin.AdminConsole.Models;
using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -52,6 +55,8 @@ public class OrganizationsController : Controller
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
private readonly IFeatureService _featureService;
private readonly IScaleSeatsCommand _scaleSeatsCommand;
public OrganizationsController(
IOrganizationService organizationService,
@ -77,7 +82,9 @@ public class OrganizationsController : Controller
IServiceAccountRepository serviceAccountRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
IRemovePaymentMethodCommand removePaymentMethodCommand,
IFeatureService featureService,
IScaleSeatsCommand scaleSeatsCommand)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@ -103,6 +110,8 @@ public class OrganizationsController : Controller
_providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
_featureService = featureService;
_scaleSeatsCommand = scaleSeatsCommand;
}
[RequirePermission(Permission.Org_List_View)]
@ -119,8 +128,9 @@ public class OrganizationsController : Controller
count = 1;
}
var encodedName = WebUtility.HtmlEncode(name);
var skip = (page - 1) * count;
var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid, skip, count);
var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);
return View(new OrganizationsModel
{
Items = organizations as List<Organization>,
@ -232,12 +242,30 @@ public class OrganizationsController : Controller
public async Task<IActionResult> Delete(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization != null)
if (organization == null)
{
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
return RedirectToAction("Index");
}
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (consolidatedBillingEnabled && organization.IsValidClient())
{
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
if (provider.IsBillable())
{
await _scaleSeatsCommand.ScalePasswordManagerSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}
await _organizationRepository.DeleteAsync(organization);
await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
return RedirectToAction("Index");
}
@ -321,7 +349,10 @@ public class OrganizationsController : Controller
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
if (organization.IsStripeEnabled())
{
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
}
return Json(null);
}

View File

@ -3,6 +3,8 @@ using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@ -55,12 +57,23 @@ public class ProviderOrganizationsController : Controller
return RedirectToAction("View", "Providers", new { id = providerId });
}
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
try
{
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
}
catch (BadRequestException ex)
{
return BadRequest(ex.Message);
}
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
if (organization.IsStripeEnabled())
{
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
}
return Json(null);
}

View File

@ -1,4 +1,6 @@
using Bit.Admin.AdminConsole.Models;
using System.ComponentModel.DataAnnotations;
using System.Net;
using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core;
@ -7,10 +9,14 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -29,10 +35,10 @@ public class ProvidersController : Controller
private readonly GlobalSettings _globalSettings;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IProviderService _providerService;
private readonly IReferenceEventService _referenceEventService;
private readonly IUserService _userService;
private readonly ICreateProviderCommand _createProviderCommand;
private readonly IFeatureService _featureService;
private readonly IProviderPlanRepository _providerPlanRepository;
public ProvidersController(
IOrganizationRepository organizationRepository,
@ -43,10 +49,10 @@ public class ProvidersController : Controller
IProviderService providerService,
GlobalSettings globalSettings,
IApplicationCacheService applicationCacheService,
IReferenceEventService referenceEventService,
IUserService userService,
ICreateProviderCommand createProviderCommand,
IFeatureService featureService)
IFeatureService featureService,
IProviderPlanRepository providerPlanRepository)
{
_organizationRepository = organizationRepository;
_organizationService = organizationService;
@ -56,10 +62,10 @@ public class ProvidersController : Controller
_providerService = providerService;
_globalSettings = globalSettings;
_applicationCacheService = applicationCacheService;
_referenceEventService = referenceEventService;
_userService = userService;
_createProviderCommand = createProviderCommand;
_featureService = featureService;
_providerPlanRepository = providerPlanRepository;
}
[RequirePermission(Permission.Provider_List_View)]
@ -89,11 +95,13 @@ public class ProvidersController : Controller
});
}
public IActionResult Create(string ownerEmail = null)
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
{
return View(new CreateProviderModel
{
OwnerEmail = ownerEmail
OwnerEmail = ownerEmail,
TeamsMonthlySeatMinimum = teamsMinimumSeats,
EnterpriseMonthlySeatMinimum = enterpriseMinimumSeats
});
}
@ -111,7 +119,11 @@ public class ProvidersController : Controller
switch (provider.Type)
{
case ProviderType.Msp:
await _createProviderCommand.CreateMspAsync(provider, model.OwnerEmail);
await _createProviderCommand.CreateMspAsync(
provider,
model.OwnerEmail,
model.TeamsMonthlySeatMinimum,
model.EnterpriseMonthlySeatMinimum);
break;
case ProviderType.Reseller:
await _createProviderCommand.CreateResellerAsync(provider);
@ -146,7 +158,17 @@ public class ProvidersController : Controller
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
return View(new ProviderEditModel(provider, users, providerOrganizations));
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
{
return View(new ProviderEditModel(provider, users, providerOrganizations, new List<ProviderPlan>()));
}
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
return View(new ProviderEditModel(provider, users, providerOrganizations, providerPlans.ToList()));
}
[HttpPost]
@ -156,14 +178,56 @@ public class ProvidersController : Controller
public async Task<IActionResult> Edit(Guid id, ProviderEditModel model)
{
var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null)
{
return RedirectToAction("Index");
}
model.ToProvider(provider);
await _providerRepository.ReplaceAsync(provider);
await _applicationCacheService.UpsertProviderAbilityAsync(provider);
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (!isConsolidatedBillingEnabled || !provider.IsBillable())
{
return RedirectToAction("Edit", new { id });
}
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
if (providerPlans.Count == 0)
{
var newProviderPlans = new List<ProviderPlan>
{
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
};
foreach (var newProviderPlan in newProviderPlans)
{
await _providerPlanRepository.CreateAsync(newProviderPlan);
}
}
else
{
foreach (var providerPlan in providerPlans)
{
if (providerPlan.PlanType == PlanType.EnterpriseMonthly)
{
providerPlan.SeatMinimum = model.EnterpriseMonthlySeatMinimum;
}
else if (providerPlan.PlanType == PlanType.TeamsMonthly)
{
providerPlan.SeatMinimum = model.TeamsMonthlySeatMinimum;
}
await _providerPlanRepository.ReplaceAsync(providerPlan);
}
}
return RedirectToAction("Edit", new { id });
}
@ -188,8 +252,9 @@ public class ProvidersController : Controller
count = 1;
}
var encodedName = WebUtility.HtmlEncode(name);
var skip = (page - 1) * count;
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count);
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(encodedName, ownerEmail, skip, count);
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
{
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
@ -199,7 +264,7 @@ public class ProvidersController : Controller
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
{
Id = uo.Id,
Name = uo.Name,
Name = uo.DisplayName(),
PlanType = uo.PlanType
}).ToList()
};
@ -248,4 +313,64 @@ public class ProvidersController : Controller
return RedirectToAction("Edit", "Providers", new { id = providerId });
}
[HttpPost]
[SelfHosted(NotSelfHostedOnly = true)]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> Delete(Guid id, string providerName)
{
if (string.IsNullOrWhiteSpace(providerName))
{
return BadRequest("Invalid provider name");
}
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
if (providerOrganizations.Count > 0)
{
return BadRequest("You must unlink all clients before you can delete a provider");
}
var provider = await _providerRepository.GetByIdAsync(id);
if (provider is null)
{
return BadRequest("Provider does not exist");
}
if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid provider name");
}
await _providerService.DeleteAsync(provider);
return NoContent();
}
[HttpPost]
[SelfHosted(NotSelfHostedOnly = true)]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> DeleteInitiation(Guid id, string providerEmail)
{
var emailAttribute = new EmailAddressAttribute();
if (!emailAttribute.IsValid(providerEmail))
{
return BadRequest("Invalid provider admin email");
}
var provider = await _providerRepository.GetByIdAsync(id);
if (provider != null)
{
try
{
await _providerService.InitiateDeleteAsync(provider, providerEmail);
}
catch (BadRequestException ex)
{
return BadRequest(ex.Message);
}
}
return NoContent();
}
}

View File

@ -24,6 +24,12 @@ public class CreateProviderModel : IValidatableObject
[Display(Name = "Primary Billing Email")]
public string BillingEmail { get; set; }
[Display(Name = "Teams (Monthly) Seat Minimum")]
public int TeamsMonthlySeatMinimum { get; set; }
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
public int EnterpriseMonthlySeatMinimum { get; set; }
public virtual Provider ToProvider()
{
return new Provider()
@ -45,6 +51,16 @@ public class CreateProviderModel : IValidatableObject
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
}
if (TeamsMonthlySeatMinimum < 0)
{
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
}
if (EnterpriseMonthlySeatMinimum < 0)
{
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
}
break;
case ProviderType.Reseller:
if (string.IsNullOrWhiteSpace(Name))

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Net;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
@ -36,8 +37,7 @@ public class OrganizationEditModel : OrganizationViewModel
BillingInfo = billingInfo;
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
Name = org.Name;
BusinessName = org.BusinessName;
Name = org.DisplayName();
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
PlanType = org.PlanType;
Plan = org.Plan;
@ -80,8 +80,6 @@ public class OrganizationEditModel : OrganizationViewModel
[Required]
[Display(Name = "Organization Name")]
public string Name { get; set; }
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
[Display(Name = "Billing Email")]
public string BillingEmail { get; set; }
[Required]
@ -145,9 +143,9 @@ public class OrganizationEditModel : OrganizationViewModel
public int? SmSeats { get; set; }
[Display(Name = "Max Autoscale Seats")]
public int? MaxAutoscaleSmSeats { get; set; }
[Display(Name = "Service Accounts")]
[Display(Name = "Machine Accounts")]
public int? SmServiceAccounts { get; set; }
[Display(Name = "Max Autoscale Service Accounts")]
[Display(Name = "Max Autoscale Machine Accounts")]
public int? MaxAutoscaleSmServiceAccounts { get; set; }
/**
@ -184,8 +182,7 @@ public class OrganizationEditModel : OrganizationViewModel
public Organization ToOrganization(Organization existingOrganization)
{
existingOrganization.Name = Name;
existingOrganization.BusinessName = BusinessName;
existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingOrganization.PlanType = PlanType.Value;
existingOrganization.Plan = Plan;

View File

@ -1,6 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Enums;
namespace Bit.Admin.AdminConsole.Models;
@ -8,13 +10,21 @@ public class ProviderEditModel : ProviderViewModel
{
public ProviderEditModel() { }
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
: base(provider, providerUsers, organizations)
public ProviderEditModel(
Provider provider,
IEnumerable<ProviderUserUserDetails> providerUsers,
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
IReadOnlyCollection<ProviderPlan> providerPlans) : base(provider, providerUsers, organizations)
{
Name = provider.Name;
BusinessName = provider.BusinessName;
Name = provider.DisplayName();
BusinessName = provider.DisplayBusinessName();
BillingEmail = provider.BillingEmail;
BillingPhone = provider.BillingPhone;
TeamsMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.TeamsMonthly);
EnterpriseMonthlySeatMinimum = GetSeatMinimum(providerPlans, PlanType.EnterpriseMonthly);
Gateway = provider.Gateway;
GatewayCustomerId = provider.GatewayCustomerId;
GatewaySubscriptionId = provider.GatewaySubscriptionId;
}
[Display(Name = "Billing Email")]
@ -24,12 +34,28 @@ public class ProviderEditModel : ProviderViewModel
[Display(Name = "Business Name")]
public string BusinessName { get; set; }
public string Name { get; set; }
[Display(Name = "Events")]
[Display(Name = "Teams (Monthly) Seat Minimum")]
public int TeamsMonthlySeatMinimum { get; set; }
public Provider ToProvider(Provider existingProvider)
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
public int EnterpriseMonthlySeatMinimum { get; set; }
[Display(Name = "Gateway")]
public GatewayType? Gateway { get; set; }
[Display(Name = "Gateway Customer Id")]
public string GatewayCustomerId { get; set; }
[Display(Name = "Gateway Subscription Id")]
public string GatewaySubscriptionId { get; set; }
public virtual Provider ToProvider(Provider existingProvider)
{
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant()?.Trim();
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
existingProvider.Gateway = Gateway;
existingProvider.GatewayCustomerId = GatewayCustomerId;
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
return existingProvider;
}
private static int GetSeatMinimum(IEnumerable<ProviderPlan> providerPlans, PlanType planType)
=> providerPlans.FirstOrDefault(providerPlan => providerPlan.PlanType == planType)?.SeatMinimum ?? 0;
}

View File

@ -4,7 +4,7 @@
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@model OrganizationEditModel
@{
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
@ -58,7 +58,7 @@
</script>
}
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1>
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Name</small></h1>
@if (Model.Provider != null)
{

View File

@ -46,7 +46,7 @@
{
<tr>
<td>
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.Name</a>
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.DisplayName()</a>
</td>
<td>
@org.Plan

View File

@ -1,10 +1,10 @@
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
@model OrganizationViewModel
@{
ViewData["Title"] = "Organization: " + Model.Organization.Name;
ViewData["Title"] = "Organization: " + Model.Organization.DisplayName();
}
<h1>Organization <small>@Model.Organization.Name</small></h1>
<h1>Organization <small>@Model.Organization.DisplayName()</small></h1>
@if (Model.Provider != null)
{

View File

@ -2,8 +2,8 @@
@model Bit.Core.AdminConsole.Entities.Provider.Provider
<dl class="row">
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Name</dd>
<dd class="col-sm-8 col-lg-9">@Model.DisplayName()</dd>
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
</dl>
</dl>

View File

@ -68,7 +68,7 @@
<dt class="col-sm-4 col-lg-3">Projects</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ProjectsCount: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Service Accounts</dt>
<dt class="col-sm-4 col-lg-3">Machine Accounts</dt>
<dd class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.ServiceAccountsCount: "N/A")</dd>
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>

View File

@ -45,7 +45,7 @@
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
@Html.CheckBoxFor(m => Model.Items[i].Selected)
</td>
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
<td>@Html.ActionLink(Model.Items[i].DisplayName(), "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
</tr>
}

View File

@ -1,6 +1,8 @@
@using Bit.SharedWeb.Utilities
@using Bit.Core.AdminConsole.Enums.Provider
@using Bit.Core
@model CreateProviderModel
@inject Bit.Core.Services.IFeatureService FeatureService
@{
ViewData["Title"] = "Create Provider";
}
@ -39,6 +41,23 @@
<label asp-for="OwnerEmail"></label>
<input type="text" class="form-control" asp-for="OwnerEmail">
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
</div>
</div>
</div>
}
</div>
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">

View File

@ -1,14 +1,16 @@
@using Bit.Admin.Enums;
@using Bit.Core
@using Bit.Core.Billing.Extensions
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@inject Bit.Core.Services.IFeatureService FeatureService
@model ProviderEditModel
@{
ViewData["Title"] = "Provider: " + Model.Provider.Name;
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
}
<h1>Provider <small>@Model.Provider.Name</small></h1>
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
<h2>Provider Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)
@ -17,12 +19,12 @@
<h2>General</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.Name</dd>
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayName()</dd>
</dl>
<h2>Business Information</h2>
<dl class="row">
<dt class="col-sm-4 col-lg-3">Business Name</dt>
<dd class="col-sm-8 col-lg-9">@Model.Provider.BusinessName</dd>
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayBusinessName()</dd>
</dl>
<h2>Billing</h2>
<div class="row">
@ -41,12 +43,147 @@
</div>
</div>
</div>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
{
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="TeamsMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<div class="form-group">
<label asp-for="Gateway"></label>
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
<option value="">--</option>
</select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewayCustomerId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewayCustomerId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
<div class="col-sm">
<div class="form-group">
<label asp-for="GatewaySubscriptionId"></label>
<div class="input-group">
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
<div class="input-group-append">
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
<i class="fa fa-external-link"></i>
</button>
</div>
</div>
</div>
</div>
</div>
}
</form>
@await Html.PartialAsync("Organizations", Model)
@if (canEdit)
{
<!-- Modals -->
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded">
<div class="p-3">
<h4 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h4>
</div>
<div class="modal-body">
<span class="font-weight-light">
Enter the email of the provider admin that will receive the request to delete the provider portal.
</span>
<form>
<div class="form-group">
<label for="provider-email" class="col-form-label">Provider email</label>
<input type="email" class="form-control" id="provider-email">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content rounded">
<div class="p-3">
<h4 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h4>
</div>
<div class="modal-body">
<span class="font-weight-light">
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
</span>
<form>
<div class="form-group">
<label for="provider-name" class="col-form-label">Provider name</label>
<input type="text" class="form-control" id="provider-name">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content rounded">
<div class="modal-body">
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
<p class="font-weight-lighter">You must unlink all clients before you can delete @Model.Name.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
<!-- End of Modal Section -->
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
{
<div class="ml-auto d-flex">
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
</div>
}
</div>
}

View File

@ -52,7 +52,7 @@
{
<tr>
<td>
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(!string.IsNullOrEmpty(provider.DisplayName()) ? provider.DisplayName() : "Pending")</a>
</td>
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
<td>@provider.Status</td>

View File

@ -45,7 +45,7 @@
{
<tr>
<td class="align-middle">
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.DisplayName()</a>
</td>
<td>
@providerOrganization.Status

View File

@ -1,9 +1,9 @@
@model ProviderViewModel
@{
ViewData["Title"] = "Provider: " + Model.Provider.Name;
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
}
<h1>Provider <small>@Model.Provider.Name</small></h1>
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
<h2>Information</h2>
@await Html.PartialAsync("_ViewInformation", Model)

View File

@ -12,7 +12,7 @@
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
},
error: function (response) {
alert("Error!");
alert("Error!: " + response.responseText);
}
});
}

View File

@ -17,4 +17,60 @@
}
return false;
}
function deleteProvider(id) {
const providerName = $('#DeleteModal input#provider-name').val();
$.ajax({
type: "POST",
url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`,
dataType: 'json',
contentType: false,
processData: false,
success: function () {
$('#DeleteModal').modal('hide');
window.location.href = `@Url.Action("Index", "Providers")`;
},
error: function (response) {
alert("Error!: " + response.responseText);
}
});
}
function initiateDeleteProvider(id) {
const email = $('#requestDeletionModal input#provider-email').val();
const providerEmail = encodeURIComponent(email);
$.ajax({
type: "POST",
url: `@Url.Action("DeleteInitiation", "Providers")?id=${id}&providerEmail=${providerEmail}`,
dataType: 'json',
contentType: false,
processData: false,
success: function () {
$('#requestDeletionModal').modal('hide');
window.location.href = `@Url.Action("Index", "Providers")`;
},
error: function (response) {
alert("Error!: " + response.responseText);
}
});
}
function openDeleteModal(providerOrganizations) {
if (providerOrganizations > 0){
$('#linkAccWarningBtn').click()
} else {
$('#deleteBtn').click()
}
}
function openRequestDeleteModal(providerOrganizations) {
if (providerOrganizations > 0){
$('#linkAccWarningBtn').click()
} else {
$('#requestDeletionBtn').click()
}
}
</script>

View File

@ -9,7 +9,6 @@
@{
var canViewGeneralDetails = AccessControlService.UserHasPermission(Permission.Org_GeneralDetails_View);
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
var canViewBusinessInformation = AccessControlService.UserHasPermission(Permission.Org_BusinessInformation_View);
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
@ -28,7 +27,7 @@
<div class="col-sm">
<div class="form-group">
<label asp-for="Name"></label>
<input type="text" class="form-control" asp-for="Name" required>
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
</div>
</div>
</div>
@ -61,19 +60,6 @@
}
}
@if (canViewBusinessInformation)
{
<h2>Business Information</h2>
<div class="row">
<div class="col-sm">
<div class="form-group">
<label asp-for="BusinessName"></label>
<input type="text" class="form-control" asp-for="BusinessName">
</div>
</div>
</div>
}
@if (canViewPlan)
{
<h2>Plan</h2>

View File

@ -57,8 +57,11 @@
case '@((byte)PlanType.TeamsAnnually2019)':
case '@((byte)PlanType.TeamsMonthly2020)':
case '@((byte)PlanType.TeamsAnnually2020)':
case '@((byte)PlanType.TeamsMonthly2023)':
case '@((byte)PlanType.TeamsAnnually2023)':
case '@((byte)PlanType.TeamsMonthly)':
case '@((byte)PlanType.TeamsAnnually)':
case '@((byte)PlanType.TeamsStarter2023)':
case '@((byte)PlanType.TeamsStarter)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
document.getElementById('@(nameof(Model.UseSso))').checked = false;
@ -79,6 +82,8 @@
case '@((byte)PlanType.EnterpriseAnnually2019)':
case '@((byte)PlanType.EnterpriseMonthly2020)':
case '@((byte)PlanType.EnterpriseAnnually2020)':
case '@((byte)PlanType.EnterpriseMonthly2023)':
case '@((byte)PlanType.EnterpriseAnnually2023)':
case '@((byte)PlanType.EnterpriseMonthly)':
case '@((byte)PlanType.EnterpriseAnnually)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = true;

View File

@ -76,14 +76,18 @@ public class JobsHostedService : BaseJobsHostedService
{
new Tuple<Type, ITrigger>(typeof(DeleteSendsJob), everyFiveMinutesTrigger),
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger),
new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
new Tuple<Type, ITrigger>(typeof(DeleteUnverifiedOrganizationDomainsJob), everyDayAtTwoAmUtcTrigger),
};
if (!(_globalSettings.SqlServer?.DisableDatabaseMaintenanceJobs ?? false))
{
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger));
jobs.Add(new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger));
}
if (!_globalSettings.SelfHosted)
{
jobs.Add(new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger));

View File

@ -88,7 +88,7 @@ public class Startup
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>();
services.AddBillingCommands();
services.AddBillingOperations();
#if OSS
services.AddOosServices();

View File

@ -87,6 +87,7 @@ public static class RolePermissionMapping
Permission.Provider_List_View,
Permission.Provider_Create,
Permission.Provider_View,
Permission.Provider_Edit,
Permission.Provider_ResendEmailInvite,
Permission.Tools_ChargeBrainTreeCustomer,
Permission.Tools_PromoteAdmin,

View File

@ -30,10 +30,6 @@
"connectionString": "SECRET",
"applicationCacheTopicName": "SECRET"
},
"documentDb": {
"uri": "SECRET",
"key": "SECRET"
},
"notificationHub": {
"connectionString": "SECRET",
"hubName": "SECRET"

View File

@ -9,15 +9,15 @@
"version": "0.0.0",
"license": "GPL-3.0",
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7",
"sass": "1.75.0",
"toastr": "2.1.4"
}
},
@ -599,17 +599,23 @@
}
},
"node_modules/bootstrap": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.0.tgz",
"integrity": "sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"jquery": "1.9.1 - 3",
"popper.js": "^1.16.0"
"popper.js": "^1.16.1"
}
},
"node_modules/brace-expansion": {
@ -1029,9 +1035,9 @@
}
},
"node_modules/del": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz",
"integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==",
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz",
"integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==",
"dev": true,
"dependencies": {
"globby": "^11.0.1",
@ -2384,9 +2390,9 @@
}
},
"node_modules/jquery": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz",
"integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -3947,9 +3953,9 @@
}
},
"node_modules/sass": {
"version": "1.49.7",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.7.tgz",
"integrity": "sha512-13dml55EMIR2rS4d/RDHHP0sXMY3+30e1TKsyXaSz3iLWVoDWEoboY8WzJd5JMnxrRHffKO3wq2mpJ0jxRJiEQ==",
"version": "1.75.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.75.0.tgz",
"integrity": "sha512-ShMYi3WkrDWxExyxSZPst4/okE9ts46xZmJDSawJQrnte7M1V9fScVB+uNXOVKRBt0PggHOwoZcn8mYX4trnBw==",
"dev": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
@ -3960,7 +3966,7 @@
"sass": "sass.js"
},
"engines": {
"node": ">=12.0.0"
"node": ">=14.0.0"
}
},
"node_modules/sass/node_modules/anymatch": {
@ -3977,12 +3983,15 @@
}
},
"node_modules/sass/node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sass/node_modules/braces": {
@ -3998,16 +4007,10 @@
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@ -4020,6 +4023,9 @@
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}

View File

@ -8,15 +8,15 @@
"build": "gulp build"
},
"devDependencies": {
"bootstrap": "4.5.0",
"del": "6.0.0",
"bootstrap": "4.6.2",
"del": "6.1.1",
"font-awesome": "4.7.0",
"gulp": "4.0.2",
"gulp-sass": "5.1.0",
"jquery": "3.5.1",
"jquery": "3.7.1",
"merge-stream": "2.0.0",
"popper.js": "1.16.1",
"sass": "1.49.7",
"sass": "1.75.0",
"toastr": "2.1.4"
}
}

View File

@ -2,7 +2,9 @@
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Api.Vault.AuthorizationHandlers.Groups;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
@ -28,6 +30,10 @@ public class GroupsController : Controller
private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IUserService _userService;
private readonly IFeatureService _featureService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository;
public GroupsController(
IGroupRepository groupRepository,
@ -38,7 +44,11 @@ public class GroupsController : Controller
IUpdateGroupCommand updateGroupCommand,
IDeleteGroupCommand deleteGroupCommand,
IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService)
IApplicationCacheService applicationCacheService,
IUserService userService,
IFeatureService featureService,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
_groupRepository = groupRepository;
_groupService = groupService;
@ -49,6 +59,10 @@ public class GroupsController : Controller
_deleteGroupCommand = deleteGroupCommand;
_authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
_userService = userService;
_featureService = featureService;
_organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository;
}
[HttpGet("{id}")]
@ -115,16 +129,30 @@ public class GroupsController : Controller
}
[HttpPost("")]
public async Task<GroupResponseModel> Post(string orgId, [FromBody] GroupRequestModel model)
public async Task<GroupResponseModel> Post(Guid orgId, [FromBody] GroupRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManageGroups(orgIdGuid))
if (!await _currentContext.ManageGroups(orgId))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
var group = model.ToGroup(orgIdGuid);
// Flexible Collections - check the user has permission to grant access to the collections for the new group
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
model.Collections?.Any() == true)
{
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
var authorized =
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyGroupAccess))
.Succeeded;
if (!authorized)
{
throw new NotFoundException("You are not authorized to grant access to these collections.");
}
}
var organization = await _organizationRepository.GetByIdAsync(orgId);
var group = model.ToGroup(orgId);
await _createGroupCommand.CreateGroupAsync(group, organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
return new GroupResponseModel(group);
@ -132,30 +160,85 @@ public class GroupsController : Controller
[HttpPut("{id}")]
[HttpPost("{id}")]
public async Task<GroupResponseModel> Put(string orgId, string id, [FromBody] GroupRequestModel model)
public async Task<GroupResponseModel> Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
{
// Use new Flexible Collections v1 logic
return await Put_vNext(orgId, id, model);
}
// Pre-Flexible Collections v1 logic follows
var group = await _groupRepository.GetByIdAsync(id);
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
var orgIdGuid = new Guid(orgId);
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
var organization = await _organizationRepository.GetByIdAsync(orgId);
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization,
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Users);
return new GroupResponseModel(group);
}
[HttpPut("{id}/users")]
public async Task PutUsers(string orgId, string id, [FromBody] IEnumerable<Guid> model)
/// <summary>
/// Put logic for Flexible Collections v1
/// </summary>
private async Task<GroupResponseModel> Put_vNext(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
var (group, currentAccess) = await _groupRepository.GetByIdWithCollectionsAsync(id);
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
{
throw new NotFoundException();
}
await _groupRepository.UpdateUsersAsync(group.Id, model);
// Check whether the user is permitted to add themselves to the group
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
if (!orgAbility.AllowAdminAccessToAllCollectionItems)
{
var userId = _userService.GetProperUserId(User).Value;
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);
var currentGroupUsers = await _groupRepository.GetManyUserIdsByIdAsync(id);
if (!currentGroupUsers.Contains(organizationUser.Id) && model.Users.Contains(organizationUser.Id))
{
throw new BadRequestException("You cannot add yourself to groups.");
}
}
// The client only sends collections that the saving user has permissions to edit.
// On the server side, we need to (1) confirm this and (2) concat these with the collections that the user
// can't edit before saving to the database.
var currentCollections = await _collectionRepository
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
var readonlyCollectionIds = new HashSet<Guid>();
foreach (var collection in currentCollections)
{
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyGroupAccess))
.Succeeded)
{
readonlyCollectionIds.Add(collection.Id);
}
}
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
{
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
}
var editedCollectionAccess = model.Collections
.Select(c => c.ToSelectionReadOnly());
var readonlyCollectionAccess = currentAccess
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
var collectionsToSave = editedCollectionAccess
.Concat(readonlyCollectionAccess)
.ToList();
var organization = await _organizationRepository.GetByIdAsync(orgId);
await _updateGroupCommand.UpdateGroupAsync(model.ToGroup(group), organization, collectionsToSave, model.Users);
return new GroupResponseModel(group);
}
[HttpDelete("{id}")]

View File

@ -80,7 +80,6 @@ public class OrganizationDomainController : Controller
var organizationDomain = new OrganizationDomain
{
OrganizationId = orgId,
Txt = model.Txt,
DomainName = model.DomainName.ToLower()
};

View File

@ -3,7 +3,9 @@ using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Request.Organizations;
using Bit.Api.Models.Response;
using Bit.Api.Utilities;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -37,10 +39,12 @@ public class OrganizationUsersController : Controller
private readonly ICurrentContext _currentContext;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpdateOrganizationUserCommand _updateOrganizationUserCommand;
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IFeatureService _featureService;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
@ -53,10 +57,12 @@ public class OrganizationUsersController : Controller
ICurrentContext currentContext,
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpdateOrganizationUserCommand updateOrganizationUserCommand,
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
IAcceptOrgUserCommand acceptOrgUserCommand,
IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService)
IApplicationCacheService applicationCacheService,
IFeatureService featureService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -68,10 +74,12 @@ public class OrganizationUsersController : Controller
_currentContext = currentContext;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_updateOrganizationUserCommand = updateOrganizationUserCommand;
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
_acceptOrgUserCommand = acceptOrgUserCommand;
_authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
_featureService = featureService;
}
[HttpGet("{id}")]
@ -90,8 +98,11 @@ public class OrganizationUsersController : Controller
response.Type = GetFlexibleCollectionsUserType(response.Type, response.Permissions);
// Set 'Edit/Delete Assigned Collections' custom permissions to false
response.Permissions.EditAssignedCollections = false;
response.Permissions.DeleteAssignedCollections = false;
if (response.Permissions is not null)
{
response.Permissions.EditAssignedCollections = false;
response.Permissions.DeleteAssignedCollections = false;
}
}
if (includeGroups)
@ -176,16 +187,30 @@ public class OrganizationUsersController : Controller
}
[HttpPost("invite")]
public async Task Invite(string orgId, [FromBody] OrganizationUserInviteRequestModel model)
public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
// Flexible Collections - check the user has permission to grant access to the collections for the new user
if (await FlexibleCollectionsIsEnabledAsync(orgId) &&
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
model.Collections?.Any() == true)
{
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Collections.Select(a => a.Id));
var authorized =
(await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.ModifyUserAccess))
.Succeeded;
if (!authorized)
{
throw new NotFoundException("You are not authorized to grant access to these collections.");
}
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.InviteUsersAsync(orgGuidId, userId.Value,
await _organizationService.InviteUsersAsync(orgId, userId.Value,
new (OrganizationUserInvite, string)[] { (new OrganizationUserInvite(model.ToData()), null) });
}
@ -305,43 +330,99 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}")]
[HttpPost("{id}")]
public async Task Put(string orgId, string id, [FromBody] OrganizationUserUpdateRequestModel model)
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (await FlexibleCollectionsIsEnabledAsync(orgId) && _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1))
{
// Use new Flexible Collections v1 logic
await Put_vNext(orgId, id, model);
return;
}
// Pre-Flexible Collections v1 code follows
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
if (organizationUser == null || organizationUser.OrganizationId != orgId)
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
model.Collections.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
}
[HttpPut("{id}/groups")]
[HttpPost("{id}/groups")]
public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model)
/// <summary>
/// Put logic for Flexible Collections v1
/// </summary>
private async Task Put_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
var (organizationUser, currentAccess) = await _organizationUserRepository.GetByIdWithCollectionsAsync(id);
if (organizationUser == null || organizationUser.OrganizationId != orgId)
{
throw new NotFoundException();
}
var loggedInUserId = _userService.GetProperUserId(User);
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
var userId = _userService.GetProperUserId(User).Value;
var editingSelf = userId == organizationUser.UserId;
// If admins are not allowed access to all collections, you cannot add yourself to a group.
// In this case we just don't update groups.
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
var groupsToSave = editingSelf && !organizationAbility.AllowAdminAccessToAllCollectionItems
? null
: model.Groups;
// If admins are not allowed access to all collections, you cannot add yourself to collections.
// This is not caught by the requirement below that you can ModifyUserAccess and must be checked separately
var currentAccessIds = currentAccess.Select(c => c.Id).ToHashSet();
if (editingSelf &&
!organizationAbility.AllowAdminAccessToAllCollectionItems &&
model.Collections.Any(c => !currentAccessIds.Contains(c.Id)))
{
throw new BadRequestException("You cannot add yourself to a collection.");
}
// The client only sends collections that the saving user has permissions to edit.
// On the server side, we need to (1) make sure the user has permissions for these collections, and
// (2) concat these with the collections that the user can't edit before saving to the database.
var currentCollections = await _collectionRepository
.GetManyByManyIdsAsync(currentAccess.Select(cas => cas.Id));
var readonlyCollectionIds = new HashSet<Guid>();
foreach (var collection in currentCollections)
{
if (!(await _authorizationService.AuthorizeAsync(User, collection, BulkCollectionOperations.ModifyUserAccess))
.Succeeded)
{
readonlyCollectionIds.Add(collection.Id);
}
}
if (model.Collections.Any(c => readonlyCollectionIds.Contains(c.Id)))
{
throw new BadRequestException("You must have Can Manage permissions to edit a collection's membership");
}
var editedCollectionAccess = model.Collections
.Select(c => c.ToSelectionReadOnly());
var readonlyCollectionAccess = currentAccess
.Where(ca => readonlyCollectionIds.Contains(ca.Id));
var collectionsToSave = editedCollectionAccess
.Concat(readonlyCollectionAccess)
.ToList();
await _updateOrganizationUserCommand.UpdateUserAsync(model.ToOrganizationUser(organizationUser), userId,
collectionsToSave, groupsToSave);
}
[HttpPut("{userId}/reset-password-enrollment")]
@ -557,8 +638,11 @@ public class OrganizationUsersController : Controller
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
// Set 'Edit/Delete Assigned Collections' custom permissions to false
orgUser.Permissions.EditAssignedCollections = false;
orgUser.Permissions.DeleteAssignedCollections = false;
if (orgUser.Permissions is not null)
{
orgUser.Permissions.EditAssignedCollections = false;
orgUser.Permissions.DeleteAssignedCollections = false;
}
return orgUser;
});
@ -570,7 +654,7 @@ public class OrganizationUsersController : Controller
private OrganizationUserType GetFlexibleCollectionsUserType(OrganizationUserType type, Permissions permissions)
{
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
if (type == OrganizationUserType.Custom)
if (type == OrganizationUserType.Custom && permissions is not null)
{
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
permissions is

View File

@ -20,6 +20,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Queries;
using Bit.Core.Context;
@ -66,9 +67,11 @@ public class OrganizationsController : Controller
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
private readonly IPushNotificationService _pushNotificationService;
private readonly ICancelSubscriptionCommand _cancelSubscriptionCommand;
private readonly IGetSubscriptionQuery _getSubscriptionQuery;
private readonly ISubscriberQueries _subscriberQueries;
private readonly IReferenceEventService _referenceEventService;
private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand;
private readonly IProviderRepository _providerRepository;
private readonly IScaleSeatsCommand _scaleSeatsCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -93,9 +96,11 @@ public class OrganizationsController : Controller
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IPushNotificationService pushNotificationService,
ICancelSubscriptionCommand cancelSubscriptionCommand,
IGetSubscriptionQuery getSubscriptionQuery,
ISubscriberQueries subscriberQueries,
IReferenceEventService referenceEventService,
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand)
IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand,
IProviderRepository providerRepository,
IScaleSeatsCommand scaleSeatsCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -119,9 +124,11 @@ public class OrganizationsController : Controller
_addSecretsManagerSubscriptionCommand = addSecretsManagerSubscriptionCommand;
_pushNotificationService = pushNotificationService;
_cancelSubscriptionCommand = cancelSubscriptionCommand;
_getSubscriptionQuery = getSubscriptionQuery;
_subscriberQueries = subscriberQueries;
_referenceEventService = referenceEventService;
_organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand;
_providerRepository = providerRepository;
_scaleSeatsCommand = scaleSeatsCommand;
}
[HttpGet("{id}")]
@ -303,7 +310,7 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
model.BillingEmail != organization.BillingEmail);
var hasRequiredPermissions = updateBilling
@ -464,8 +471,8 @@ public class OrganizationsController : Controller
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
}
[HttpPost("{id}/churn")]
public async Task PostChurn(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
[HttpPost("{id}/cancel")]
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
{
if (!await _currentContext.EditSubscription(id))
{
@ -479,7 +486,7 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
var subscription = await _getSubscriptionQuery.GetSubscription(organization);
var subscription = await _subscriberQueries.GetSubscriptionOrThrow(organization);
await _cancelSubscriptionCommand.CancelSubscription(subscription,
new OffboardingSurveyResponse
@ -499,19 +506,6 @@ public class OrganizationsController : Controller
});
}
[HttpPost("{id}/cancel")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostCancel(string id)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.EditSubscription(orgIdGuid))
{
throw new NotFoundException();
}
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
}
[HttpPost("{id}/reinstate")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostReinstate(string id)
@ -573,10 +567,23 @@ public class OrganizationsController : Controller
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
else
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
if (consolidatedBillingEnabled && organization.IsValidClient())
{
await _organizationService.DeleteAsync(organization);
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
if (provider.IsBillable())
{
await _scaleSeatsCommand.ScalePasswordManagerSeats(
provider,
organization.PlanType,
-organization.Seats ?? 0);
}
}
await _organizationService.DeleteAsync(organization);
}
[HttpPost("{id}/import")]
@ -737,7 +744,7 @@ public class OrganizationsController : Controller
[HttpPut("{id}/tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PutTaxInfo(string id, [FromBody] OrganizationTaxInfoUpdateRequestModel model)
public async Task PutTaxInfo(string id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!await _currentContext.OrganizationOwner(orgIdGuid))

View File

@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Extensions;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -112,6 +113,9 @@ public class ProviderOrganizationsController : Controller
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
if (organization.IsStripeEnabled())
{
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
}
}
}

View File

@ -1,9 +1,12 @@
using Bit.Api.AdminConsole.Models.Request.Providers;
using Bit.Api.AdminConsole.Models.Response.Providers;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Commands;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Authorization;
@ -20,15 +23,23 @@ public class ProvidersController : Controller
private readonly IProviderService _providerService;
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly IStartSubscriptionCommand _startSubscriptionCommand;
private readonly ILogger<ProvidersController> _logger;
public ProvidersController(IUserService userService, IProviderRepository providerRepository,
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings)
IProviderService providerService, ICurrentContext currentContext, GlobalSettings globalSettings,
IFeatureService featureService, IStartSubscriptionCommand startSubscriptionCommand,
ILogger<ProvidersController> logger)
{
_userService = userService;
_providerRepository = providerRepository;
_providerService = providerService;
_currentContext = currentContext;
_globalSettings = globalSettings;
_featureService = featureService;
_startSubscriptionCommand = startSubscriptionCommand;
_logger = logger;
}
[HttpGet("{id:guid}")]
@ -86,6 +97,66 @@ public class ProvidersController : Controller
var response =
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key);
if (_featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
{
var taxInfo = new TaxInfo
{
BillingAddressCountry = model.TaxInfo.Country,
BillingAddressPostalCode = model.TaxInfo.PostalCode,
TaxIdNumber = model.TaxInfo.TaxId,
BillingAddressLine1 = model.TaxInfo.Line1,
BillingAddressLine2 = model.TaxInfo.Line2,
BillingAddressCity = model.TaxInfo.City,
BillingAddressState = model.TaxInfo.State
};
try
{
await _startSubscriptionCommand.StartSubscription(provider, taxInfo);
}
catch
{
// We don't want to trap the user on the setup page, so we'll let this go through but the provider will be in an un-billable state.
_logger.LogError("Failed to create subscription for provider with ID {ID} during setup", provider.Id);
}
}
return new ProviderResponseModel(response);
}
[HttpPost("{id}/delete-recover-token")]
[AllowAnonymous]
public async Task PostDeleteRecoverToken(Guid id, [FromBody] ProviderVerifyDeleteRecoverRequestModel model)
{
var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null)
{
throw new NotFoundException();
}
await _providerService.DeleteAsync(provider, model.Token);
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(Guid id)
{
if (!_currentContext.ProviderProviderAdmin(id))
{
throw new NotFoundException();
}
var provider = await _providerRepository.GetByIdAsync(id);
if (provider == null)
{
throw new NotFoundException();
}
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
await _providerService.DeleteAsync(provider);
}
}

View File

@ -4,9 +4,6 @@ namespace Bit.Api.AdminConsole.Models.Request;
public class OrganizationDomainRequestModel
{
[Required]
public string Txt { get; set; }
[Required]
public string DomainName { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
@ -9,9 +10,11 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationCreateRequestModel : IValidatableObject
{
[Required]
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; }
[Required]
[StringLength(256)]

View File

@ -1,16 +1,20 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Models.Data;
using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationUpdateRequestModel
{
[Required]
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; }
[EmailAddress]
[Required]

View File

@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel
}
}
public class OrganizationUserUpdateGroupsRequestModel
{
[Required]
public IEnumerable<string> GroupIds { get; set; }
}
public class OrganizationUserResetPasswordEnrollmentRequestModel
{
public string ResetPasswordKey { get; set; }

View File

@ -1,14 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Api.Models.Request;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Providers;
public class ProviderSetupRequestModel
{
[Required]
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; }
[Required]
[StringLength(256)]
@ -18,6 +23,7 @@ public class ProviderSetupRequestModel
public string Token { get; set; }
[Required]
public string Key { get; set; }
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
public virtual Provider ToProvider(Provider provider)
{

View File

@ -1,15 +1,19 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Settings;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Request.Providers;
public class ProviderUpdateRequestModel
{
[Required]
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
[StringLength(50)]
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; }
[EmailAddress]
[Required]

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.AdminConsole.Models.Request.Providers;
public class ProviderVerifyDeleteRecoverRequestModel
{
[Required]
public string Token { get; set; }
}

View File

@ -1,4 +1,5 @@
using Bit.Api.Models.Response;
using System.Text.Json.Serialization;
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
@ -60,7 +61,9 @@ public class OrganizationResponseModel : ResponseModel
}
public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string BusinessName { get; set; }
public string BusinessAddress1 { get; set; }
public string BusinessAddress2 { get; set; }
@ -123,8 +126,14 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
if (hideSensitiveData)
{
BillingEmail = null;
Subscription.Items = null;
UpcomingInvoice.Amount = null;
if (Subscription != null)
{
Subscription.Items = null;
}
if (UpcomingInvoice != null)
{
UpcomingInvoice.Amount = null;
}
}
}
@ -133,12 +142,13 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{
if (license != null)
{
// License expiration should always include grace period - See OrganizationLicense.cs
// License expiration should always include grace period (unless it's in a Trial) - See OrganizationLicense.cs.
Expiration = license.Expires;
// Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ??
license.Expires?.AddDays(-Constants
.OrganizationSelfHostSubscriptionGracePeriodDays);
// Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period unless it's in a Trial.
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial
? license.Expires
: license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums.Provider;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Enums;
@ -73,7 +74,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
if (FlexibleCollections)
{
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
if (Type == OrganizationUserType.Custom)
if (Type == OrganizationUserType.Custom && Permissions is not null)
{
if ((Permissions.EditAssignedCollections || Permissions.DeleteAssignedCollections) &&
Permissions is
@ -97,12 +98,16 @@ public class ProfileOrganizationResponseModel : ResponseModel
}
// Set 'Edit/Delete Assigned Collections' custom permissions to false
Permissions.EditAssignedCollections = false;
Permissions.DeleteAssignedCollections = false;
if (Permissions is not null)
{
Permissions.EditAssignedCollections = false;
Permissions.DeleteAssignedCollections = false;
}
}
}
public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
public bool UsePolicies { get; set; }
public bool UseSso { get; set; }
@ -135,6 +140,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
public Guid? UserId { get; set; }
public bool HasPublicAndPrivateKeys { get; set; }
public Guid? ProviderId { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; }
public ProviderType? ProviderType { get; set; }
public string FamilySponsorshipFriendlyName { get; set; }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Enums.Provider;
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
@ -20,9 +21,11 @@ public class ProfileProviderResponseModel : ResponseModel
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(provider.Permissions);
UserId = provider.UserId;
UseEvents = provider.UseEvents;
ProviderStatus = provider.ProviderStatus;
}
public Guid Id { get; set; }
[JsonConverter(typeof(HtmlEncodingStringConverter))]
public string Name { get; set; }
public string Key { get; set; }
public ProviderUserStatusType Status { get; set; }
@ -31,4 +34,5 @@ public class ProfileProviderResponseModel : ResponseModel
public Permissions Permissions { get; set; }
public Guid? UserId { get; set; }
public bool UseEvents { get; set; }
public ProviderStatusType ProviderStatus { get; set; }
}

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