mirror of
https://github.com/bitwarden/server.git
synced 2025-05-29 15:24:51 -05:00
Merge remote-tracking branch 'origin/main' into xunit-v3-migration
This commit is contained in:
commit
c52c64f07d
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"swashbuckle.aspnetcore.cli": {
|
||||
"version": "6.9.0",
|
||||
"version": "7.2.0",
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
bitwarden_server:
|
||||
image: mcr.microsoft.com/devcontainers/dotnet:8.0
|
||||
@ -13,7 +11,8 @@ services:
|
||||
platform: linux/amd64
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
../../dev/.env
|
||||
- path: ../../dev/.env
|
||||
required: false
|
||||
environment:
|
||||
ACCEPT_EULA: "Y"
|
||||
MSSQL_PID: Developer
|
||||
|
@ -3,6 +3,11 @@
|
||||
"dockerComposeFile": "../../.devcontainer/bitwarden_common/docker-compose.yml",
|
||||
"service": "bitwarden_server",
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "16"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
"source": "../../dev/.data/keys",
|
||||
@ -13,7 +18,6 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
"features": {},
|
||||
"extensions": ["ms-dotnettools.csdevkit"]
|
||||
}
|
||||
},
|
||||
|
@ -51,4 +51,10 @@ Proceed? [y/N] " response
|
||||
}
|
||||
|
||||
# main
|
||||
one_time_setup
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
one_time_setup
|
||||
else
|
||||
# Ignore interactive elements when running in codespaces since they are not supported there
|
||||
# TODO Write codespaces specific instructions and link here
|
||||
echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup"
|
||||
fi
|
||||
|
@ -6,6 +6,11 @@
|
||||
],
|
||||
"service": "bitwarden_server",
|
||||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "16"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
"source": "../../dev/.data/keys",
|
||||
@ -16,15 +21,39 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
"features": {},
|
||||
"extensions": ["ms-dotnettools.csdevkit"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh",
|
||||
"forwardPorts": [1080, 1433, 3306, 5432, 10000, 10001, 10002],
|
||||
"portsAttributes": {
|
||||
"1080": {
|
||||
"label": "Mail Catcher",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"1433": {
|
||||
"label": "SQL Server",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"3306": {
|
||||
"label": "MySQL",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"5432": {
|
||||
"label": "PostgreSQL",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"10000": {
|
||||
"label": "Azurite Storage Blob",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"10001": {
|
||||
"label": "Azurite Storage Queue ",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"10002": {
|
||||
"label": "Azurite Storage Table",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
bitwarden_storage:
|
||||
image: mcr.microsoft.com/azure-storage/azurite:latest
|
||||
|
@ -70,7 +70,29 @@ Press <Enter> to continue."
|
||||
sleep 5 # wait for DB container to start
|
||||
dotnet run --project ./util/MsSqlMigratorUtility "$SQL_CONNECTION_STRING"
|
||||
fi
|
||||
read -r -p "Would you like to install the Stripe CLI? [y/N] " stripe_response
|
||||
if [[ "$stripe_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
install_stripe_cli
|
||||
fi
|
||||
}
|
||||
|
||||
# Install Stripe CLI
|
||||
install_stripe_cli() {
|
||||
echo "Installing Stripe CLI..."
|
||||
# Add Stripe CLI GPG key so that apt can verify the packages authenticity.
|
||||
# If Stripe ever changes the key, we'll need to update this. Visit https://docs.stripe.com/stripe-cli?install-method=apt if so
|
||||
curl -s https://packages.stripe.dev/api/security/keypair/stripe-cli-gpg/public | gpg --dearmor | sudo tee /usr/share/keyrings/stripe.gpg >/dev/null
|
||||
# Add Stripe CLI repository to apt sources
|
||||
echo "deb [signed-by=/usr/share/keyrings/stripe.gpg] https://packages.stripe.dev/stripe-cli-debian-local stable main" | sudo tee -a /etc/apt/sources.list.d/stripe.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install -y stripe
|
||||
}
|
||||
|
||||
# main
|
||||
one_time_setup
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
one_time_setup
|
||||
else
|
||||
# Ignore interactive elements when running in codespaces since they are not supported there
|
||||
# TODO Write codespaces specific instructions and link here
|
||||
echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup"
|
||||
fi
|
14
.github/CODEOWNERS
vendored
14
.github/CODEOWNERS
vendored
@ -15,11 +15,7 @@
|
||||
|
||||
## These are shared workflows ##
|
||||
.github/workflows/_move_finalization_db_scripts.yml
|
||||
.github/workflows/build.yml
|
||||
.github/workflows/cleanup-after-pr.yml
|
||||
.github/workflows/cleanup-rc-branch.yml
|
||||
.github/workflows/release.yml
|
||||
.github/workflows/repository-management.yml
|
||||
|
||||
# Database Operations for database changes
|
||||
src/Sql/** @bitwarden/dept-dbops
|
||||
@ -38,7 +34,6 @@ src/Identity @bitwarden/team-auth-dev
|
||||
# Key Management team
|
||||
**/KeyManagement @bitwarden/team-key-management-dev
|
||||
|
||||
**/SecretsManager @bitwarden/team-secrets-manager-dev
|
||||
**/Tools @bitwarden/team-tools-dev
|
||||
|
||||
# Vault team
|
||||
@ -69,6 +64,15 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
|
||||
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
||||
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
|
||||
# Platform team
|
||||
.github/workflows/build.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
**/*Platform* @bitwarden/team-platform-dev
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (BRE)
|
||||
**/packages.lock.json
|
||||
Directory.Build.props
|
||||
|
198
.github/renovate.json
vendored
198
.github/renovate.json
vendored
@ -1,198 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>bitwarden/renovate-config"],
|
||||
"enabledManagers": [
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions",
|
||||
"npm",
|
||||
"nuget"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"groupName": "dockerfile minor",
|
||||
"matchManagers": ["dockerfile"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"groupName": "docker-compose minor",
|
||||
"matchManagers": ["docker-compose"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"groupName": "gh minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions", "dockerfile", "docker-compose"],
|
||||
"commitMessagePrefix": "[deps] DevOps:"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["DnsClient"],
|
||||
"description": "Admin Console owned dependencies",
|
||||
"commitMessagePrefix": "[deps] AC:",
|
||||
"reviewers": ["team:team-admin-console-dev"]
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["src/Admin/package.json", "src/Sso/package.json"],
|
||||
"description": "Admin & SSO npm packages",
|
||||
"commitMessagePrefix": "[deps] Auth:",
|
||||
"reviewers": ["team:team-auth-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.Extensions.Identity.Stores",
|
||||
"Otp.NET",
|
||||
"Sustainsys.Saml2.AspNetCore2",
|
||||
"YubicoDotNetClient"
|
||||
],
|
||||
"description": "Auth owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Auth:",
|
||||
"reviewers": ["team:team-auth-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AutoFixture.AutoNSubstitute",
|
||||
"AutoFixture.Xunit2",
|
||||
"BenchmarkDotNet",
|
||||
"BitPay.Light",
|
||||
"Braintree",
|
||||
"coverlet.collector",
|
||||
"FluentAssertions",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
"Microsoft.Extensions.Logging.Console",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
"Serilog.AspNetCore",
|
||||
"Serilog.Extensions.Logging",
|
||||
"Serilog.Extensions.Logging.File",
|
||||
"Serilog.Sinks.AzureCosmosDB",
|
||||
"Serilog.Sinks.SyslogMessages",
|
||||
"Stripe.net",
|
||||
"Swashbuckle.AspNetCore",
|
||||
"Swashbuckle.AspNetCore.SwaggerGen",
|
||||
"xunit",
|
||||
"xunit.runner.visualstudio"
|
||||
],
|
||||
"description": "Billing owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Billing:",
|
||||
"reviewers": ["team:team-billing-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^Microsoft.Extensions.Logging"],
|
||||
"groupName": "Microsoft.Extensions.Logging",
|
||||
"description": "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"Dapper",
|
||||
"dbup-sqlserver",
|
||||
"dotnet-ef",
|
||||
"linq2db.EntityFrameworkCore",
|
||||
"Microsoft.Azure.Cosmos",
|
||||
"Microsoft.Data.SqlClient",
|
||||
"Microsoft.EntityFrameworkCore.Design",
|
||||
"Microsoft.EntityFrameworkCore.InMemory",
|
||||
"Microsoft.EntityFrameworkCore.Relational",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||
"Microsoft.Extensions.Caching.SqlServer",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||
"Pomelo.EntityFrameworkCore.MySql"
|
||||
],
|
||||
"description": "DbOps owned dependencies",
|
||||
"commitMessagePrefix": "[deps] DbOps:",
|
||||
"reviewers": ["team:dept-dbops"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["CommandDotNet", "YamlDotNet"],
|
||||
"description": "DevOps owned dependencies",
|
||||
"commitMessagePrefix": "[deps] DevOps:",
|
||||
"reviewers": ["team:dept-devops"]
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.AspNetCore.Http",
|
||||
"Quartz"
|
||||
],
|
||||
"description": "Platform owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Platform:",
|
||||
"reviewers": ["team:team-platform-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["EntityFrameworkCore", "^dotnet-ef"],
|
||||
"groupName": "EntityFrameworkCore",
|
||||
"description": "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||
"AWSSDK.SimpleEmail",
|
||||
"AWSSDK.SQS",
|
||||
"Handlebars.Net",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"MailKit",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||
"Microsoft.Azure.NotificationHubs",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||
"Microsoft.Extensions.Configuration",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"SendGrid"
|
||||
],
|
||||
"description": "Tools owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Tools:",
|
||||
"reviewers": ["team:team-tools-dev"]
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^Microsoft.AspNetCore.SignalR"],
|
||||
"groupName": "SignalR",
|
||||
"description": "Group SignalR to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^Microsoft.Extensions.Configuration"],
|
||||
"groupName": "Microsoft.Extensions.Configuration",
|
||||
"description": "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["^Microsoft.Extensions.DependencyInjection"],
|
||||
"groupName": "Microsoft.Extensions.DependencyInjection",
|
||||
"description": "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": [
|
||||
"AngleSharp",
|
||||
"AspNetCore.HealthChecks.AzureServiceBus",
|
||||
"AspNetCore.HealthChecks.AzureStorage",
|
||||
"AspNetCore.HealthChecks.Network",
|
||||
"AspNetCore.HealthChecks.Redis",
|
||||
"AspNetCore.HealthChecks.SendGrid",
|
||||
"AspNetCore.HealthChecks.SqlServer",
|
||||
"AspNetCore.HealthChecks.Uris"
|
||||
],
|
||||
"description": "Vault owned dependencies",
|
||||
"commitMessagePrefix": "[deps] Vault:",
|
||||
"reviewers": ["team:team-vault-dev"]
|
||||
}
|
||||
],
|
||||
"ignoreDeps": ["dotnet-sdk"]
|
||||
}
|
199
.github/renovate.json5
vendored
Normal file
199
.github/renovate.json5
vendored
Normal file
@ -0,0 +1,199 @@
|
||||
{
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
extends: ["github>bitwarden/renovate-config"], // Extends our default configuration for pinned dependencies
|
||||
enabledManagers: [
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions",
|
||||
"npm",
|
||||
"nuget",
|
||||
],
|
||||
packageRules: [
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
matchManagers: ["dockerfile"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
groupName: "docker-compose minor",
|
||||
matchManagers: ["docker-compose"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
groupName: "github-action minor",
|
||||
matchManagers: ["github-actions"],
|
||||
matchUpdateTypes: ["minor"],
|
||||
},
|
||||
{
|
||||
matchManagers: ["dockerfile", "docker-compose"],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
},
|
||||
{
|
||||
matchPackageNames: ["DnsClient"],
|
||||
description: "Admin Console owned dependencies",
|
||||
commitMessagePrefix: "[deps] AC:",
|
||||
reviewers: ["team:team-admin-console-dev"],
|
||||
},
|
||||
{
|
||||
matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"],
|
||||
description: "Admin & SSO npm packages",
|
||||
commitMessagePrefix: "[deps] Auth:",
|
||||
reviewers: ["team:team-auth-dev"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.Extensions.Identity.Stores",
|
||||
"Otp.NET",
|
||||
"Sustainsys.Saml2.AspNetCore2",
|
||||
"YubicoDotNetClient",
|
||||
],
|
||||
description: "Auth owned dependencies",
|
||||
commitMessagePrefix: "[deps] Auth:",
|
||||
reviewers: ["team:team-auth-dev"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AutoFixture.AutoNSubstitute",
|
||||
"AutoFixture.Xunit2",
|
||||
"BenchmarkDotNet",
|
||||
"BitPay.Light",
|
||||
"Braintree",
|
||||
"coverlet.collector",
|
||||
"CsvHelper",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
"Microsoft.Extensions.Logging.Console",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
"Serilog.AspNetCore",
|
||||
"Serilog.Extensions.Logging",
|
||||
"Serilog.Extensions.Logging.File",
|
||||
"Serilog.Sinks.AzureCosmosDB",
|
||||
"Serilog.Sinks.SyslogMessages",
|
||||
"Stripe.net",
|
||||
"Swashbuckle.AspNetCore",
|
||||
"Swashbuckle.AspNetCore.SwaggerGen",
|
||||
"xunit",
|
||||
"xunit.runner.visualstudio",
|
||||
],
|
||||
description: "Billing owned dependencies",
|
||||
commitMessagePrefix: "[deps] Billing:",
|
||||
reviewers: ["team:team-billing-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
||||
groupName: "Microsoft.Extensions.Logging",
|
||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"Dapper",
|
||||
"dbup-sqlserver",
|
||||
"dotnet-ef",
|
||||
"linq2db.EntityFrameworkCore",
|
||||
"Microsoft.Azure.Cosmos",
|
||||
"Microsoft.Data.SqlClient",
|
||||
"Microsoft.EntityFrameworkCore.Design",
|
||||
"Microsoft.EntityFrameworkCore.InMemory",
|
||||
"Microsoft.EntityFrameworkCore.Relational",
|
||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||
"Microsoft.Extensions.Caching.Cosmos",
|
||||
"Microsoft.Extensions.Caching.SqlServer",
|
||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||
"Pomelo.EntityFrameworkCore.MySql",
|
||||
],
|
||||
description: "DbOps owned dependencies",
|
||||
commitMessagePrefix: "[deps] DbOps:",
|
||||
reviewers: ["team:dept-dbops"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: ["CommandDotNet", "YamlDotNet"],
|
||||
description: "DevOps owned dependencies",
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
reviewers: ["team:dept-bre"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AspNetCoreRateLimit",
|
||||
"AspNetCoreRateLimit.Redis",
|
||||
"Azure.Data.Tables",
|
||||
"Azure.Messaging.EventGrid",
|
||||
"Azure.Messaging.ServiceBus",
|
||||
"Azure.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||
"Microsoft.AspNetCore.Http",
|
||||
"Quartz",
|
||||
],
|
||||
description: "Platform owned dependencies",
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
reviewers: ["team:team-platform-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||
"AWSSDK.SimpleEmail",
|
||||
"AWSSDK.SQS",
|
||||
"Handlebars.Net",
|
||||
"LaunchDarkly.ServerSdk",
|
||||
"MailKit",
|
||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||
"Microsoft.Azure.NotificationHubs",
|
||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||
"Microsoft.Extensions.Configuration",
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"SendGrid",
|
||||
],
|
||||
description: "Tools owned dependencies",
|
||||
commitMessagePrefix: "[deps] Tools:",
|
||||
reviewers: ["team:team-tools-dev"],
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
|
||||
groupName: "SignalR",
|
||||
description: "Group SignalR to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
|
||||
groupName: "Microsoft.Extensions.Configuration",
|
||||
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
|
||||
groupName: "Microsoft.Extensions.DependencyInjection",
|
||||
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"AngleSharp",
|
||||
"AspNetCore.HealthChecks.AzureServiceBus",
|
||||
"AspNetCore.HealthChecks.AzureStorage",
|
||||
"AspNetCore.HealthChecks.Network",
|
||||
"AspNetCore.HealthChecks.Redis",
|
||||
"AspNetCore.HealthChecks.SendGrid",
|
||||
"AspNetCore.HealthChecks.SqlServer",
|
||||
"AspNetCore.HealthChecks.Uris",
|
||||
],
|
||||
description: "Vault owned dependencies",
|
||||
commitMessagePrefix: "[deps] Vault:",
|
||||
reviewers: ["team:team-vault-dev"],
|
||||
},
|
||||
],
|
||||
ignoreDeps: ["dotnet-sdk"],
|
||||
}
|
72
.github/workflows/build.yml
vendored
72
.github/workflows/build.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@ -120,7 +120,7 @@ jobs:
|
||||
ls -atlh ../../../
|
||||
|
||||
- name: Upload project artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ${{ matrix.project_name }}.zip
|
||||
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
||||
@ -131,6 +131,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
security-events: write
|
||||
id-token: write
|
||||
needs:
|
||||
- build-artifacts
|
||||
strategy:
|
||||
@ -276,7 +277,8 @@ jobs:
|
||||
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||
id: build-docker
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||
with:
|
||||
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
@ -286,16 +288,33 @@ jobs:
|
||||
secrets: |
|
||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
env:
|
||||
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||
run: |
|
||||
IFS="," read -a tags <<< "${TAGS}"
|
||||
images=""
|
||||
for tag in "${tags[@]}"; do
|
||||
images+="${tag}@${DIGEST} "
|
||||
done
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
|
||||
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
|
||||
with:
|
||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||
fail-build: false
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
|
||||
@ -310,7 +329,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -374,7 +393,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-US.zip
|
||||
path: docker-stub-US.zip
|
||||
@ -384,7 +403,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-EU.zip
|
||||
path: docker-stub-EU.zip
|
||||
@ -394,7 +413,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-US-sha256.txt
|
||||
path: docker-stub-US-sha256.txt
|
||||
@ -404,7 +423,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-EU-sha256.txt
|
||||
path: docker-stub-EU-sha256.txt
|
||||
@ -428,7 +447,7 @@ jobs:
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Public API Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: swagger.json
|
||||
path: swagger.json
|
||||
@ -462,14 +481,14 @@ jobs:
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Internal API Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: internal.json
|
||||
path: internal.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Identity Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: identity.json
|
||||
path: identity.json
|
||||
@ -498,7 +517,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -514,7 +533,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact for Windows
|
||||
if: ${{ contains(matrix.target, 'win') == true }}
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
|
||||
@ -522,7 +541,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact
|
||||
if: ${{ contains(matrix.target, 'win') == false }}
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
||||
@ -530,7 +549,9 @@ jobs:
|
||||
|
||||
self-host-build:
|
||||
name: Trigger self-host build
|
||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-docker
|
||||
@ -633,6 +654,21 @@ jobs:
|
||||
}
|
||||
})
|
||||
|
||||
trigger-ephemeral-environment-sync:
|
||||
name: Trigger Ephemeral Environment Sync
|
||||
needs: trigger-ee-updates
|
||||
if: |
|
||||
github.event_name == 'pull_request_target'
|
||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
||||
with:
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||
project: server
|
||||
sync_environment: true
|
||||
pull_request_number: ${{ github.event.number }}
|
||||
secrets: inherit
|
||||
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Collect
|
||||
id: collect
|
||||
uses: launchdarkly/find-code-references-in-pull-request@d008aa4f321d8cd35314d9cb095388dcfde84439 # v2.0.0
|
||||
uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0
|
||||
with:
|
||||
project-key: default
|
||||
environment-key: dev
|
||||
|
@ -1,33 +1,14 @@
|
||||
name: Ephemeral environment cleanup
|
||||
name: Ephemeral Environment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [unlabeled]
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
name: Validate PR
|
||||
trigger-ee-updates:
|
||||
name: Trigger Ephemeral Environment updates
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate config exists in path
|
||||
id: validate-config
|
||||
run: |
|
||||
if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then
|
||||
echo "Ephemeral environment config found in path, continuing."
|
||||
echo "config-exists=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
cleanup-config:
|
||||
name: Cleanup ephemeral environment
|
||||
runs-on: ubuntu-24.04
|
||||
needs: validate-pr
|
||||
if: ${{ needs.validate-pr.outputs.config-exists }}
|
||||
if: github.event.label.name == 'ephemeral-environment'
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -41,7 +22,7 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Trigger Ephemeral Environment cleanup
|
||||
- name: Trigger Ephemeral Environment update
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
@ -49,11 +30,9 @@ jobs:
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
repo: 'devops',
|
||||
workflow_id: '_ephemeral_environment_pr_manager.yml',
|
||||
workflow_id: '_update_ephemeral_tags.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF,
|
||||
cleanup_config: true,
|
||||
project: 'server'
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||
}
|
||||
})
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||
with:
|
||||
artifacts: "docker-stub-US.zip,
|
||||
docker-stub-US-sha256.txt,
|
||||
|
8
.github/workflows/repository-management.yml
vendored
8
.github/workflows/repository-management.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
- setup
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -206,6 +206,7 @@ jobs:
|
||||
- name: Check out main branch
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@ -241,6 +242,7 @@ jobs:
|
||||
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
|
||||
git push -u origin $destination_branch
|
||||
fi
|
||||
}
|
||||
|
||||
# If we are cutting 'hotfix-rc':
|
||||
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
|
||||
|
11
.github/workflows/scan.yml
vendored
11
.github/workflows/scan.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "zulu"
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
run: dotnet tool install dotnet-sonarscanner -g
|
||||
@ -80,12 +80,11 @@ jobs:
|
||||
- name: Scan with SonarCloud
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \
|
||||
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
||||
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
||||
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
|
||||
/d:sonar.host.url="https://sonarcloud.io"
|
||||
/d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }}
|
||||
dotnet build
|
||||
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check
|
||||
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
with:
|
||||
stale-issue-label: "needs-reply"
|
||||
stale-pr-label: "needs-changes"
|
||||
|
42
.github/workflows/test-database.yml
vendored
42
.github/workflows/test-database.yml
vendored
@ -17,6 +17,7 @@ on:
|
||||
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
|
||||
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
|
||||
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
|
||||
- "src/**/Entities/**/*.cs" # Database entity definitions
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/test-database.yml" # This file
|
||||
@ -28,36 +29,18 @@ on:
|
||||
- "src/Infrastructure.Dapper/**" # Changes to SQL Server Dapper Repository Layer
|
||||
- "src/Infrastructure.EntityFramework/**" # Changes to Entity Framework Repository Layer
|
||||
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
|
||||
- "src/**/Entities/**/*.cs" # Database entity definitions
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@ -107,7 +90,7 @@ jobs:
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
|
||||
- name: Migrate MariaDB
|
||||
working-directory: "util/MySqlMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
@ -144,7 +127,7 @@ jobs:
|
||||
# Unified MariaDB
|
||||
BW_TEST_DATABASES__4__TYPE: "MySql"
|
||||
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
|
||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
shell: pwsh
|
||||
|
||||
- name: Print MySQL Logs
|
||||
@ -164,14 +147,17 @@ jobs:
|
||||
run: 'docker logs $(docker ps --quiet --filter "name=mssql")'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
reporter: dotnet-trx
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
|
||||
- name: Docker Compose down
|
||||
if: always()
|
||||
working-directory: "dev"
|
||||
@ -186,7 +172,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -200,7 +186,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload DACPAC
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: sql.dacpac
|
||||
path: Sql.dacpac
|
||||
@ -226,7 +212,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Report validation results
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: report.xml
|
||||
path: |
|
||||
@ -237,7 +223,7 @@ jobs:
|
||||
run: |
|
||||
if grep -q "<Operations>" "report.xml"; then
|
||||
echo
|
||||
echo "Migrations are out of sync with sqlproj!"
|
||||
echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project."
|
||||
exit 1
|
||||
else
|
||||
echo "Report looks good"
|
||||
|
30
.github/workflows/test.yml
vendored
30
.github/workflows/test.yml
vendored
@ -13,29 +13,10 @@ env:
|
||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||
|
||||
jobs:
|
||||
check-test-secrets:
|
||||
name: Check for test secrets
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
available: ${{ steps.check-test-secrets.outputs.available }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Check
|
||||
id: check-test-secrets
|
||||
run: |
|
||||
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
|
||||
echo "available=true" >> $GITHUB_OUTPUT;
|
||||
else
|
||||
echo "available=false" >> $GITHUB_OUTPUT;
|
||||
fi
|
||||
|
||||
testing:
|
||||
name: Run tests
|
||||
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-test-secrets
|
||||
permissions:
|
||||
checks: write
|
||||
contents: read
|
||||
@ -49,7 +30,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -68,8 +49,8 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
|
||||
uses: dorny/test-reporter@6e6a65b7a0bd2c9197df7d0ae36ac5cee784230c # v2.0.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
path: "**/*-test-results.trx"
|
||||
@ -77,7 +58,4 @@ jobs:
|
||||
fail-on-error: true
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
|
18
.vscode/extensions.json
vendored
Normal file
18
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nick-rudenko.back-n-forth",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"MS-vsliveshare.vsliveshare",
|
||||
|
||||
"mhutchie.git-graph",
|
||||
"donjayamanne.githistory",
|
||||
"eamodio.gitlens",
|
||||
|
||||
"jakebathman.mysql-syntax",
|
||||
"ckolkman.vscode-postgres",
|
||||
|
||||
"ms-dotnettools.csharp",
|
||||
"formulahendry.dotnet-test-explorer",
|
||||
"adrianwilczynski.user-secrets"
|
||||
]
|
||||
}
|
@ -3,11 +3,17 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.11.0</Version>
|
||||
<Version>2025.3.3</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<!-- Treat it as a test project if the project hasn't set their own value and it follows our test project conventions -->
|
||||
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
||||
<!-- Uncomment the below line when we are ready to enable nullable repo wide -->
|
||||
<!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> -->
|
||||
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
|
@ -18,7 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
.editorconfig = .editorconfig
|
||||
TRADEMARK_GUIDELINES.md = TRADEMARK_GUIDELINES.md
|
||||
SECURITY.md = SECURITY.md
|
||||
NuGet.Config = NuGet.Config
|
||||
LICENSE_FAQ.md = LICENSE_FAQ.md
|
||||
LICENSE_BITWARDEN.txt = LICENSE_BITWARDEN.txt
|
||||
LICENSE_AGPL.txt = LICENSE_AGPL.txt
|
||||
@ -126,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -314,6 +315,10 @@ Global
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -364,6 +369,7 @@ Global
|
||||
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -5,12 +5,12 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Providers;
|
||||
@ -27,6 +27,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public RemoveOrganizationFromProviderCommand(
|
||||
IEventService eventService,
|
||||
@ -38,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
IFeatureService featureService,
|
||||
IProviderBillingService providerBillingService,
|
||||
ISubscriberService subscriberService,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_mailService = mailService;
|
||||
@ -50,6 +52,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
_providerBillingService = providerBillingService;
|
||||
_subscriberService = subscriberService;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task RemoveOrganizationFromProvider(
|
||||
@ -110,7 +113,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
Email = organization.BillingEmail
|
||||
});
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType).PasswordManager;
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
@ -124,7 +127,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
},
|
||||
OffSession = true,
|
||||
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
|
||||
Items = [new SubscriptionItemOptions { Price = plan.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
|
||||
};
|
||||
|
||||
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -27,7 +28,11 @@ namespace Bit.Commercial.Core.AdminConsole.Services;
|
||||
|
||||
public class ProviderService : IProviderService
|
||||
{
|
||||
public static PlanType[] ProviderDisallowedOrganizationTypes = new[] { PlanType.Free, PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 };
|
||||
private static readonly PlanType[] _resellerDisallowedOrganizationTypes = [
|
||||
PlanType.Free,
|
||||
PlanType.FamiliesAnnually,
|
||||
PlanType.FamiliesAnnually2019
|
||||
];
|
||||
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly IMailService _mailService;
|
||||
@ -46,6 +51,7 @@ public class ProviderService : IProviderService
|
||||
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
@ -54,7 +60,7 @@ public class ProviderService : IProviderService
|
||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService)
|
||||
IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@ -73,6 +79,7 @@ public class ProviderService : IProviderService
|
||||
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null)
|
||||
@ -448,30 +455,31 @@ public class ProviderService : IProviderService
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId,
|
||||
GetStripeSeatPlanId(organization.PlanType));
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
var subscriptionItem = await GetSubscriptionItemAsync(
|
||||
organization.GatewaySubscriptionId,
|
||||
plan.PasswordManager.StripeSeatPlanId);
|
||||
|
||||
var extractedPlanType = PlanTypeMappings(organization);
|
||||
var extractedPlan = await _pricingClient.GetPlanOrThrow(extractedPlanType);
|
||||
|
||||
if (subscriptionItem != null)
|
||||
{
|
||||
await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization);
|
||||
await UpdateSubscriptionAsync(subscriptionItem, extractedPlan.PasswordManager.StripeSeatPlanId, organization);
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationRepository.UpsertAsync(organization);
|
||||
}
|
||||
|
||||
private async Task<Stripe.SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
|
||||
private async Task<SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
|
||||
{
|
||||
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
|
||||
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
|
||||
}
|
||||
|
||||
private static string GetStripeSeatPlanId(PlanType planType)
|
||||
{
|
||||
return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId;
|
||||
}
|
||||
|
||||
private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
|
||||
private async Task UpdateSubscriptionAsync(SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -690,13 +698,14 @@ public class ProviderService : IProviderService
|
||||
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (_resellerDisallowedOrganizationTypes.Contains(requestedType))
|
||||
{
|
||||
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException($"Unsupported provider type {providerType}.");
|
||||
}
|
||||
|
||||
if (ProviderDisallowedOrganizationTypes.Contains(requestedType))
|
||||
{
|
||||
throw new BadRequestException($"Providers cannot manage organizations with the requested plan type ({requestedType}). Only Teams and Enterprise accounts are allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
using System.Globalization;
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
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;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -24,16 +28,119 @@ using Stripe;
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
|
||||
public class ProviderBillingService(
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<ProviderBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
IPricingClient pricingClient,
|
||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IProviderBillingService
|
||||
ISubscriberService subscriberService,
|
||||
ITaxService taxService) : IProviderBillingService
|
||||
{
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task AddExistingOrganization(
|
||||
Provider provider,
|
||||
Organization organization,
|
||||
string key)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
CancelAtPeriodEnd = false
|
||||
});
|
||||
|
||||
var subscription =
|
||||
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionCancelOptions
|
||||
{
|
||||
CancellationDetails = new SubscriptionCancellationDetailsOptions
|
||||
{
|
||||
Comment = $"Organization was added to Provider with ID {provider.Id}"
|
||||
},
|
||||
InvoiceNow = true,
|
||||
Prorate = true,
|
||||
Expand = ["latest_invoice", "test_clock"]
|
||||
});
|
||||
|
||||
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||
|
||||
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
||||
|
||||
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
|
||||
{
|
||||
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||
}
|
||||
|
||||
var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(managedPlanType);
|
||||
organization.Plan = plan.Name;
|
||||
organization.PlanType = plan.Type;
|
||||
organization.MaxCollections = plan.PasswordManager.MaxCollections;
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
organization.UsePolicies = plan.HasPolicies;
|
||||
organization.UseSso = plan.HasSso;
|
||||
organization.UseGroups = plan.HasGroups;
|
||||
organization.UseEvents = plan.HasEvents;
|
||||
organization.UseDirectory = plan.HasDirectory;
|
||||
organization.UseTotp = plan.HasTotp;
|
||||
organization.Use2fa = plan.Has2fa;
|
||||
organization.UseApi = plan.HasApi;
|
||||
organization.UseResetPassword = plan.HasResetPassword;
|
||||
organization.SelfHost = plan.HasSelfHost;
|
||||
organization.UsersGetPremium = plan.UsersGetPremium;
|
||||
organization.UseCustomPermissions = plan.HasCustomPermissions;
|
||||
organization.UseScim = plan.HasScim;
|
||||
organization.UseKeyConnector = plan.HasKeyConnector;
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
organization.BillingEmail = provider.BillingEmail!;
|
||||
organization.GatewaySubscriptionId = null;
|
||||
organization.ExpirationDate = null;
|
||||
organization.MaxAutoscaleSeats = null;
|
||||
organization.Status = OrganizationStatusType.Managed;
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key
|
||||
};
|
||||
|
||||
/*
|
||||
* We have to scale the provider's seats before the ProviderOrganization
|
||||
* row is inserted so the added organization's seats don't get double counted.
|
||||
*/
|
||||
await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value);
|
||||
|
||||
await Task.WhenAll(
|
||||
organizationRepository.ReplaceAsync(organization),
|
||||
providerOrganizationRepository.CreateAsync(providerOrganization)
|
||||
);
|
||||
|
||||
var clientCustomer = await subscriberService.GetCustomer(organization);
|
||||
|
||||
if (clientCustomer.Balance != 0)
|
||||
{
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = clientCustomer.Balance,
|
||||
Currency = "USD",
|
||||
Description = $"Unused, prorated time for client organization with ID {organization.Id}."
|
||||
});
|
||||
}
|
||||
|
||||
await eventService.LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||
@ -48,7 +155,8 @@ public class ProviderBillingService(
|
||||
return;
|
||||
}
|
||||
|
||||
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||
var oldPlanConfiguration = await pricingClient.GetPlanOrThrow(plan.PlanType);
|
||||
var newPlanConfiguration = await pricingClient.GetPlanOrThrow(command.NewPlan);
|
||||
|
||||
plan.PlanType = command.NewPlan;
|
||||
await providerPlanRepository.ReplaceAsync(plan);
|
||||
@ -72,7 +180,7 @@ public class ProviderBillingService(
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Price = newPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = oldSubscriptionItem!.Quantity
|
||||
},
|
||||
new SubscriptionItemOptions
|
||||
@ -98,7 +206,7 @@ public class ProviderBillingService(
|
||||
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
|
||||
}
|
||||
organization.PlanType = command.NewPlan;
|
||||
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name;
|
||||
organization.Plan = newPlanConfiguration.Name;
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
}
|
||||
@ -205,6 +313,80 @@ public class ProviderBillingService(
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
|
||||
Provider provider,
|
||||
Guid userId)
|
||||
{
|
||||
var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId);
|
||||
|
||||
if (providerUser is not { Status: ProviderUserStatusType.Confirmed })
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type);
|
||||
|
||||
var active = (await Task.WhenAll(candidates.Select(async organization =>
|
||||
{
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
return (organization, subscription);
|
||||
})))
|
||||
.Where(pair => pair.subscription is
|
||||
{
|
||||
Status:
|
||||
StripeConstants.SubscriptionStatus.Active or
|
||||
StripeConstants.SubscriptionStatus.Trialing or
|
||||
StripeConstants.SubscriptionStatus.PastDue
|
||||
}).ToList();
|
||||
|
||||
if (active.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return await Task.WhenAll(active.Select(async pair =>
|
||||
{
|
||||
var (organization, _) = pair;
|
||||
|
||||
var planName = await DerivePlanName(provider, organization);
|
||||
|
||||
var addable = new AddableOrganization(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
planName,
|
||||
organization.Seats!.Value);
|
||||
|
||||
if (providerUser.Type != ProviderUserType.ServiceUser)
|
||||
{
|
||||
return addable;
|
||||
}
|
||||
|
||||
var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||
|
||||
var requiresPurchase =
|
||||
await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value);
|
||||
|
||||
return addable with { Disabled = requiresPurchase };
|
||||
}));
|
||||
|
||||
async Task<string> DerivePlanName(Provider localProvider, Organization localOrganization)
|
||||
{
|
||||
if (localProvider.Type == ProviderType.Msp)
|
||||
{
|
||||
return localOrganization.PlanType switch
|
||||
{
|
||||
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => "Enterprise",
|
||||
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => "Teams",
|
||||
_ => throw new BillingException()
|
||||
};
|
||||
}
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(localOrganization.PlanType);
|
||||
return plan.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ScaleSeats(
|
||||
Provider provider,
|
||||
PlanType planType,
|
||||
@ -293,20 +475,17 @@ public class ProviderBillingService(
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
ArgumentNullException.ThrowIfNull(taxInfo);
|
||||
|
||||
if (string.IsNullOrEmpty(taxInfo.BillingAddressCountry) ||
|
||||
string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
|
||||
if (taxInfo is not
|
||||
{
|
||||
BillingAddressCountry: not null and not "",
|
||||
BillingAddressPostalCode: not null and not ""
|
||||
})
|
||||
{
|
||||
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var providerDisplayName = provider.DisplayName();
|
||||
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
var options = new CustomerCreateOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
@ -326,26 +505,46 @@ public class ProviderBillingService(
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = provider.SubscriberType(),
|
||||
Value = providerDisplayName?.Length <= 30
|
||||
? providerDisplayName
|
||||
: providerDisplayName?[..30]
|
||||
Value = provider.DisplayName()?.Length <= 30
|
||||
? provider.DisplayName()
|
||||
: provider.DisplayName()?[..30]
|
||||
}
|
||||
]
|
||||
},
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", globalSettings.BaseServiceUri.CloudRegion }
|
||||
},
|
||||
TaxIdData = taxInfo.HasTaxId ?
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }
|
||||
]
|
||||
: null
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
|
||||
{
|
||||
var taxIdType = taxService.GetStripeTaxCode(
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
if (taxIdType == null)
|
||||
{
|
||||
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
|
||||
options.TaxIdData =
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
||||
];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(provider.DiscountId))
|
||||
{
|
||||
options.Coupon = provider.DiscountId;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
return await stripeAdapter.CustomerCreateAsync(options);
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
@ -373,7 +572,7 @@ public class ProviderBillingService(
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
|
||||
if (!providerPlan.IsConfigured())
|
||||
{
|
||||
@ -429,6 +628,19 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
Provider provider,
|
||||
TokenizedPaymentSource tokenizedPaymentSource,
|
||||
TaxInformation taxInformation)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
|
||||
subscriberService.UpdateTaxInformation(provider, taxInformation));
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
{
|
||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||
@ -457,8 +669,10 @@ public class ProviderBillingService(
|
||||
|
||||
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||
{
|
||||
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
var newPlan = await pricingClient.GetPlanOrThrow(newPlanConfiguration.Plan);
|
||||
|
||||
var priceId = newPlan.PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||
|
||||
if (providerPlan.PurchasedSeats == 0)
|
||||
@ -522,7 +736,7 @@ public class ProviderBillingService(
|
||||
ProviderPlan providerPlan,
|
||||
int newlyAssignedSeats) => async (currentlySubscribedSeats, newlySubscribedSeats) =>
|
||||
{
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
|
||||
await paymentService.AdjustSeats(
|
||||
provider,
|
||||
@ -546,7 +760,7 @@ public class ProviderBillingService(
|
||||
var providerOrganizations =
|
||||
await providerOrganizationRepository.GetManyDetailsByProviderAsync(provider.Id);
|
||||
|
||||
var plan = StaticStore.GetPlan(planType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(planType);
|
||||
|
||||
return providerOrganizations
|
||||
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
|
||||
@ -567,4 +781,21 @@ public class ProviderBillingService(
|
||||
|
||||
return providerPlan;
|
||||
}
|
||||
|
||||
private async Task<PlanType> GetManagedPlanTypeAsync(
|
||||
Provider provider,
|
||||
Organization organization)
|
||||
{
|
||||
if (provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType;
|
||||
}
|
||||
|
||||
return organization.PlanType switch
|
||||
{
|
||||
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly,
|
||||
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly,
|
||||
_ => throw new BillingException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -28,6 +28,7 @@ public class MaxProjectsQuery : IMaxProjectsQuery
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122
|
||||
var plan = StaticStore.GetPlan(org.PlanType);
|
||||
if (plan?.SecretsManager == null)
|
||||
{
|
||||
|
@ -1,8 +1,10 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
@ -22,9 +24,10 @@ public class GroupsController : Controller
|
||||
private readonly IGetGroupsListQuery _getGroupsListQuery;
|
||||
private readonly IDeleteGroupCommand _deleteGroupCommand;
|
||||
private readonly IPatchGroupCommand _patchGroupCommand;
|
||||
private readonly IPatchGroupCommandvNext _patchGroupCommandvNext;
|
||||
private readonly IPostGroupCommand _postGroupCommand;
|
||||
private readonly IPutGroupCommand _putGroupCommand;
|
||||
private readonly ILogger<GroupsController> _logger;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public GroupsController(
|
||||
IGroupRepository groupRepository,
|
||||
@ -32,18 +35,21 @@ public class GroupsController : Controller
|
||||
IGetGroupsListQuery getGroupsListQuery,
|
||||
IDeleteGroupCommand deleteGroupCommand,
|
||||
IPatchGroupCommand patchGroupCommand,
|
||||
IPatchGroupCommandvNext patchGroupCommandvNext,
|
||||
IPostGroupCommand postGroupCommand,
|
||||
IPutGroupCommand putGroupCommand,
|
||||
ILogger<GroupsController> logger)
|
||||
IFeatureService featureService
|
||||
)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_getGroupsListQuery = getGroupsListQuery;
|
||||
_deleteGroupCommand = deleteGroupCommand;
|
||||
_patchGroupCommand = patchGroupCommand;
|
||||
_patchGroupCommandvNext = patchGroupCommandvNext;
|
||||
_postGroupCommand = postGroupCommand;
|
||||
_putGroupCommand = putGroupCommand;
|
||||
_logger = logger;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -97,8 +103,21 @@ public class GroupsController : Controller
|
||||
[HttpPatch("{id}")]
|
||||
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests))
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(id);
|
||||
if (group == null || group.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("Group not found.");
|
||||
}
|
||||
|
||||
await _patchGroupCommandvNext.PatchGroupAsync(group, model);
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
await _patchGroupCommand.PatchGroupAsync(organization, id, model);
|
||||
|
||||
return new NoContentResult();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Groups.Interfaces;
|
||||
|
||||
public interface IPatchGroupCommandvNext
|
||||
{
|
||||
Task PatchGroupAsync(Group group, ScimPatchModel model);
|
||||
}
|
170
bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs
Normal file
170
bitwarden_license/src/Scim/Groups/PatchGroupCommandvNext.cs
Normal file
@ -0,0 +1,170 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
|
||||
namespace Bit.Scim.Groups;
|
||||
|
||||
public class PatchGroupCommandvNext : IPatchGroupCommandvNext
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IGroupService _groupService;
|
||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||
private readonly ILogger<PatchGroupCommandvNext> _logger;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
|
||||
public PatchGroupCommandvNext(
|
||||
IGroupRepository groupRepository,
|
||||
IGroupService groupService,
|
||||
IUpdateGroupCommand updateGroupCommand,
|
||||
ILogger<PatchGroupCommandvNext> logger,
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_groupService = groupService;
|
||||
_updateGroupCommand = updateGroupCommand;
|
||||
_logger = logger;
|
||||
_organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
|
||||
{
|
||||
foreach (var operation in model.Operations)
|
||||
{
|
||||
await HandleOperationAsync(group, operation);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
|
||||
{
|
||||
switch (operation.Op?.ToLowerInvariant())
|
||||
{
|
||||
// Replace a list of members
|
||||
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||
{
|
||||
var ids = GetOperationValueIds(operation.Value);
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||
break;
|
||||
}
|
||||
|
||||
// Replace group name from path
|
||||
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
|
||||
{
|
||||
group.Name = operation.Value.GetString();
|
||||
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||
break;
|
||||
}
|
||||
|
||||
// Replace group name from value object
|
||||
case PatchOps.Replace when
|
||||
string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
|
||||
{
|
||||
group.Name = displayNameProperty.GetString();
|
||||
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add a single member
|
||||
case PatchOps.Add when
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
||||
TryGetOperationPathId(operation.Path, out var addId):
|
||||
{
|
||||
await AddMembersAsync(group, [addId]);
|
||||
break;
|
||||
}
|
||||
|
||||
// Add a list of members
|
||||
case PatchOps.Add when
|
||||
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||
{
|
||||
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove a single member
|
||||
case PatchOps.Remove when
|
||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
||||
TryGetOperationPathId(operation.Path, out var removeId):
|
||||
{
|
||||
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove a list of members
|
||||
case PatchOps.Remove when
|
||||
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||
{
|
||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||
foreach (var v in GetOperationValueIds(operation.Value))
|
||||
{
|
||||
orgUserIds.Remove(v);
|
||||
}
|
||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
_logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
|
||||
{
|
||||
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
|
||||
// is removed. To avoid excessive load on the database, we check against the high availability replica and
|
||||
// return early if they already exist.
|
||||
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
|
||||
if (usersToAdd.IsSubsetOf(groupMembers))
|
||||
{
|
||||
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
|
||||
}
|
||||
|
||||
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
|
||||
{
|
||||
var ids = new HashSet<Guid>();
|
||||
foreach (var obj in objArray.EnumerateArray())
|
||||
{
|
||||
if (obj.TryGetProperty("value", out var valueProperty))
|
||||
{
|
||||
if (valueProperty.TryGetGuid(out var guid))
|
||||
{
|
||||
ids.Add(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static bool TryGetOperationPathId(string path, out Guid pathId)
|
||||
{
|
||||
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||
return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
@ -14,17 +11,13 @@ namespace Bit.Scim.Groups;
|
||||
public class PostGroupCommand : IPostGroupCommand
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly ICreateGroupCommand _createGroupCommand;
|
||||
|
||||
public PostGroupCommand(
|
||||
IGroupRepository groupRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IScimContext scimContext,
|
||||
ICreateGroupCommand createGroupCommand)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_scimContext = scimContext;
|
||||
_createGroupCommand = createGroupCommand;
|
||||
}
|
||||
|
||||
@ -50,11 +43,6 @@ public class PostGroupCommand : IPostGroupCommand
|
||||
|
||||
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||
{
|
||||
if (_scimContext.RequestScimProvider != ScimProviderType.Okta)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.Members == null)
|
||||
{
|
||||
return;
|
||||
|
@ -1,10 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.Models;
|
||||
|
||||
@ -13,16 +11,13 @@ namespace Bit.Scim.Groups;
|
||||
public class PutGroupCommand : IPutGroupCommand
|
||||
{
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||
|
||||
public PutGroupCommand(
|
||||
IGroupRepository groupRepository,
|
||||
IScimContext scimContext,
|
||||
IUpdateGroupCommand updateGroupCommand)
|
||||
{
|
||||
_groupRepository = groupRepository;
|
||||
_scimContext = scimContext;
|
||||
_updateGroupCommand = updateGroupCommand;
|
||||
}
|
||||
|
||||
@ -43,12 +38,6 @@ public class PutGroupCommand : IPutGroupCommand
|
||||
|
||||
private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model)
|
||||
{
|
||||
if (_scimContext.RequestScimProvider != ScimProviderType.Okta &&
|
||||
_scimContext.RequestScimProvider != ScimProviderType.Ping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.Members == null)
|
||||
{
|
||||
return;
|
||||
|
@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Stripe;
|
||||
|
||||
|
@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using IdentityModel;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -7,3 +7,16 @@ public static class ScimConstants
|
||||
public const string Scim2SchemaUser = "urn:ietf:params:scim:schemas:core:2.0:User";
|
||||
public const string Scim2SchemaGroup = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
||||
}
|
||||
|
||||
public static class PatchOps
|
||||
{
|
||||
public const string Replace = "replace";
|
||||
public const string Add = "add";
|
||||
public const string Remove = "remove";
|
||||
}
|
||||
|
||||
public static class PatchPaths
|
||||
{
|
||||
public const string Members = "members";
|
||||
public const string DisplayName = "displayname";
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ public static class ScimServiceCollectionExtensions
|
||||
public static void AddScimGroupCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
|
||||
services.AddScoped<IPatchGroupCommandvNext, PatchGroupCommandvNext>();
|
||||
services.AddScoped<IPostGroupCommand, PostGroupCommand>();
|
||||
services.AddScoped<IPutGroupCommand, PutGroupCommand>();
|
||||
}
|
||||
|
@ -19,10 +19,10 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -7,9 +7,9 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Infrastructure;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
647
bitwarden_license/src/Sso/package-lock.json
generated
647
bitwarden_license/src/Sso/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,10 +15,10 @@
|
||||
"devDependencies": {
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.0",
|
||||
"mini-css-extract-plugin": "2.9.1",
|
||||
"sass": "1.79.5",
|
||||
"sass-loader": "16.0.2",
|
||||
"webpack": "5.95.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.85.0",
|
||||
"sass-loader": "16.0.4",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -205,6 +206,8 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
|
||||
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan);
|
||||
|
||||
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
|
||||
providerOrganization.OrganizationId,
|
||||
[],
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -550,8 +551,14 @@ public class ProviderServiceTests
|
||||
organization.PlanType = PlanType.EnterpriseMonthly;
|
||||
organization.Plan = "Enterprise (Monthly)";
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var expectedPlanType = PlanType.EnterpriseMonthly2020;
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(expectedPlanType)
|
||||
.Returns(StaticStore.GetPlan(expectedPlanType));
|
||||
|
||||
var expectedPlanId = "2020-enterprise-org-seat-monthly";
|
||||
|
||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -128,6 +129,9 @@ public class ProviderBillingServiceTests
|
||||
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
|
||||
.Returns(existingPlan);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(existingPlan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(existingPlan.PlanType));
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
@ -156,6 +160,9 @@ public class ProviderBillingServiceTests
|
||||
var command =
|
||||
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(command.NewPlan)
|
||||
.Returns(StaticStore.GetPlan(command.NewPlan));
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ChangePlan(command);
|
||||
|
||||
@ -390,6 +397,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
// 50 seats currently assigned with a seat minimum of 100
|
||||
@ -451,6 +464,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -515,6 +534,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -579,6 +604,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
var providerPlan = providerPlans.First();
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
@ -636,6 +667,8 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
new ProviderOrganizationOrganizationDetails
|
||||
@ -672,6 +705,8 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(planType).Returns(StaticStore.GetPlan(planType));
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetManyDetailsByProviderAsync(provider.Id).Returns(
|
||||
[
|
||||
new ProviderOrganizationOrganizationDetails
|
||||
@ -696,18 +731,6 @@ public class ProviderBillingServiceTests
|
||||
|
||||
#region SetupCustomer
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_NullProvider_ThrowsArgumentNullException(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
TaxInfo taxInfo) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SetupCustomer(null, taxInfo));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_NullTaxInfo_ThrowsArgumentNullException(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SetupCustomer(provider, null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_MissingCountry_ContactSupport(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
@ -746,6 +769,12 @@ public class ProviderBillingServiceTests
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns(taxInfo.TaxIdType);
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
@ -777,6 +806,29 @@ public class ProviderBillingServiceTests
|
||||
Assert.Equivalent(expected, actual);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
|
||||
SutProvider<ProviderBillingService> sutProvider,
|
||||
Provider provider,
|
||||
TaxInfo taxInfo)
|
||||
{
|
||||
provider.Name = "MSP";
|
||||
|
||||
taxInfo.BillingAddressCountry = "AD";
|
||||
|
||||
sutProvider.GetDependency<ITaxService>()
|
||||
.GetStripeTaxCode(Arg.Is<string>(
|
||||
p => p == taxInfo.BillingAddressCountry),
|
||||
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
|
||||
.Returns((string)null);
|
||||
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
|
||||
await sutProvider.Sut.SetupCustomer(provider, taxInfo));
|
||||
|
||||
Assert.IsType<BadRequestException>(actual);
|
||||
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetupSubscription
|
||||
@ -827,6 +879,9 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
.Returns(providerPlans);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.EnterpriseMonthly)
|
||||
.Returns(StaticStore.GetPlan(PlanType.EnterpriseMonthly));
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
@ -852,6 +907,9 @@ public class ProviderBillingServiceTests
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
.Returns(providerPlans);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(PlanType.TeamsMonthly)
|
||||
.Returns(StaticStore.GetPlan(PlanType.TeamsMonthly));
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupSubscription(provider));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>()
|
||||
@ -894,6 +952,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
.Returns(providerPlans);
|
||||
|
||||
@ -939,6 +1003,12 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IProviderPlanRepository>().GetByProviderId(provider.Id)
|
||||
.Returns(providerPlans);
|
||||
|
||||
@ -1037,6 +1107,12 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
@ -1110,6 +1186,12 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 15 }
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
@ -1183,6 +1265,12 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
@ -1250,6 +1338,12 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 50, PurchasedSeats = 20 }
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
@ -1323,6 +1417,12 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0 }
|
||||
};
|
||||
|
||||
foreach (var plan in providerPlans)
|
||||
{
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(plan.PlanType)
|
||||
.Returns(StaticStore.GetPlan(plan.PlanType));
|
||||
}
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
|
@ -0,0 +1,151 @@
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Commercial.Core.Test.Billing;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class TaxServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData("AD", "A-123456-Z", "ad_nrt")]
|
||||
[BitAutoData("AD", "A123456Z", "ad_nrt")]
|
||||
[BitAutoData("AR", "20-12345678-9", "ar_cuit")]
|
||||
[BitAutoData("AR", "20123456789", "ar_cuit")]
|
||||
[BitAutoData("AU", "01259983598", "au_abn")]
|
||||
[BitAutoData("AU", "123456789123", "au_arn")]
|
||||
[BitAutoData("AT", "ATU12345678", "eu_vat")]
|
||||
[BitAutoData("BH", "123456789012345", "bh_vat")]
|
||||
[BitAutoData("BY", "123456789", "by_tin")]
|
||||
[BitAutoData("BE", "BE0123456789", "eu_vat")]
|
||||
[BitAutoData("BO", "123456789", "bo_tin")]
|
||||
[BitAutoData("BR", "01.234.456/5432-10", "br_cnpj")]
|
||||
[BitAutoData("BR", "01234456543210", "br_cnpj")]
|
||||
[BitAutoData("BR", "123.456.789-87", "br_cpf")]
|
||||
[BitAutoData("BR", "12345678987", "br_cpf")]
|
||||
[BitAutoData("BG", "123456789", "bg_uic")]
|
||||
[BitAutoData("BG", "BG012100705", "eu_vat")]
|
||||
[BitAutoData("CA", "100728494", "ca_bn")]
|
||||
[BitAutoData("CA", "123456789RT0001", "ca_gst_hst")]
|
||||
[BitAutoData("CA", "PST-1234-1234", "ca_pst_bc")]
|
||||
[BitAutoData("CA", "123456-7", "ca_pst_mb")]
|
||||
[BitAutoData("CA", "1234567", "ca_pst_sk")]
|
||||
[BitAutoData("CA", "1234567890TQ1234", "ca_qst")]
|
||||
[BitAutoData("CL", "11.121.326-1", "cl_tin")]
|
||||
[BitAutoData("CL", "11121326-1", "cl_tin")]
|
||||
[BitAutoData("CL", "23.121.326-K", "cl_tin")]
|
||||
[BitAutoData("CL", "43651326-K", "cl_tin")]
|
||||
[BitAutoData("CN", "123456789012345678", "cn_tin")]
|
||||
[BitAutoData("CN", "123456789012345", "cn_tin")]
|
||||
[BitAutoData("CO", "123.456.789-0", "co_nit")]
|
||||
[BitAutoData("CO", "1234567890", "co_nit")]
|
||||
[BitAutoData("CR", "1-234-567890", "cr_tin")]
|
||||
[BitAutoData("CR", "1234567890", "cr_tin")]
|
||||
[BitAutoData("HR", "HR12345678912", "eu_vat")]
|
||||
[BitAutoData("HR", "12345678901", "hr_oib")]
|
||||
[BitAutoData("CY", "CY12345678X", "eu_vat")]
|
||||
[BitAutoData("CZ", "CZ12345678", "eu_vat")]
|
||||
[BitAutoData("DK", "DK12345678", "eu_vat")]
|
||||
[BitAutoData("DO", "123-4567890-1", "do_rcn")]
|
||||
[BitAutoData("DO", "12345678901", "do_rcn")]
|
||||
[BitAutoData("EC", "1234567890001", "ec_ruc")]
|
||||
[BitAutoData("EG", "123456789", "eg_tin")]
|
||||
[BitAutoData("SV", "1234-567890-123-4", "sv_nit")]
|
||||
[BitAutoData("SV", "12345678901234", "sv_nit")]
|
||||
[BitAutoData("EE", "EE123456789", "eu_vat")]
|
||||
[BitAutoData("EU", "EU123456789", "eu_oss_vat")]
|
||||
[BitAutoData("FI", "FI12345678", "eu_vat")]
|
||||
[BitAutoData("FR", "FR12345678901", "eu_vat")]
|
||||
[BitAutoData("GE", "123456789", "ge_vat")]
|
||||
[BitAutoData("DE", "1234567890", "de_stn")]
|
||||
[BitAutoData("DE", "DE123456789", "eu_vat")]
|
||||
[BitAutoData("GR", "EL123456789", "eu_vat")]
|
||||
[BitAutoData("HK", "12345678", "hk_br")]
|
||||
[BitAutoData("HU", "HU12345678", "eu_vat")]
|
||||
[BitAutoData("HU", "12345678-1-23", "hu_tin")]
|
||||
[BitAutoData("HU", "12345678123", "hu_tin")]
|
||||
[BitAutoData("IS", "123456", "is_vat")]
|
||||
[BitAutoData("IN", "12ABCDE1234F1Z5", "in_gst")]
|
||||
[BitAutoData("IN", "12ABCDE3456FGZH", "in_gst")]
|
||||
[BitAutoData("ID", "012.345.678.9-012.345", "id_npwp")]
|
||||
[BitAutoData("ID", "0123456789012345", "id_npwp")]
|
||||
[BitAutoData("IE", "IE1234567A", "eu_vat")]
|
||||
[BitAutoData("IE", "IE1234567AB", "eu_vat")]
|
||||
[BitAutoData("IL", "000012345", "il_vat")]
|
||||
[BitAutoData("IL", "123456789", "il_vat")]
|
||||
[BitAutoData("IT", "IT12345678901", "eu_vat")]
|
||||
[BitAutoData("JP", "1234567890123", "jp_cn")]
|
||||
[BitAutoData("JP", "12345", "jp_rn")]
|
||||
[BitAutoData("KZ", "123456789012", "kz_bin")]
|
||||
[BitAutoData("KE", "P000111111A", "ke_pin")]
|
||||
[BitAutoData("LV", "LV12345678912", "eu_vat")]
|
||||
[BitAutoData("LI", "CHE123456789", "li_uid")]
|
||||
[BitAutoData("LI", "12345", "li_vat")]
|
||||
[BitAutoData("LT", "LT123456789123", "eu_vat")]
|
||||
[BitAutoData("LU", "LU12345678", "eu_vat")]
|
||||
[BitAutoData("MY", "12345678", "my_frp")]
|
||||
[BitAutoData("MY", "C 1234567890", "my_itn")]
|
||||
[BitAutoData("MY", "C1234567890", "my_itn")]
|
||||
[BitAutoData("MY", "A12-3456-78912345", "my_sst")]
|
||||
[BitAutoData("MY", "A12345678912345", "my_sst")]
|
||||
[BitAutoData("MT", "MT12345678", "eu_vat")]
|
||||
[BitAutoData("MX", "ABC010203AB9", "mx_rfc")]
|
||||
[BitAutoData("MD", "1003600", "md_vat")]
|
||||
[BitAutoData("MA", "12345678", "ma_vat")]
|
||||
[BitAutoData("NL", "NL123456789B12", "eu_vat")]
|
||||
[BitAutoData("NZ", "123456789", "nz_gst")]
|
||||
[BitAutoData("NG", "12345678-0001", "ng_tin")]
|
||||
[BitAutoData("NO", "123456789MVA", "no_vat")]
|
||||
[BitAutoData("NO", "1234567", "no_voec")]
|
||||
[BitAutoData("OM", "OM1234567890", "om_vat")]
|
||||
[BitAutoData("PE", "12345678901", "pe_ruc")]
|
||||
[BitAutoData("PH", "123456789012", "ph_tin")]
|
||||
[BitAutoData("PL", "PL1234567890", "eu_vat")]
|
||||
[BitAutoData("PT", "PT123456789", "eu_vat")]
|
||||
[BitAutoData("RO", "RO1234567891", "eu_vat")]
|
||||
[BitAutoData("RO", "1234567890123", "ro_tin")]
|
||||
[BitAutoData("RU", "1234567891", "ru_inn")]
|
||||
[BitAutoData("RU", "123456789", "ru_kpp")]
|
||||
[BitAutoData("SA", "123456789012345", "sa_vat")]
|
||||
[BitAutoData("RS", "123456789", "rs_pib")]
|
||||
[BitAutoData("SG", "M12345678X", "sg_gst")]
|
||||
[BitAutoData("SG", "123456789F", "sg_uen")]
|
||||
[BitAutoData("SK", "SK1234567891", "eu_vat")]
|
||||
[BitAutoData("SI", "SI12345678", "eu_vat")]
|
||||
[BitAutoData("SI", "12345678", "si_tin")]
|
||||
[BitAutoData("ZA", "4123456789", "za_vat")]
|
||||
[BitAutoData("KR", "123-45-67890", "kr_brn")]
|
||||
[BitAutoData("KR", "1234567890", "kr_brn")]
|
||||
[BitAutoData("ES", "A12345678", "es_cif")]
|
||||
[BitAutoData("ES", "ESX1234567X", "eu_vat")]
|
||||
[BitAutoData("SE", "SE123456789012", "eu_vat")]
|
||||
[BitAutoData("CH", "CHE-123.456.789 HR", "ch_uid")]
|
||||
[BitAutoData("CH", "CHE123456789HR", "ch_uid")]
|
||||
[BitAutoData("CH", "CHE-123.456.789 MWST", "ch_vat")]
|
||||
[BitAutoData("CH", "CHE123456789MWST", "ch_vat")]
|
||||
[BitAutoData("TW", "12345678", "tw_vat")]
|
||||
[BitAutoData("TH", "1234567890123", "th_vat")]
|
||||
[BitAutoData("TR", "0123456789", "tr_tin")]
|
||||
[BitAutoData("UA", "123456789", "ua_vat")]
|
||||
[BitAutoData("AE", "123456789012345", "ae_trn")]
|
||||
[BitAutoData("GB", "XI123456789", "eu_vat")]
|
||||
[BitAutoData("GB", "GB123456789", "gb_vat")]
|
||||
[BitAutoData("US", "12-3456789", "us_ein")]
|
||||
[BitAutoData("UY", "123456789012", "uy_ruc")]
|
||||
[BitAutoData("UZ", "123456789", "uz_tin")]
|
||||
[BitAutoData("UZ", "123456789012", "uz_vat")]
|
||||
[BitAutoData("VE", "A-12345678-9", "ve_rif")]
|
||||
[BitAutoData("VE", "A123456789", "ve_rif")]
|
||||
[BitAutoData("VN", "1234567890", "vn_tin")]
|
||||
public void GetStripeTaxCode_WithValidCountryAndTaxId_ReturnsExpectedTaxIdType(
|
||||
string country,
|
||||
string taxId,
|
||||
string expected,
|
||||
SutProvider<TaxService> sutProvider)
|
||||
{
|
||||
var result = sutProvider.Sut.GetStripeTaxCode(country, taxId);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Scim.IntegrationTest.Factories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.IntegrationTest.Controllers.v2;
|
||||
|
||||
public class GroupsControllerPatchTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly ScimApplicationFactory _factory;
|
||||
|
||||
public GroupsControllerPatchTests(ScimApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
_factory.ReinitializeDbForTests(databaseContext);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Single(databaseContext.GroupUsers);
|
||||
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
|
||||
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddSingleMember_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId2;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
},
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
|
||||
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Empty(databaseContext.GroupUsers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_NotFound()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = Guid.NewGuid();
|
||||
var inputModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
var expectedResponse = new ScimErrorResponseModel
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Detail = "Group not found.",
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Scim.Groups.Interfaces;
|
||||
using Bit.Scim.IntegrationTest.Factories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.IntegrationTest.Controllers.v2;
|
||||
|
||||
public class GroupsControllerPatchTestsvNext : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly ScimApplicationFactory _factory;
|
||||
|
||||
public GroupsControllerPatchTestsvNext(ScimApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
|
||||
// Enable the feature flag for new PatchGroupsCommand and stub out the old command to be safe
|
||||
_factory.SubstituteService((IFeatureService featureService)
|
||||
=> featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests).Returns(true));
|
||||
_factory.SubstituteService((IPatchGroupCommand patchGroupCommand)
|
||||
=> patchGroupCommand.PatchGroupAsync(Arg.Any<Organization>(), Arg.Any<Guid>(), Arg.Any<ScimPatchModel>())
|
||||
.ThrowsAsync(new Exception("This test suite should be testing the vNext command, but the existing command was called.")));
|
||||
}
|
||||
|
||||
public Task InitializeAsync()
|
||||
{
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
_factory.ReinitializeDbForTests(databaseContext);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Single(databaseContext.GroupUsers);
|
||||
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
|
||||
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddSingleMember_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId2;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
},
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
|
||||
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Empty(databaseContext.GroupUsers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_NotFound()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = Guid.NewGuid();
|
||||
var inputModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
var expectedResponse = new ScimErrorResponseModel
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Detail = "Group not found.",
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
}
|
@ -9,9 +9,6 @@ namespace Bit.Scim.IntegrationTest.Controllers.v2;
|
||||
|
||||
public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private const int _initialGroupCount = 3;
|
||||
private const int _initialGroupUsersCount = 2;
|
||||
|
||||
private readonly ScimApplicationFactory _factory;
|
||||
|
||||
public GroupsControllerTests(ScimApplicationFactory factory)
|
||||
@ -237,10 +234,10 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel, "Id");
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(_initialGroupCount + 1, databaseContext.Groups.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount + 1, databaseContext.Groups.Count());
|
||||
Assert.True(databaseContext.Groups.Any(g => g.Name == displayName && g.ExternalId == externalId));
|
||||
|
||||
Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == responseModel.Id && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
}
|
||||
|
||||
@ -248,7 +245,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string displayName)
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string? displayName)
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var model = new ScimGroupRequestModel
|
||||
@ -281,7 +278,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
Assert.Equal(StatusCodes.Status409Conflict, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(_initialGroupCount, databaseContext.Groups.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
|
||||
Assert.False(databaseContext.Groups.Any(g => g.Name == "New Group"));
|
||||
}
|
||||
|
||||
@ -354,216 +351,6 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
|
||||
Assert.Equal(_initialGroupUsersCount, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_ReplaceMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Single(databaseContext.GroupUsers);
|
||||
|
||||
Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
|
||||
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddSingleMember_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(_initialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_AddListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId2;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "add",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var newDisplayName = "Patch Display Name";
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
|
||||
Value = JsonDocument.Parse("{}").RootElement
|
||||
},
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(_initialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
||||
Assert.Equal(_initialGroupCount, databaseContext.Groups.Count());
|
||||
|
||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
||||
Assert.Equal(newDisplayName, group.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_RemoveListMembers_Success()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
||||
var inputModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>()
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Empty(databaseContext.GroupUsers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Patch_NotFound()
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var groupId = Guid.NewGuid();
|
||||
var inputModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
||||
};
|
||||
var expectedResponse = new ScimErrorResponseModel
|
||||
{
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Detail = "Group not found.",
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
|
||||
};
|
||||
|
||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
||||
|
||||
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_Success()
|
||||
{
|
||||
@ -575,7 +362,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
||||
|
||||
var databaseContext = _factory.GetDatabaseContext();
|
||||
Assert.Equal(_initialGroupCount - 1, databaseContext.Groups.Count());
|
||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount - 1, databaseContext.Groups.Count());
|
||||
Assert.True(databaseContext.Groups.FirstOrDefault(g => g.Id == groupId) == null);
|
||||
}
|
||||
|
||||
|
@ -324,7 +324,7 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidEmail_BadRequest(string email)
|
||||
public async Task Post_InvalidEmail_BadRequest(string? email)
|
||||
{
|
||||
var displayName = "Test User 5";
|
||||
var externalId = "UE";
|
||||
|
@ -9,8 +9,6 @@ using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Scim.Models;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
@ -18,7 +16,8 @@ namespace Bit.Scim.IntegrationTest.Factories;
|
||||
|
||||
public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{
|
||||
public readonly new TestServer Server;
|
||||
public const int InitialGroupCount = 3;
|
||||
public const int InitialGroupUsersCount = 2;
|
||||
|
||||
public static readonly Guid TestUserId1 = Guid.Parse("2e8173db-8e8d-4de1-ac38-91b15c6d8dcb");
|
||||
public static readonly Guid TestUserId2 = Guid.Parse("b57846fc-0e94-4c93-9de5-9d0389eeadfb");
|
||||
@ -33,32 +32,29 @@ public class ScimApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
public static readonly Guid TestOrganizationUserId3 = Guid.Parse("be2f9045-e2b6-4173-ad44-4c69c3ea8140");
|
||||
public static readonly Guid TestOrganizationUserId4 = Guid.Parse("1f5689b7-e96e-4840-b0b1-eb3d5b5fd514");
|
||||
|
||||
public ScimApplicationFactory()
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
WebApplicationFactory<Startup> webApplicationFactory = WithWebHostBuilder(builder =>
|
||||
base.ConfigureWebHost(builder);
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
services
|
||||
.AddAuthentication("Test")
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
|
||||
|
||||
// Override to bypass SCIM authorization
|
||||
services.AddAuthorization(config =>
|
||||
{
|
||||
services
|
||||
.AddAuthentication("Test")
|
||||
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", options => { });
|
||||
|
||||
// Override to bypass SCIM authorization
|
||||
services.AddAuthorization(config =>
|
||||
config.AddPolicy("Scim", policy =>
|
||||
{
|
||||
config.AddPolicy("Scim", policy =>
|
||||
{
|
||||
policy.RequireAssertion(a => true);
|
||||
});
|
||||
policy.RequireAssertion(a => true);
|
||||
});
|
||||
|
||||
var mailService = services.First(sd => sd.ServiceType == typeof(IMailService));
|
||||
services.Remove(mailService);
|
||||
services.AddSingleton<IMailService, NoopMailService>();
|
||||
});
|
||||
});
|
||||
|
||||
Server = webApplicationFactory.Server;
|
||||
var mailService = services.First(sd => sd.ServiceType == typeof(IMailService));
|
||||
services.Remove(mailService);
|
||||
services.AddSingleton<IMailService, NoopMailService>();
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<HttpContext> GroupsGetAsync(Guid organizationId, Guid id)
|
||||
|
@ -0,0 +1,381 @@
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Groups;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PatchGroupCommandvNextTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
|
||||
Organization organization, Group group, IEnumerable<Guid> userIds)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||
arg.Count() == userIds.Count() &&
|
||||
arg.ToHashSet().SetEquals(userIds)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
|
||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "replace",
|
||||
Path = "displayname",
|
||||
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||
Assert.Equal(displayName, group.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "replace",
|
||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||
Assert.Equal(displayName, group.Name);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{userId}\"]",
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
|
||||
SutProvider<PatchGroupCommandvNext> sutProvider,
|
||||
Organization organization,
|
||||
Group group,
|
||||
ICollection<Guid> existingMembers)
|
||||
{
|
||||
// User being added is already in group
|
||||
var userId = existingMembers.First();
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members[value eq \"{userId}\"]",
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.AddGroupUsersByIdAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||
arg.Count() == userIds.Count &&
|
||||
arg.ToHashSet().SetEquals(userIds)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
|
||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group,
|
||||
ICollection<Guid> existingMembers)
|
||||
{
|
||||
// Create 3 userIds
|
||||
var fixture = new Fixture { RepeatCount = 3 };
|
||||
var userIds = fixture.CreateMany<Guid>().ToList();
|
||||
|
||||
// Copy the list and add a duplicate
|
||||
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
|
||||
Assert.Equal(4, userIdsWithDuplicate.Count);
|
||||
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer
|
||||
.Serialize(userIdsWithDuplicate
|
||||
.Select(uid => new { value = uid })
|
||||
.ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||
arg.Count() == 3 &&
|
||||
arg.ToHashSet().SetEquals(userIds)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
|
||||
SutProvider<PatchGroupCommandvNext> sutProvider,
|
||||
Organization organization, Group group,
|
||||
ICollection<Guid> existingMembers,
|
||||
ICollection<Guid> userIds)
|
||||
{
|
||||
// A user is already in the group, but some still need to be added
|
||||
userIds.Add(existingMembers.First());
|
||||
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "add",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>()
|
||||
.Received(1)
|
||||
.AddGroupUsersByIdAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||
arg.Count() == userIds.Count &&
|
||||
arg.ToHashSet().SetEquals(userIds)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, Guid userId)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new ScimPatchModel.OperationModel
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members[value eq \"{userId}\"]",
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
|
||||
Organization organization, Group group, ICollection<Guid> existingMembers)
|
||||
{
|
||||
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
sutProvider.GetDependency<IGroupRepository>()
|
||||
.GetManyUserIdsByIdAsync(group.Id)
|
||||
.Returns(existingMembers);
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Op = "remove",
|
||||
Path = $"members",
|
||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||
}
|
||||
},
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
var expectedRemainingUsers = existingMembers.Skip(2).ToList();
|
||||
await sutProvider.GetDependency<IGroupRepository>()
|
||||
.Received(1)
|
||||
.UpdateUsersAsync(
|
||||
group.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||
arg.Count() == expectedRemainingUsers.Count &&
|
||||
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PatchGroup_NoAction_Success(
|
||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group)
|
||||
{
|
||||
group.OrganizationId = organization.Id;
|
||||
|
||||
var scimPatchModel = new Models.ScimPatchModel
|
||||
{
|
||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
|
||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
|
||||
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
|
||||
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
@ -73,10 +71,6 @@ public class PostGroupCommandTests
|
||||
.GetManyByOrganizationIdAsync(organization.Id)
|
||||
.Returns(groups);
|
||||
|
||||
sutProvider.GetDependency<IScimContext>()
|
||||
.RequestScimProvider
|
||||
.Returns(ScimProviderType.Okta);
|
||||
|
||||
var group = await sutProvider.Sut.PostGroupAsync(organization, scimGroupRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<ICreateGroupCommand>().Received(1).CreateGroupAsync(group, organization, EventSystemUser.SCIM, null);
|
||||
|
@ -1,10 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Groups;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Utilities;
|
||||
@ -62,10 +60,6 @@ public class PutGroupCommandTests
|
||||
.GetByIdAsync(group.Id)
|
||||
.Returns(group);
|
||||
|
||||
sutProvider.GetDependency<IScimContext>()
|
||||
.RequestScimProvider
|
||||
.Returns(ScimProviderType.Okta);
|
||||
|
||||
var inputModel = new ScimGroupRequestModel
|
||||
{
|
||||
DisplayName = displayName,
|
||||
|
@ -20,4 +20,8 @@ IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs
|
||||
# Optional reverse proxy configuration
|
||||
# Should match server listen ports in reverse-proxy.conf
|
||||
API_PROXY_PORT=4100
|
||||
IDENTITY_PROXY_PORT=33756
|
||||
IDENTITY_PROXY_PORT=33756
|
||||
|
||||
# Optional RabbitMQ configuration
|
||||
RABBITMQ_DEFAULT_USER=bitwarden
|
||||
RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123
|
||||
|
@ -84,6 +84,20 @@ services:
|
||||
profiles:
|
||||
- idp
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:management
|
||||
container_name: rabbitmq
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq_data
|
||||
profiles:
|
||||
- rabbitmq
|
||||
|
||||
reverse-proxy:
|
||||
image: nginx:alpine
|
||||
container_name: reverse-proxy
|
||||
@ -95,7 +109,23 @@ services:
|
||||
profiles:
|
||||
- proxy
|
||||
|
||||
service-bus:
|
||||
container_name: service-bus
|
||||
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- "./servicebusemulator_config.json:/ServiceBus_Emulator/ConfigFiles/Config.json"
|
||||
ports:
|
||||
- "5672:5672"
|
||||
environment:
|
||||
SQL_SERVER: mssql
|
||||
MSSQL_SA_PASSWORD: "${MSSQL_PASSWORD}"
|
||||
ACCEPT_EULA: "Y"
|
||||
profiles:
|
||||
- servicebus
|
||||
|
||||
volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
rabbitmq_data:
|
||||
|
@ -7,11 +7,13 @@ param(
|
||||
[switch]$mysql,
|
||||
[switch]$mssql,
|
||||
[switch]$sqlite,
|
||||
[switch]$selfhost
|
||||
[switch]$selfhost,
|
||||
[switch]$test
|
||||
)
|
||||
|
||||
# Abort on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
$currentDir = Get-Location
|
||||
|
||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
||||
$mssql = $true;
|
||||
@ -25,36 +27,62 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($all -or $mssql) {
|
||||
function Get-UserSecrets {
|
||||
# The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments
|
||||
# to ensure a valid json
|
||||
return dotnet user-secrets list --json --project ../src/Api | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
}
|
||||
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||
function Get-UserSecrets {
|
||||
# The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments
|
||||
# to ensure a valid json
|
||||
return dotnet user-secrets list --json --project "$currentDir/../src/Api" | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$currentDir = Get-Location
|
||||
if ($all -or $mssql) {
|
||||
if ($all -or !$test) {
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
}
|
||||
|
||||
Foreach ($item in @(@($mysql, "MySQL", "MySqlMigrations"), @($postgres, "PostgreSQL", "PostgresMigrations"), @($sqlite, "SQLite", "SqliteMigrations"))) {
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||
}
|
||||
|
||||
if ($all -or $test) {
|
||||
$testMsSqlConnectionString = $(Get-UserSecrets).'databases:3:connectionString'
|
||||
if ($testMsSqlConnectionString) {
|
||||
$testEnvName = "test databases"
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $testEnvName"
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$testMsSqlConnectionString"
|
||||
} else {
|
||||
Write-Host "Connection string for a test MSSQL database not found in secrets.json!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Foreach ($item in @(
|
||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
|
||||
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1)
|
||||
)) {
|
||||
if (!$item[0] -and !$all) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Starting $($item[1]) Migrations"
|
||||
Set-Location "$currentDir/../util/$($item[2])/"
|
||||
dotnet ef database update
|
||||
if(!$test -or $all) {
|
||||
Write-Host "Starting $($item[1]) Migrations"
|
||||
$connectionString = $(Get-UserSecrets)."globalSettings:$($item[3]):connectionString"
|
||||
dotnet ef database update --connection "$connectionString"
|
||||
}
|
||||
if ($test -or $all) {
|
||||
$testConnectionString = $(Get-UserSecrets)."databases:$($item[4]):connectionString"
|
||||
if ($testConnectionString) {
|
||||
Write-Host "Starting $($item[1]) Migrations for test databases"
|
||||
dotnet ef database update --connection "$testConnectionString"
|
||||
} else {
|
||||
Write-Host "Connection string for a test $($item[1]) database not found in secrets.json!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set-Location "$currentDir"
|
||||
|
@ -21,7 +21,7 @@
|
||||
"connectionString": "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
|
||||
},
|
||||
"sqlite": {
|
||||
"connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.sqlite"
|
||||
"connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.db"
|
||||
},
|
||||
"identityServer": {
|
||||
"certificateThumbprint": "<your Identity certificate thumbprint with no spaces>"
|
||||
|
38
dev/servicebusemulator_config.json
Normal file
38
dev/servicebusemulator_config.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"UserConfig": {
|
||||
"Namespaces": [
|
||||
{
|
||||
"Name": "sbemulatorns",
|
||||
"Queues": [
|
||||
{
|
||||
"Name": "queue.1",
|
||||
"Properties": {
|
||||
"DeadLetteringOnMessageExpiration": false,
|
||||
"DefaultMessageTimeToLive": "PT1H",
|
||||
"DuplicateDetectionHistoryTimeWindow": "PT20S",
|
||||
"ForwardDeadLetteredMessagesTo": "",
|
||||
"ForwardTo": "",
|
||||
"LockDuration": "PT1M",
|
||||
"MaxDeliveryCount": 3,
|
||||
"RequiresDuplicateDetection": false,
|
||||
"RequiresSession": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"Topics": [
|
||||
{
|
||||
"Name": "event-logging",
|
||||
"Subscriptions": [
|
||||
{
|
||||
"Name": "events-write-subscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Logging": {
|
||||
"Type": "File"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
Invoke-RestMethod -OutFile $scriptPath -Uri "https://go.btwrdn.co/bw-ps"
|
||||
Write-Output "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Your 'bitwarden.ps1' script has been automatically upgraded. Please run it again."
|
@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cat << "EOF"
|
||||
_ _ _ _
|
||||
| |__ (_) |___ ____ _ _ __ __| | ___ _ __
|
||||
| '_ \| | __\ \ /\ / / _` | '__/ _` |/ _ \ '_ \
|
||||
| |_) | | |_ \ V V / (_| | | | (_| | __/ | | |
|
||||
|_.__/|_|\__| \_/\_/ \__,_|_| \__,_|\___|_| |_|
|
||||
EOF
|
||||
|
||||
cat << EOF
|
||||
Open source password management solutions
|
||||
Copyright 2015-$(date +'%Y'), 8bit Solutions LLC
|
||||
https://bitwarden.com, https://github.com/bitwarden
|
||||
===================================================
|
||||
EOF
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
SCRIPT_NAME=$(basename "$0")
|
||||
SCRIPT_PATH="$DIR/$SCRIPT_NAME"
|
||||
BITWARDEN_SCRIPT_URL="https://go.btwrdn.co/bw-sh"
|
||||
|
||||
if curl -L -s -w "http_code %{http_code}" -o $SCRIPT_PATH.1 $BITWARDEN_SCRIPT_URL | grep -q "^http_code 20[0-9]"
|
||||
then
|
||||
mv $SCRIPT_PATH.1 $SCRIPT_PATH
|
||||
chmod u+x $SCRIPT_PATH
|
||||
echo "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Your 'bitwarden.sh' script has been automatically upgraded. Please run it again."
|
||||
else
|
||||
rm -f $SCRIPT_PATH.1
|
||||
fi
|
@ -1,47 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################
|
||||
# Builds a specified service
|
||||
# Arguments:
|
||||
# 1: Project to build
|
||||
# 2: Project path
|
||||
##############################
|
||||
build() {
|
||||
local project=$1
|
||||
local project_dir=$2
|
||||
|
||||
echo "Building $project"
|
||||
echo "Build Path: $project_dir"
|
||||
echo "=================="
|
||||
|
||||
chmod u+x "$project_dir/build.sh"
|
||||
"$project_dir/build.sh"
|
||||
}
|
||||
|
||||
# Get Project
|
||||
PROJECT=$1; shift
|
||||
|
||||
case "$PROJECT" in
|
||||
"admin" | "Admin") build Admin $PWD/src/Admin ;;
|
||||
"api" | "Api") build Api $PWD/src/Api ;;
|
||||
"billing" | "Billing") build Billing $PWD/src/Billing ;;
|
||||
"events" | "Events") build Events $PWD/src/Events ;;
|
||||
"eventsprocessor" | "EventsProcessor") build EventsProcessor $PWD/src/EventsProcessor ;;
|
||||
"icons" | "Icons") build Icons $PWD/src/Icons ;;
|
||||
"identity" | "Identity") build Identity $PWD/src/Identity ;;
|
||||
"notifications" | "Notifications") build Notifications $PWD/src/Notifications ;;
|
||||
"server" | "Server") build Server $PWD/util/Server ;;
|
||||
"sso" | "Sso") build Sso $PWD/bitwarden_license/src/Sso ;;
|
||||
"")
|
||||
build Admin $PWD/src/Admin
|
||||
build Api $PWD/src/Api
|
||||
build Billing $PWD/src/Billing
|
||||
build Events $PWD/src/Events
|
||||
build EventsProcessor $PWD/src/EventsProcessor
|
||||
build Icons $PWD/src/Icons
|
||||
build Identity $PWD/src/Identity
|
||||
build Notifications $PWD/src/Notifications
|
||||
build Server $PWD/util/Server
|
||||
build Sso $PWD/bitwarden_license/src/Sso
|
||||
;;
|
||||
esac
|
@ -1,88 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################
|
||||
# Builds the docker image from a pre-built build directory
|
||||
# Arguments:
|
||||
# 1: Project Name
|
||||
# 2: Project Directory
|
||||
# 3: Docker Tag
|
||||
# 4: Docker push
|
||||
# Outputs:
|
||||
# Output to STDOUT or STDERR.
|
||||
# Returns:
|
||||
# Returned values other than the default exit status of the last command run.
|
||||
##############################
|
||||
docker_build() {
|
||||
local project_name=$1
|
||||
local project_dir=$2
|
||||
local docker_tag=$3
|
||||
local docker_push=$4
|
||||
|
||||
local project_name_lower=$(echo "$project_name" | awk '{print tolower($0)}')
|
||||
|
||||
echo "Building docker image: bitwarden/$project_name_lower:$docker_tag"
|
||||
echo "=============================="
|
||||
docker build -t bitwarden/$project_name_lower:$docker_tag $project_dir
|
||||
|
||||
if [ "$docker_push" == "1" ]; then
|
||||
docker push bitwarden/$project_name_lower:$docker_tag
|
||||
fi
|
||||
}
|
||||
|
||||
# Get Project
|
||||
PROJECT=$1; shift
|
||||
|
||||
# Get Params
|
||||
TAG="latest"
|
||||
PUSH=0
|
||||
|
||||
while [ ! $# -eq 0 ]; do
|
||||
case "$1" in
|
||||
-t | --tag)
|
||||
if [[ $2 ]]; then
|
||||
TAG="$2"
|
||||
shift
|
||||
else
|
||||
exp "--tag requires a value"
|
||||
fi
|
||||
;;
|
||||
--push) PUSH=1 ;;
|
||||
-h | --help ) usage && exit ;;
|
||||
*) usage && exit ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
|
||||
case "$PROJECT" in
|
||||
"admin" | "Admin") docker_build Admin $PWD/src/Admin $TAG $PUSH ;;
|
||||
"api" | "Api") docker_build Api $PWD/src/Api $TAG $PUSH ;;
|
||||
"attachments" | "Attachments") docker_build Attachments $PWD/util/Attachments $TAG $PUSH ;;
|
||||
#"billing" | "Billing") docker_build Billing $PWD/src/Billing $TAG $PUSH ;;
|
||||
"events" | "Events") docker_build Events $PWD/src/Events $TAG $PUSH ;;
|
||||
"eventsprocessor" | "EventsProcessor") docker_build EventsProcessor $PWD/src/EventsProcessor $TAG $PUSH ;;
|
||||
"icons" | "Icons") docker_build Icons $PWD/src/Icons $TAG $PUSH ;;
|
||||
"identity" | "Identity") docker_build Identity $PWD/src/Identity $TAG $PUSH ;;
|
||||
"mssql" | "MsSql" | "Mssql") docker_build MsSql $PWD/util/MsSql $TAG $PUSH ;;
|
||||
"nginx" | "Nginx") docker_build Nginx $PWD/util/Nginx $TAG $PUSH ;;
|
||||
"notifications" | "Notifications") docker_build Notifications $PWD/src/Notifications $TAG $PUSH ;;
|
||||
"server" | "Server") docker_build Server $PWD/util/Server $TAG $PUSH ;;
|
||||
"setup" | "Setup") docker_build Setup $PWD/util/Setup $TAG $PUSH ;;
|
||||
"sso" | "Sso") docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH ;;
|
||||
"")
|
||||
docker_build Admin $PWD/src/Admin $TAG $PUSH
|
||||
docker_build Api $PWD/src/Api $TAG $PUSH
|
||||
docker_build Attachments $PWD/util/Attachments $TAG $PUSH
|
||||
#docker_build Billing $PWD/src/Billing $TAG $PUSH
|
||||
docker_build Events $PWD/src/Events $TAG $PUSH
|
||||
docker_build EventsProcessor $PWD/src/EventsProcessor $TAG $PUSH
|
||||
docker_build Icons $PWD/src/Icons $TAG $PUSH
|
||||
docker_build Identity $PWD/src/Identity $TAG $PUSH
|
||||
docker_build MsSql $PWD/util/MsSql $TAG $PUSH
|
||||
docker_build Nginx $PWD/util/Nginx $TAG $PUSH
|
||||
docker_build Notifications $PWD/src/Notifications $TAG $PUSH
|
||||
docker_build Server $PWD/util/Server $TAG $PUSH
|
||||
docker_build Setup $PWD/util/Setup $TAG $PUSH
|
||||
docker_build Sso $PWD/bitwarden_license/src/Sso $TAG $PUSH
|
||||
;;
|
||||
esac
|
@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##############################
|
||||
# Builds the docker image from a pre-built build directory
|
||||
# Arguments:
|
||||
# 1: Project Name
|
||||
# 2: Project Directory
|
||||
# 3: Docker Tag
|
||||
# 4: Docker push
|
||||
##############################
|
||||
deploy_app_service() {
|
||||
local project_name=$1
|
||||
local project_dir=$2
|
||||
|
||||
local project_name_lower=$(echo "$project_name" | awk '{print tolower($0)}')
|
||||
local webapp_name=$(az keyvault secret show --vault-name bitwarden-qa-kv --name appservices-$project_name_lower-webapp-name --query value --output tsv)
|
||||
|
||||
cd $project_dir/obj/build-output/publish
|
||||
zip -r $project_name.zip .
|
||||
mv $project_name.zip ../../../
|
||||
#az webapp deploy --resource-group bw-qa-env --name $webapp_name \
|
||||
# --src-path $project_name.zip --verbose --type zip --restart true --subscription "Bitwarden Test"
|
||||
}
|
||||
|
||||
PROJECT=$1; shift
|
||||
|
||||
case "$PROJECT" in
|
||||
"api" | "Api") deploy_app_service Api $PWD/src/Api ;;
|
||||
"admin" | "Admin") deploy_app_service Admin $PWD/src/Admin ;;
|
||||
"identity" | "Identity") deploy_app_service Identity $PWD/src/Identity ;;
|
||||
"events" | "Events") deploy_app_service Events $PWD/src/Events ;;
|
||||
"billing" | "Billing") deploy_app_service Billing $PWD/src/Billing ;;
|
||||
"sso" | "Sso") deploy_app_service Sso $PWD/bitwarden_license/src/Sso ;;
|
||||
"")
|
||||
deploy_app_service Api $PWD/src/Api
|
||||
deploy_app_service Admin $PWD/src/Admin
|
||||
deploy_app_service Identity $PWD/src/Identity
|
||||
deploy_app_service Events $PWD/src/Events
|
||||
deploy_app_service Billing $PWD/src/Billing
|
||||
deploy_app_service Sso $PWD/bitwarden_license/src/Sso
|
||||
;;
|
||||
esac
|
@ -1,16 +0,0 @@
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
$bitwardenPath = Split-Path $scriptPath | Split-Path | Split-Path
|
||||
$files = Get-ChildItem $bitwardenPath
|
||||
$scriptFound = $false
|
||||
foreach ($file in $files) {
|
||||
if ($file.Name -eq "bitwarden.ps1") {
|
||||
$scriptFound = $true
|
||||
Invoke-RestMethod -OutFile "$($bitwardenPath)/bitwarden.ps1" -Uri "https://go.btwrdn.co/bw-ps"
|
||||
Write-Output "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Your 'bitwarden.ps1' script has been automatically upgraded. Please run it again."
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $scriptFound) {
|
||||
Write-Output "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Please run 'bitwarden.ps1 -updateself' before updating."
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cat << "EOF"
|
||||
_ _ _ _
|
||||
| |__ (_) |___ ____ _ _ __ __| | ___ _ __
|
||||
| '_ \| | __\ \ /\ / / _` | '__/ _` |/ _ \ '_ \
|
||||
| |_) | | |_ \ V V / (_| | | | (_| | __/ | | |
|
||||
|_.__/|_|\__| \_/\_/ \__,_|_| \__,_|\___|_| |_|
|
||||
EOF
|
||||
|
||||
cat << EOF
|
||||
Open source password management solutions
|
||||
Copyright 2015-$(date +'%Y'), 8bit Solutions LLC
|
||||
https://bitwarden.com, https://github.com/bitwarden
|
||||
===================================================
|
||||
EOF
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
BITWARDEN_SCRIPT_URL="https://go.btwrdn.co/bw-sh"
|
||||
|
||||
cd $DIR
|
||||
cd ../../
|
||||
|
||||
FOUND=false
|
||||
|
||||
for i in *.sh; do
|
||||
if [ $i = "bitwarden.sh" ]
|
||||
then
|
||||
FOUND=true
|
||||
if curl -L -s -w "http_code %{http_code}" -o bitwarden.sh.1 $BITWARDEN_SCRIPT_URL | grep -q "^http_code 20[0-9]"
|
||||
then
|
||||
mv bitwarden.sh.1 bitwarden.sh
|
||||
chmod u+x bitwarden.sh
|
||||
echo "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Your 'bitwarden.sh' script has been automatically upgraded. Please run it again."
|
||||
else
|
||||
rm -f bitwarden.sh.1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $FOUND = false ]
|
||||
then
|
||||
echo "We have moved our self-hosted scripts to their own repository (https://github.com/bitwarden/self-host). Please run 'bitwarden.sh updateself' before updating."
|
||||
fi
|
@ -16,7 +16,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Billing\Controllers\" />
|
||||
<Folder Include="Billing\Models\" />
|
||||
</ItemGroup>
|
||||
|
||||
<Choose>
|
||||
|
@ -3,13 +3,14 @@ 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.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -56,7 +57,8 @@ public class OrganizationsController : Controller
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -83,7 +85,8 @@ public class OrganizationsController : Controller
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IProviderBillingService providerBillingService,
|
||||
IFeatureService featureService)
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -109,7 +112,8 @@ public class OrganizationsController : Controller
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_providerBillingService = providerBillingService;
|
||||
_featureService = featureService;
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -209,6 +213,8 @@ public class OrganizationsController : Controller
|
||||
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
: -1;
|
||||
|
||||
var plans = await _pricingClient.ListPlans();
|
||||
|
||||
return View(new OrganizationEditModel(
|
||||
organization,
|
||||
provider,
|
||||
@ -221,6 +227,7 @@ public class OrganizationsController : Controller
|
||||
billingHistoryInfo,
|
||||
billingSyncConnection,
|
||||
_globalSettings,
|
||||
plans,
|
||||
secrets,
|
||||
projects,
|
||||
serviceAccounts,
|
||||
@ -250,8 +257,9 @@ public class OrganizationsController : Controller
|
||||
|
||||
UpdateOrganization(organization, model);
|
||||
|
||||
if (organization.UseSecretsManager &&
|
||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
|
||||
{
|
||||
TempData["Error"] = "Plan does not support Secrets Manager";
|
||||
return RedirectToAction("Edit", new { id });
|
||||
@ -306,7 +314,7 @@ public class OrganizationsController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Org_Delete)]
|
||||
[RequirePermission(Permission.Org_RequestDelete)]
|
||||
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -320,7 +328,7 @@ public class OrganizationsController : Controller
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization != null)
|
||||
{
|
||||
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||
await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||
TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
|
||||
}
|
||||
}
|
||||
@ -418,6 +426,11 @@ public class OrganizationsController : Controller
|
||||
|
||||
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
|
||||
{
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))
|
||||
{
|
||||
organization.Name = WebUtility.HtmlEncode(model.Name);
|
||||
}
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||
{
|
||||
organization.Enabled = model.Enabled;
|
||||
@ -448,6 +461,7 @@ public class OrganizationsController : Controller
|
||||
organization.UseTotp = model.UseTotp;
|
||||
organization.UsersGetPremium = model.UsersGetPremium;
|
||||
organization.UseSecretsManager = model.UseSecretsManager;
|
||||
organization.UseRiskInsights = model.UseRiskInsights;
|
||||
|
||||
//secrets
|
||||
organization.SmSeats = model.SmSeats;
|
||||
@ -475,14 +489,6 @@ public class OrganizationsController : Controller
|
||||
Organization organization,
|
||||
OrganizationEditModel update)
|
||||
{
|
||||
var scaleMSPOnClientOrganizationUpdate =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
|
||||
|
||||
if (!scaleMSPOnClientOrganizationUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// No scaling required
|
||||
|
@ -3,7 +3,6 @@ using System.Net;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
@ -12,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -43,6 +43,7 @@ public class ProvidersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
@ -61,7 +62,8 @@ public class ProvidersController : Controller
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment)
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
@ -76,6 +78,7 @@ public class ProvidersController : Controller
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
@ -133,11 +136,6 @@ public class ProvidersController : Controller
|
||||
[HttpGet("providers/create/multi-organization-enterprise")]
|
||||
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
return RedirectToAction("Create");
|
||||
}
|
||||
|
||||
return View(new CreateMultiOrganizationEnterpriseProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail,
|
||||
@ -211,10 +209,6 @@ public class ProvidersController : Controller
|
||||
}
|
||||
var provider = model.ToProvider();
|
||||
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
return RedirectToAction("Create");
|
||||
}
|
||||
await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync(
|
||||
provider,
|
||||
model.OwnerEmail,
|
||||
@ -235,7 +229,8 @@ public class ProvidersController : Controller
|
||||
|
||||
var users = await _providerUserRepository.GetManyDetailsByProviderAsync(id);
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
return View(new ProviderViewModel(provider, users, providerOrganizations));
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
return View(new ProviderViewModel(provider, users, providerOrganizations, providerPlans.ToList()));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
@ -250,6 +245,18 @@ public class ProvidersController : Controller
|
||||
return View(provider);
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IActionResult> Cancel(Guid id)
|
||||
{
|
||||
var provider = await GetEditModel(id);
|
||||
if (provider == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
@ -412,7 +419,9 @@ public class ProvidersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
return View(new OrganizationEditModel(provider));
|
||||
var plans = await _pricingClient.ListPlans();
|
||||
|
||||
return View(new OrganizationEditModel(provider, plans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@ -10,6 +10,9 @@ public class CreateMspProviderModel : IValidatableObject
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Subscription Discount")]
|
||||
public string DiscountId { get; set; }
|
||||
|
||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||
public int TeamsMonthlySeatMinimum { get; set; }
|
||||
|
||||
@ -20,7 +23,8 @@ public class CreateMspProviderModel : IValidatableObject
|
||||
{
|
||||
return new Provider
|
||||
{
|
||||
Type = ProviderType.Msp
|
||||
Type = ProviderType.Msp,
|
||||
DiscountId = DiscountId
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@ -17,15 +18,18 @@ namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationEditModel : OrganizationViewModel
|
||||
{
|
||||
private readonly List<Plan> _plans;
|
||||
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
public OrganizationEditModel(Provider provider, List<Plan> plans)
|
||||
{
|
||||
Provider = provider;
|
||||
BillingEmail = provider.Type == ProviderType.Reseller ? provider.BillingEmail : string.Empty;
|
||||
PlanType = Core.Billing.Enums.PlanType.TeamsMonthly;
|
||||
Plan = Core.Billing.Enums.PlanType.TeamsMonthly.GetDisplayAttribute()?.GetName();
|
||||
LicenseKey = RandomLicenseKey;
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
public OrganizationEditModel(
|
||||
@ -40,6 +44,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
IEnumerable<OrganizationConnection> connections,
|
||||
GlobalSettings globalSettings,
|
||||
List<Plan> plans,
|
||||
int secrets,
|
||||
int projects,
|
||||
int serviceAccounts,
|
||||
@ -80,6 +85,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
Use2fa = org.Use2fa;
|
||||
UseApi = org.UseApi;
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
UseRiskInsights = org.UseRiskInsights;
|
||||
UseResetPassword = org.UseResetPassword;
|
||||
SelfHost = org.SelfHost;
|
||||
UsersGetPremium = org.UsersGetPremium;
|
||||
@ -95,6 +101,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
@ -143,7 +151,9 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
[Display(Name = "SCIM")]
|
||||
public bool UseScim { get; set; }
|
||||
[Display(Name = "Secrets Manager")]
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public new bool UseSecretsManager { get; set; }
|
||||
[Display(Name = "Risk Insights")]
|
||||
public new bool UseRiskInsights { get; set; }
|
||||
[Display(Name = "Self Host")]
|
||||
public bool SelfHost { get; set; }
|
||||
[Display(Name = "Users Get Premium")]
|
||||
@ -180,7 +190,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
* Add mappings for individual properties as you need them
|
||||
*/
|
||||
public object GetPlansHelper() =>
|
||||
StaticStore.Plans
|
||||
_plans
|
||||
.Select(p =>
|
||||
{
|
||||
var plan = new
|
||||
@ -284,6 +294,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
existingOrganization.Use2fa = Use2fa;
|
||||
existingOrganization.UseApi = UseApi;
|
||||
existingOrganization.UseSecretsManager = UseSecretsManager;
|
||||
existingOrganization.UseRiskInsights = UseRiskInsights;
|
||||
existingOrganization.UseResetPassword = UseResetPassword;
|
||||
existingOrganization.SelfHost = SelfHost;
|
||||
existingOrganization.UsersGetPremium = UsersGetPremium;
|
||||
|
@ -69,4 +69,5 @@ public class OrganizationViewModel
|
||||
public int ServiceAccountsCount { get; set; }
|
||||
public int OccupiedSmSeatsCount { get; set; }
|
||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||
}
|
||||
|
@ -10,4 +10,6 @@ public class OrganizationsModel : PagedModel<Organization>
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
|
||||
public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||
IReadOnlyCollection<ProviderPlan> providerPlans,
|
||||
string gatewayCustomerUrl = null,
|
||||
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations)
|
||||
string gatewaySubscriptionUrl = null) : base(provider, providerUsers, organizations, providerPlans)
|
||||
{
|
||||
Name = provider.DisplayName();
|
||||
BusinessName = provider.DisplayBusinessName();
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Admin.Billing.Models;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
@ -8,17 +11,57 @@ public class ProviderViewModel
|
||||
{
|
||||
public ProviderViewModel() { }
|
||||
|
||||
public ProviderViewModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
public ProviderViewModel(
|
||||
Provider provider,
|
||||
IEnumerable<ProviderUserUserDetails> providerUsers,
|
||||
IEnumerable<ProviderOrganizationOrganizationDetails> organizations,
|
||||
IReadOnlyCollection<ProviderPlan> providerPlans)
|
||||
{
|
||||
Provider = provider;
|
||||
UserCount = providerUsers.Count();
|
||||
ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin);
|
||||
|
||||
ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id);
|
||||
|
||||
if (Provider.Type == ProviderType.Msp)
|
||||
{
|
||||
var usedTeamsSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.TeamsMonthly)
|
||||
.Sum(po => po.OccupiedSeats) ?? 0;
|
||||
var teamsProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.TeamsMonthly);
|
||||
if (teamsProviderPlan != null && teamsProviderPlan.IsConfigured())
|
||||
{
|
||||
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Teams (Monthly) Subscription", teamsProviderPlan, usedTeamsSeats));
|
||||
}
|
||||
|
||||
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
|
||||
.Sum(po => po.OccupiedSeats) ?? 0;
|
||||
var enterpriseProviderPlan = providerPlans.FirstOrDefault(plan => plan.PlanType == PlanType.EnterpriseMonthly);
|
||||
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
|
||||
{
|
||||
ProviderPlanViewModels.Add(new ProviderPlanViewModel("Enterprise (Monthly) Subscription", enterpriseProviderPlan, usedEnterpriseSeats));
|
||||
}
|
||||
}
|
||||
else if (Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
var usedEnterpriseSeats = ProviderOrganizations.Where(po => po.PlanType == PlanType.EnterpriseMonthly)
|
||||
.Sum(po => po.OccupiedSeats).GetValueOrDefault(0);
|
||||
var enterpriseProviderPlan = providerPlans.FirstOrDefault();
|
||||
if (enterpriseProviderPlan != null && enterpriseProviderPlan.IsConfigured())
|
||||
{
|
||||
var planLabel = enterpriseProviderPlan.PlanType switch
|
||||
{
|
||||
PlanType.EnterpriseMonthly => "Enterprise (Monthly) Subscription",
|
||||
PlanType.EnterpriseAnnually => "Enterprise (Annually) Subscription",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
ProviderPlanViewModels.Add(new ProviderPlanViewModel(planLabel, enterpriseProviderPlan, usedEnterpriseSeats));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int UserCount { get; set; }
|
||||
public Provider Provider { get; set; }
|
||||
public IEnumerable<ProviderUserUserDetails> ProviderAdmins { get; set; }
|
||||
public IEnumerable<ProviderOrganizationOrganizationDetails> ProviderOrganizations { get; set; }
|
||||
public List<ProviderPlanViewModel> ProviderPlanViewModels { get; set; } = [];
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
|
||||
var canRequestDelete = AccessControlService.UserHasPermission(Permission.Org_RequestDelete);
|
||||
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
@ -120,12 +121,15 @@
|
||||
Unlink provider
|
||||
</button>
|
||||
}
|
||||
@if (canDelete)
|
||||
@if (canRequestDelete)
|
||||
{
|
||||
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
||||
<button class="btn btn-danger me-2" type="submit">Request Delete</button>
|
||||
</form>
|
||||
}
|
||||
@if (canDelete)
|
||||
{
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||
<button class="btn btn-outline-danger" type="submit">Delete</button>
|
||||
|
@ -81,16 +81,7 @@
|
||||
<i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i>
|
||||
}
|
||||
}
|
||||
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
|
||||
{
|
||||
<i class="fa fa-plus-square fa-lg fa-fw"
|
||||
title="Additional Storage, @(org.MaxStorageGb - 1) GB"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
|
||||
title="No Additional Storage"></i>
|
||||
}
|
||||
<i class="fa fa-hdd-o fa-lg fa-fw" title="Used Storage, @Model.StorageGB(org) GB"></i>
|
||||
@if(org.Enabled)
|
||||
{
|
||||
<i class="fa fa-check-circle fa-lg fa-fw"
|
||||
|
@ -55,19 +55,11 @@
|
||||
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
|
||||
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
|
||||
|
||||
@if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
||||
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
|
||||
}
|
||||
else
|
||||
{
|
||||
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
||||
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
|
||||
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
||||
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
|
||||
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
|
||||
}
|
||||
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
|
||||
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Secrets Manager</h2>
|
||||
|
@ -12,11 +12,6 @@
|
||||
var providerTypes = Enum.GetValues<ProviderType>()
|
||||
.OrderBy(x => x.GetDisplayAttribute().Order)
|
||||
.ToList();
|
||||
|
||||
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
|
||||
}
|
||||
}
|
||||
|
||||
<h1>Create Provider</h1>
|
||||
|
@ -1,3 +1,4 @@
|
||||
@using Bit.Core.Billing.Constants
|
||||
@model CreateMspProviderModel
|
||||
|
||||
@{
|
||||
@ -12,6 +13,19 @@
|
||||
<label asp-for="OwnerEmail" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@{
|
||||
var selectList = new List<SelectListItem>
|
||||
{
|
||||
new ("No discount", string.Empty, true),
|
||||
new ("20% - Open", StripeConstants.CouponIDs.MSPDiscounts.Open),
|
||||
new ("35% - Silver", StripeConstants.CouponIDs.MSPDiscounts.Silver),
|
||||
new ("50% - Gold", StripeConstants.CouponIDs.MSPDiscounts.Gold)
|
||||
};
|
||||
}
|
||||
<label asp-for="DiscountId" class="form-label"></label>
|
||||
<select class="form-select" asp-for="DiscountId" asp-items="selectList"></select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
|
@ -19,8 +19,8 @@
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
<div class="ms-auto d-flex">
|
||||
<form asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to cancel?')">
|
||||
<form asp-controller="Providers" asp-action="Cancel" asp-route-id="@Model.Provider.Id"
|
||||
onsubmit="return confirm('Are you sure you want to cancel?')">
|
||||
<button class="btn btn-outline-secondary" type="submit">Cancel</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -17,6 +17,10 @@
|
||||
|
||||
<h2>Provider Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@if (Model.ProviderPlanViewModels.Any())
|
||||
{
|
||||
@await Html.PartialAsync("~/Billing/Views/Providers/ProviderPlans.cshtml", Model.ProviderPlanViewModels)
|
||||
}
|
||||
@await Html.PartialAsync("Admins", Model)
|
||||
<form method="post" id="edit-form">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
@ -72,32 +76,29 @@
|
||||
}
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
{
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
PlanType.EnterpriseAnnually,
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan" class="form-label"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseMinimumSeats" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
PlanType.EnterpriseAnnually,
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan" class="form-label"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="EnterpriseMinimumSeats" class="form-label"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -7,5 +7,9 @@
|
||||
|
||||
<h2>Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@if (Model.ProviderPlanViewModels.Any())
|
||||
{
|
||||
@await Html.PartialAsync("ProviderPlans", Model.ProviderPlanViewModels)
|
||||
}
|
||||
@await Html.PartialAsync("Admins", Model)
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
|
@ -12,6 +12,7 @@
|
||||
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
||||
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
||||
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
||||
var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit);
|
||||
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
||||
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
|
||||
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
|
||||
@ -28,7 +29,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required disabled="@(canEditName ? null : "disabled")">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,7 +95,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2>Features</h2>
|
||||
<div class="row mb-3">
|
||||
<div class="row mb-4">
|
||||
<div class="col-4">
|
||||
<h3>General</h3>
|
||||
<div class="form-check mb-2">
|
||||
@ -146,7 +147,7 @@
|
||||
<label class="form-check-label" asp-for="UseCustomPermissions"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-3">
|
||||
<h3>Password Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseTotp" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
@ -157,13 +158,20 @@
|
||||
<label class="form-check-label" asp-for="UsersGetPremium"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="col-3">
|
||||
<h3>Secrets Manager</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseSecretsManager" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseSecretsManager"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<h3>Access Intelligence</h3>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" asp-for="UseRiskInsights" disabled='@(canEditPlan ? null : "disabled")'>
|
||||
<label class="form-check-label" asp-for="UseRiskInsights"></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
26
src/Admin/Billing/Models/ProviderPlanViewModel.cs
Normal file
26
src/Admin/Billing/Models/ProviderPlanViewModel.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Bit.Core.Billing.Entities;
|
||||
|
||||
namespace Bit.Admin.Billing.Models;
|
||||
|
||||
public class ProviderPlanViewModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int PurchasedSeats { get; set; }
|
||||
public int AssignedSeats { get; set; }
|
||||
public int UsedSeats { get; set; }
|
||||
public int RemainingSeats { get; set; }
|
||||
|
||||
public ProviderPlanViewModel(
|
||||
string name,
|
||||
ProviderPlan providerPlan,
|
||||
int usedSeats)
|
||||
{
|
||||
var purchasedSeats = (providerPlan.SeatMinimum ?? 0) + (providerPlan.PurchasedSeats ?? 0);
|
||||
|
||||
Name = name;
|
||||
PurchasedSeats = purchasedSeats;
|
||||
AssignedSeats = providerPlan.AllocatedSeats ?? 0;
|
||||
UsedSeats = usedSeats;
|
||||
RemainingSeats = purchasedSeats - AssignedSeats;
|
||||
}
|
||||
}
|
18
src/Admin/Billing/Views/Providers/ProviderPlans.cshtml
Normal file
18
src/Admin/Billing/Views/Providers/ProviderPlans.cshtml
Normal file
@ -0,0 +1,18 @@
|
||||
@model List<Bit.Admin.Billing.Models.ProviderPlanViewModel>
|
||||
@foreach (var plan in Model)
|
||||
{
|
||||
<h2>@plan.Name</h2>
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Purchased Seats</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@plan.PurchasedSeats</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Assigned Seats</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@plan.AssignedSeats</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Used Seats</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@plan.UsedSeats</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Remaining Seats</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@plan.RemainingSeats</dd>
|
||||
</dl>
|
||||
}
|
@ -4,16 +4,17 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
|
||||
namespace Bit.Admin.Controllers;
|
||||
|
||||
@ -28,8 +29,8 @@ public class ToolsController : Controller
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ITaxRateRepository _taxRateRepository;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
@ -41,7 +42,7 @@ public class ToolsController : Controller
|
||||
ITransactionRepository transactionRepository,
|
||||
IInstallationRepository installationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ITaxRateRepository taxRateRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IPaymentService paymentService,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IWebHostEnvironment environment)
|
||||
@ -53,7 +54,7 @@ public class ToolsController : Controller
|
||||
_transactionRepository = transactionRepository;
|
||||
_installationRepository = installationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_taxRateRepository = taxRateRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_paymentService = paymentService;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_environment = environment;
|
||||
@ -220,6 +221,44 @@ public class ToolsController : Controller
|
||||
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
|
||||
public IActionResult PromoteProviderServiceUser()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
|
||||
public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var providerUsers = await _providerUserRepository.GetManyByProviderAsync(
|
||||
model.ProviderId.Value, null);
|
||||
var serviceUser = providerUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
|
||||
if (serviceUser == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.UserId), "Service User Id not found in this provider.");
|
||||
}
|
||||
else if (serviceUser.Type != Core.AdminConsole.Enums.Provider.ProviderUserType.ServiceUser)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.UserId), "User is not a service user of this provider.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
serviceUser.Type = Core.AdminConsole.Enums.Provider.ProviderUserType.ProviderAdmin;
|
||||
await _providerUserRepository.ReplaceAsync(serviceUser);
|
||||
return RedirectToAction("Edit", "Providers", new { id = model.ProviderId.Value });
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_GenerateLicenseFile)]
|
||||
public IActionResult GenerateLicense()
|
||||
{
|
||||
@ -300,165 +339,6 @@ public class ToolsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRate(int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var rates = await _taxRateRepository.SearchAsync(skip, count);
|
||||
return View(new TaxRatesModel
|
||||
{
|
||||
Items = rates.ToList(),
|
||||
Page = page,
|
||||
Count = count
|
||||
});
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stripeTaxRateId))
|
||||
{
|
||||
return View(new TaxRateAddEditModel());
|
||||
}
|
||||
|
||||
var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId);
|
||||
var model = new TaxRateAddEditModel()
|
||||
{
|
||||
StripeTaxRateId = stripeTaxRateId,
|
||||
Country = rate.Country,
|
||||
State = rate.State,
|
||||
PostalCode = rate.PostalCode,
|
||||
Rate = rate.Rate
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateUpload(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
// Build rates and validate them first before updating DB & Stripe
|
||||
var taxRateUpdates = new List<TaxRate>();
|
||||
var currentTaxRates = await _taxRateRepository.GetAllActiveAsync();
|
||||
using var reader = new StreamReader(file.OpenReadStream());
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var taxParts = line.Split(',');
|
||||
if (taxParts.Length < 2)
|
||||
{
|
||||
throw new Exception($"This line is not in the format of <postal code>,<rate>,<state code>,<country code>: {line}");
|
||||
}
|
||||
var postalCode = taxParts[0].Trim();
|
||||
if (string.IsNullOrWhiteSpace(postalCode))
|
||||
{
|
||||
throw new Exception($"'{line}' is not valid, the first element must contain a postal code.");
|
||||
}
|
||||
if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100)
|
||||
{
|
||||
throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}");
|
||||
}
|
||||
var state = taxParts.Length > 2 ? taxParts[2] : null;
|
||||
var country = (taxParts.Length > 3 ? taxParts[3] : null);
|
||||
if (string.IsNullOrWhiteSpace(country))
|
||||
{
|
||||
country = "US";
|
||||
}
|
||||
var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ??
|
||||
new TaxRate
|
||||
{
|
||||
Country = country,
|
||||
PostalCode = postalCode,
|
||||
Active = true,
|
||||
};
|
||||
taxRate.Rate = rate;
|
||||
taxRate.State = state ?? taxRate.State;
|
||||
taxRateUpdates.Add(taxRate);
|
||||
}
|
||||
|
||||
foreach (var taxRate in taxRateUpdates)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(taxRate.Id))
|
||||
{
|
||||
await _paymentService.UpdateTaxRateAsync(taxRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.CreateTaxRateAsync(taxRate);
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateAddEdit(TaxRateAddEditModel model)
|
||||
{
|
||||
var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode });
|
||||
if (existingRateCheck.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var taxRate = new TaxRate()
|
||||
{
|
||||
Id = model.StripeTaxRateId,
|
||||
Country = model.Country,
|
||||
State = model.State,
|
||||
PostalCode = model.PostalCode,
|
||||
Rate = model.Rate
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId))
|
||||
{
|
||||
await _paymentService.UpdateTaxRateAsync(taxRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.CreateTaxRateAsync(taxRate);
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateArchive(string stripeTaxRateId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(stripeTaxRateId))
|
||||
{
|
||||
await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId });
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
||||
public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
|
||||
{
|
||||
|
@ -102,12 +102,14 @@ public class UsersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id, withOrganizations: false);
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain));
|
||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -162,6 +164,22 @@ public class UsersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
// TODO: Feature flag to be removed in PM-14207
|
||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||
{
|
||||
|
@ -17,13 +17,16 @@ public enum Permission
|
||||
User_Billing_View,
|
||||
User_Billing_Edit,
|
||||
User_Billing_LaunchGateway,
|
||||
User_NewDeviceException_Edit,
|
||||
|
||||
Org_List_View,
|
||||
Org_OrgInformation_View,
|
||||
Org_GeneralDetails_View,
|
||||
Org_Name_Edit,
|
||||
Org_CheckEnabledBox,
|
||||
Org_BusinessInformation_View,
|
||||
Org_InitiateTrial,
|
||||
Org_RequestDelete,
|
||||
Org_Delete,
|
||||
Org_BillingInformation_View,
|
||||
Org_BillingInformation_DownloadInvoice,
|
||||
@ -44,6 +47,7 @@ public enum Permission
|
||||
|
||||
Tools_ChargeBrainTreeCustomer,
|
||||
Tools_PromoteAdmin,
|
||||
Tools_PromoteProviderServiceUser,
|
||||
Tools_GenerateLicenseFile,
|
||||
Tools_ManageTaxRates,
|
||||
Tools_ManageStripeSubscriptions,
|
||||
|
13
src/Admin/Models/PromoteProviderServiceUserModel.cs
Normal file
13
src/Admin/Models/PromoteProviderServiceUserModel.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class PromoteProviderServiceUserModel
|
||||
{
|
||||
[Required]
|
||||
[Display(Name = "Provider Service User Id")]
|
||||
public Guid? UserId { get; set; }
|
||||
[Required]
|
||||
[Display(Name = "Provider Id")]
|
||||
public Guid? ProviderId { get; set; }
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class TaxRateAddEditModel
|
||||
{
|
||||
public string StripeTaxRateId { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string State { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class TaxRatesModel : PagedModel<TaxRate>
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
@ -9,10 +9,7 @@ namespace Bit.Admin.Models;
|
||||
|
||||
public class UserEditModel
|
||||
{
|
||||
public UserEditModel()
|
||||
{
|
||||
|
||||
}
|
||||
public UserEditModel() { }
|
||||
|
||||
public UserEditModel(
|
||||
User user,
|
||||
@ -21,10 +18,12 @@ public class UserEditModel
|
||||
BillingInfo billingInfo,
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
GlobalSettings globalSettings,
|
||||
bool? domainVerified
|
||||
)
|
||||
bool? claimedAccount,
|
||||
bool? activeNewDeviceVerificationException)
|
||||
{
|
||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
|
||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);
|
||||
|
||||
ActiveNewDeviceVerificationException = activeNewDeviceVerificationException ?? false;
|
||||
|
||||
BillingInfo = billingInfo;
|
||||
BillingHistoryInfo = billingHistoryInfo;
|
||||
@ -48,6 +47,8 @@ public class UserEditModel
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; init; }
|
||||
public bool ActiveNewDeviceVerificationException { get; init; }
|
||||
|
||||
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; init; }
|
||||
|
@ -14,7 +14,7 @@ public class UserViewModel
|
||||
public bool Premium { get; }
|
||||
public short? MaxStorageGb { get; }
|
||||
public bool EmailVerified { get; }
|
||||
public bool? DomainVerified { get; }
|
||||
public bool? ClaimedAccount { get; }
|
||||
public bool TwoFactorEnabled { get; }
|
||||
public DateTime AccountRevisionDate { get; }
|
||||
public DateTime RevisionDate { get; }
|
||||
@ -36,7 +36,7 @@ public class UserViewModel
|
||||
bool premium,
|
||||
short? maxStorageGb,
|
||||
bool emailVerified,
|
||||
bool? domainVerified,
|
||||
bool? claimedAccount,
|
||||
bool twoFactorEnabled,
|
||||
DateTime accountRevisionDate,
|
||||
DateTime revisionDate,
|
||||
@ -58,7 +58,7 @@ public class UserViewModel
|
||||
Premium = premium;
|
||||
MaxStorageGb = maxStorageGb;
|
||||
EmailVerified = emailVerified;
|
||||
DomainVerified = domainVerified;
|
||||
ClaimedAccount = claimedAccount;
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
AccountRevisionDate = accountRevisionDate;
|
||||
RevisionDate = revisionDate;
|
||||
@ -79,7 +79,7 @@ public class UserViewModel
|
||||
users.Select(user => MapViewModel(user, lookup, false));
|
||||
|
||||
public static UserViewModel MapViewModel(User user,
|
||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
|
||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? claimedAccount) =>
|
||||
new(
|
||||
user.Id,
|
||||
user.Name,
|
||||
@ -89,7 +89,7 @@ public class UserViewModel
|
||||
user.Premium,
|
||||
user.MaxStorageGb,
|
||||
user.EmailVerified,
|
||||
domainVerified,
|
||||
claimedAccount,
|
||||
IsTwoFactorEnabled(user, lookup),
|
||||
user.AccountRevisionDate,
|
||||
user.RevisionDate,
|
||||
@ -106,7 +106,7 @@ public class UserViewModel
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
||||
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
|
||||
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? claimedAccount) =>
|
||||
new(
|
||||
user.Id,
|
||||
user.Name,
|
||||
@ -116,7 +116,7 @@ public class UserViewModel
|
||||
user.Premium,
|
||||
user.MaxStorageGb,
|
||||
user.EmailVerified,
|
||||
domainVerified,
|
||||
claimedAccount,
|
||||
isTwoFactorEnabled,
|
||||
user.AccountRevisionDate,
|
||||
user.RevisionDate,
|
||||
|
@ -12,7 +12,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_Delete,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
@ -24,12 +23,16 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
Permission.Org_BusinessInformation_View,
|
||||
Permission.Org_InitiateTrial,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Org_BillingInformation_View,
|
||||
Permission.Org_BillingInformation_DownloadInvoice,
|
||||
Permission.Org_Plan_View,
|
||||
@ -45,6 +48,7 @@ public static class RolePermissionMapping
|
||||
Permission.Provider_ResendEmailInvite,
|
||||
Permission.Tools_ChargeBrainTreeCustomer,
|
||||
Permission.Tools_PromoteAdmin,
|
||||
Permission.Tools_PromoteProviderServiceUser,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions
|
||||
@ -55,7 +59,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_Delete,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
@ -68,11 +71,15 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
Permission.Org_BusinessInformation_View,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Org_BillingInformation_View,
|
||||
Permission.Org_BillingInformation_DownloadInvoice,
|
||||
Permission.Org_BillingInformation_CreateEditTransaction,
|
||||
@ -91,6 +98,7 @@ public static class RolePermissionMapping
|
||||
Permission.Provider_ResendEmailInvite,
|
||||
Permission.Tools_ChargeBrainTreeCustomer,
|
||||
Permission.Tools_PromoteAdmin,
|
||||
Permission.Tools_PromoteProviderServiceUser,
|
||||
Permission.Tools_GenerateLicenseFile,
|
||||
Permission.Tools_ManageTaxRates,
|
||||
Permission.Tools_ManageStripeSubscriptions,
|
||||
@ -102,7 +110,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
@ -110,6 +117,9 @@ public static class RolePermissionMapping
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
@ -121,6 +131,7 @@ public static class RolePermissionMapping
|
||||
Permission.Org_Licensing_View,
|
||||
Permission.Org_Billing_View,
|
||||
Permission.Org_Billing_LaunchGateway,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Provider_List_View,
|
||||
Permission.Provider_View
|
||||
}
|
||||
@ -130,7 +141,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
@ -141,6 +151,8 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
@ -154,6 +166,7 @@ public static class RolePermissionMapping
|
||||
Permission.Org_Billing_View,
|
||||
Permission.Org_Billing_Edit,
|
||||
Permission.Org_Billing_LaunchGateway,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Provider_Edit,
|
||||
Permission.Provider_View,
|
||||
Permission.Provider_List_View,
|
||||
@ -171,12 +184,13 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
Permission.User_Premium_View,
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Licensing_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
|
@ -11,14 +11,14 @@
|
||||
var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
|
||||
var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
|
||||
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
||||
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
||||
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
|
||||
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
|
||||
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
|
||||
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
|
||||
|
||||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin ||
|
||||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
|
||||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
|
||||
canGenerateLicense || canManageStripeSubscriptions;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -88,7 +88,13 @@
|
||||
@if (canPromoteAdmin)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="PromoteAdmin">
|
||||
Promote Admin
|
||||
Promote Organization Admin
|
||||
</a>
|
||||
}
|
||||
@if (canPromoteProviderServiceUser)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="PromoteProviderServiceUser">
|
||||
Promote Provider Service User
|
||||
</a>
|
||||
}
|
||||
@if (canGenerateLicense)
|
||||
@ -97,12 +103,6 @@
|
||||
Generate License
|
||||
</a>
|
||||
}
|
||||
@if (canManageTaxRates)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="TaxRate">
|
||||
Manage Tax Rates
|
||||
</a>
|
||||
}
|
||||
@if (canManageStripeSubscriptions)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions">
|
||||
|
25
src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml
Normal file
25
src/Admin/Views/Tools/PromoteProviderServiceUser.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@model PromoteProviderServiceUserModel
|
||||
@{
|
||||
ViewData["Title"] = "Promote Provider Service User";
|
||||
}
|
||||
|
||||
<h1>Promote Provider Service User</h1>
|
||||
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="UserId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="UserId">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="ProviderId" class="form-label"></label>
|
||||
<input type="text" class="form-control" asp-for="ProviderId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Promote Service User</button>
|
||||
</form>
|
@ -1,127 +0,0 @@
|
||||
@model TaxRatesModel
|
||||
@{
|
||||
ViewData["Title"] = "Tax Rates";
|
||||
}
|
||||
|
||||
<h1>Manage Tax Rates</h1>
|
||||
|
||||
<h2>Bulk Upload Tax Rates</h2>
|
||||
<section>
|
||||
<p>
|
||||
Upload a CSV file containing multiple tax rates in bulk in order to update existing rates by country
|
||||
and postal code OR to create new rates where a currently active rate is not found already.
|
||||
</p>
|
||||
<p>CSV Upload Format</p>
|
||||
<ul>
|
||||
<li><b>Postal Code</b> (required) - The postal code for the tax rate.</li>
|
||||
<li><b>Rate</b> (required) - The effective tax rate for this postal code.</li>
|
||||
<li><b>State</b> (<i>optional</i>) - The ISO-2 character code for the state. Optional but recommended.</li>
|
||||
<li><b>Country</b> (<i>optional</i>) - The ISO-2 character country code, defaults to "US" if not provided.</li>
|
||||
</ul>
|
||||
<p>Example (white-space is ignored):</p>
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<pre class="mb-0">87654,8.25,FL,US
|
||||
22334,8.5,CA
|
||||
11223,7</pre>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" name="file" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="Upload" class="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr class="my-4">
|
||||
<h2>View & Manage Tax Rates</h2>
|
||||
<a class="btn btn-primary mb-3" asp-controller="Tools" asp-action="TaxRateAddEdit">Add a Rate</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 190px;">Id</th>
|
||||
<th style="width: 80px;">Country</th>
|
||||
<th style="width: 80px;">State</th>
|
||||
<th style="width: 150px;">Postal Code</th>
|
||||
<th style="width: 160px;">Tax Rate</th>
|
||||
<th style="width: 80px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var rate in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@{
|
||||
var taxRateToEdit = new Dictionary<string, string>
|
||||
{
|
||||
{ "id", rate.Id },
|
||||
{ "stripeTaxRateId", rate.Id }
|
||||
};
|
||||
}
|
||||
<a asp-controller="Tools" asp-action="TaxRateAddEdit" asp-all-route-data="taxRateToEdit">@rate.Id</a>
|
||||
</td>
|
||||
<td>
|
||||
@rate.Country
|
||||
</td>
|
||||
<td>
|
||||
@rate.State
|
||||
</td>
|
||||
<td>
|
||||
@rate.PostalCode
|
||||
</td>
|
||||
<td>
|
||||
@rate.Rate%
|
||||
</td>
|
||||
<td>
|
||||
<a class="delete-button" data-id="@rate.Id" asp-controller="Tools" asp-action="TaxRateArchive" asp-route-stripeTaxRateId="@rate.Id">
|
||||
<i class="fa fa-trash fa-lg fa-fw"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Tax rates pagination">
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.PreviousPage.Value" asp-route-count="@Model.Count">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.NextPage.Value" asp-route-count="@Model.Count">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
@ -1,356 +0,0 @@
|
||||
@model TaxRateAddEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Add/Edit Tax Rate";
|
||||
}
|
||||
|
||||
|
||||
<h1>@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Edit") Tax Rate</h1>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.StripeTaxRateId))
|
||||
{
|
||||
<p>Note: Updating a Tax Rate archives the currently selected rate and creates a new rate with a new ID. The previous data still exists in a disabled state.</p>
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<input type="hidden" asp-for="StripeTaxRateId">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Country"></label>
|
||||
<select asp-for="Country" class="form-control" required>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="US">United States</option>
|
||||
<option value="CN">China</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="AU">Australia</option>
|
||||
<option value="IN">India</option>
|
||||
<option value="-" disabled></option>
|
||||
<option value="AF">Afghanistan</option>
|
||||
<option value="AX">Åland Islands</option>
|
||||
<option value="AL">Albania</option>
|
||||
<option value="DZ">Algeria</option>
|
||||
<option value="AS">American Samoa</option>
|
||||
<option value="AD">Andorra</option>
|
||||
<option value="AO">Angola</option>
|
||||
<option value="AI">Anguilla</option>
|
||||
<option value="AQ">Antarctica</option>
|
||||
<option value="AG">Antigua and Barbuda</option>
|
||||
<option value="AR">Argentina</option>
|
||||
<option value="AM">Armenia</option>
|
||||
<option value="AW">Aruba</option>
|
||||
<option value="AT">Austria</option>
|
||||
<option value="AZ">Azerbaijan</option>
|
||||
<option value="BS">Bahamas</option>
|
||||
<option value="BH">Bahrain</option>
|
||||
<option value="BD">Bangladesh</option>
|
||||
<option value="BB">Barbados</option>
|
||||
<option value="BY">Belarus</option>
|
||||
<option value="BE">Belgium</option>
|
||||
<option value="BZ">Belize</option>
|
||||
<option value="BJ">Benin</option>
|
||||
<option value="BM">Bermuda</option>
|
||||
<option value="BT">Bhutan</option>
|
||||
<option value="BO">Bolivia, Plurinational State of</option>
|
||||
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
|
||||
<option value="BA">Bosnia and Herzegovina</option>
|
||||
<option value="BW">Botswana</option>
|
||||
<option value="BV">Bouvet Island</option>
|
||||
<option value="BR">Brazil</option>
|
||||
<option value="IO">British Indian Ocean Territory</option>
|
||||
<option value="BN">Brunei Darussalam</option>
|
||||
<option value="BG">Bulgaria</option>
|
||||
<option value="BF">Burkina Faso</option>
|
||||
<option value="BI">Burundi</option>
|
||||
<option value="KH">Cambodia</option>
|
||||
<option value="CM">Cameroon</option>
|
||||
<option value="CV">Cape Verde</option>
|
||||
<option value="KY">Cayman Islands</option>
|
||||
<option value="CF">Central African Republic</option>
|
||||
<option value="TD">Chad</option>
|
||||
<option value="CL">Chile</option>
|
||||
<option value="CX">Christmas Island</option>
|
||||
<option value="CC">Cocos (Keeling) Islands</option>
|
||||
<option value="CO">Colombia</option>
|
||||
<option value="KM">Comoros</option>
|
||||
<option value="CG">Congo</option>
|
||||
<option value="CD">Congo, the Democratic Republic of the</option>
|
||||
<option value="CK">Cook Islands</option>
|
||||
<option value="CR">Costa Rica</option>
|
||||
<option value="CI">Côte d'Ivoire</option>
|
||||
<option value="HR">Croatia</option>
|
||||
<option value="CU">Cuba</option>
|
||||
<option value="CW">Curaçao</option>
|
||||
<option value="CY">Cyprus</option>
|
||||
<option value="CZ">Czech Republic</option>
|
||||
<option value="DK">Denmark</option>
|
||||
<option value="DJ">Djibouti</option>
|
||||
<option value="DM">Dominica</option>
|
||||
<option value="DO">Dominican Republic</option>
|
||||
<option value="EC">Ecuador</option>
|
||||
<option value="EG">Egypt</option>
|
||||
<option value="SV">El Salvador</option>
|
||||
<option value="GQ">Equatorial Guinea</option>
|
||||
<option value="ER">Eritrea</option>
|
||||
<option value="EE">Estonia</option>
|
||||
<option value="ET">Ethiopia</option>
|
||||
<option value="FK">Falkland Islands (Malvinas)</option>
|
||||
<option value="FO">Faroe Islands</option>
|
||||
<option value="FJ">Fiji</option>
|
||||
<option value="FI">Finland</option>
|
||||
<option value="GF">French Guiana</option>
|
||||
<option value="PF">French Polynesia</option>
|
||||
<option value="TF">French Southern Territories</option>
|
||||
<option value="GA">Gabon</option>
|
||||
<option value="GM">Gambia</option>
|
||||
<option value="GE">Georgia</option>
|
||||
<option value="GH">Ghana</option>
|
||||
<option value="GI">Gibraltar</option>
|
||||
<option value="GR">Greece</option>
|
||||
<option value="GL">Greenland</option>
|
||||
<option value="GD">Grenada</option>
|
||||
<option value="GP">Guadeloupe</option>
|
||||
<option value="GU">Guam</option>
|
||||
<option value="GT">Guatemala</option>
|
||||
<option value="GG">Guernsey</option>
|
||||
<option value="GN">Guinea</option>
|
||||
<option value="GW">Guinea-Bissau</option>
|
||||
<option value="GY">Guyana</option>
|
||||
<option value="HT">Haiti</option>
|
||||
<option value="HM">Heard Island and McDonald Islands</option>
|
||||
<option value="VA">Holy See (Vatican City State)</option>
|
||||
<option value="HN">Honduras</option>
|
||||
<option value="HK">Hong Kong</option>
|
||||
<option value="HU">Hungary</option>
|
||||
<option value="IS">Iceland</option>
|
||||
<option value="ID">Indonesia</option>
|
||||
<option value="IR">Iran, Islamic Republic of</option>
|
||||
<option value="IQ">Iraq</option>
|
||||
<option value="IE">Ireland</option>
|
||||
<option value="IM">Isle of Man</option>
|
||||
<option value="IL">Israel</option>
|
||||
<option value="IT">Italy</option>
|
||||
<option value="JM">Jamaica</option>
|
||||
<option value="JP">Japan</option>
|
||||
<option value="JE">Jersey</option>
|
||||
<option value="JO">Jordan</option>
|
||||
<option value="KZ">Kazakhstan</option>
|
||||
<option value="KE">Kenya</option>
|
||||
<option value="KI">Kiribati</option>
|
||||
<option value="KP">Korea, Democratic People's Republic of</option>
|
||||
<option value="KR">Korea, Republic of</option>
|
||||
<option value="KW">Kuwait</option>
|
||||
<option value="KG">Kyrgyzstan</option>
|
||||
<option value="LA">Lao People's Democratic Republic</option>
|
||||
<option value="LV">Latvia</option>
|
||||
<option value="LB">Lebanon</option>
|
||||
<option value="LS">Lesotho</option>
|
||||
<option value="LR">Liberia</option>
|
||||
<option value="LY">Libya</option>
|
||||
<option value="LI">Liechtenstein</option>
|
||||
<option value="LT">Lithuania</option>
|
||||
<option value="LU">Luxembourg</option>
|
||||
<option value="MO">Macao</option>
|
||||
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
|
||||
<option value="MG">Madagascar</option>
|
||||
<option value="MW">Malawi</option>
|
||||
<option value="MY">Malaysia</option>
|
||||
<option value="MV">Maldives</option>
|
||||
<option value="ML">Mali</option>
|
||||
<option value="MT">Malta</option>
|
||||
<option value="MH">Marshall Islands</option>
|
||||
<option value="MQ">Martinique</option>
|
||||
<option value="MR">Mauritania</option>
|
||||
<option value="MU">Mauritius</option>
|
||||
<option value="YT">Mayotte</option>
|
||||
<option value="MX">Mexico</option>
|
||||
<option value="FM">Micronesia, Federated States of</option>
|
||||
<option value="MD">Moldova, Republic of</option>
|
||||
<option value="MC">Monaco</option>
|
||||
<option value="MN">Mongolia</option>
|
||||
<option value="ME">Montenegro</option>
|
||||
<option value="MS">Montserrat</option>
|
||||
<option value="MA">Morocco</option>
|
||||
<option value="MZ">Mozambique</option>
|
||||
<option value="MM">Myanmar</option>
|
||||
<option value="NA">Namibia</option>
|
||||
<option value="NR">Nauru</option>
|
||||
<option value="NP">Nepal</option>
|
||||
<option value="NL">Netherlands</option>
|
||||
<option value="NC">New Caledonia</option>
|
||||
<option value="NZ">New Zealand</option>
|
||||
<option value="NI">Nicaragua</option>
|
||||
<option value="NE">Niger</option>
|
||||
<option value="NG">Nigeria</option>
|
||||
<option value="NU">Niue</option>
|
||||
<option value="NF">Norfolk Island</option>
|
||||
<option value="MP">Northern Mariana Islands</option>
|
||||
<option value="NO">Norway</option>
|
||||
<option value="OM">Oman</option>
|
||||
<option value="PK">Pakistan</option>
|
||||
<option value="PW">Palau</option>
|
||||
<option value="PS">Palestinian Territory, Occupied</option>
|
||||
<option value="PA">Panama</option>
|
||||
<option value="PG">Papua New Guinea</option>
|
||||
<option value="PY">Paraguay</option>
|
||||
<option value="PE">Peru</option>
|
||||
<option value="PH">Philippines</option>
|
||||
<option value="PN">Pitcairn</option>
|
||||
<option value="PL">Poland</option>
|
||||
<option value="PT">Portugal</option>
|
||||
<option value="PR">Puerto Rico</option>
|
||||
<option value="QA">Qatar</option>
|
||||
<option value="RE">Réunion</option>
|
||||
<option value="RO">Romania</option>
|
||||
<option value="RU">Russian Federation</option>
|
||||
<option value="RW">Rwanda</option>
|
||||
<option value="BL">Saint Barthélemy</option>
|
||||
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
|
||||
<option value="KN">Saint Kitts and Nevis</option>
|
||||
<option value="LC">Saint Lucia</option>
|
||||
<option value="MF">Saint Martin (French part)</option>
|
||||
<option value="PM">Saint Pierre and Miquelon</option>
|
||||
<option value="VC">Saint Vincent and the Grenadines</option>
|
||||
<option value="WS">Samoa</option>
|
||||
<option value="SM">San Marino</option>
|
||||
<option value="ST">Sao Tome and Principe</option>
|
||||
<option value="SA">Saudi Arabia</option>
|
||||
<option value="SN">Senegal</option>
|
||||
<option value="RS">Serbia</option>
|
||||
<option value="SC">Seychelles</option>
|
||||
<option value="SL">Sierra Leone</option>
|
||||
<option value="SG">Singapore</option>
|
||||
<option value="SX">Sint Maarten (Dutch part)</option>
|
||||
<option value="SK">Slovakia</option>
|
||||
<option value="SI">Slovenia</option>
|
||||
<option value="SB">Solomon Islands</option>
|
||||
<option value="SO">Somalia</option>
|
||||
<option value="ZA">South Africa</option>
|
||||
<option value="GS">South Georgia and the South Sandwich Islands</option>
|
||||
<option value="SS">South Sudan</option>
|
||||
<option value="ES">Spain</option>
|
||||
<option value="LK">Sri Lanka</option>
|
||||
<option value="SD">Sudan</option>
|
||||
<option value="SR">Suriname</option>
|
||||
<option value="SJ">Svalbard and Jan Mayen</option>
|
||||
<option value="SZ">Swaziland</option>
|
||||
<option value="SE">Sweden</option>
|
||||
<option value="CH">Switzerland</option>
|
||||
<option value="SY">Syrian Arab Republic</option>
|
||||
<option value="TW">Taiwan</option>
|
||||
<option value="TJ">Tajikistan</option>
|
||||
<option value="TZ">Tanzania, United Republic of</option>
|
||||
<option value="TH">Thailand</option>
|
||||
<option value="TL">Timor-Leste</option>
|
||||
<option value="TG">Togo</option>
|
||||
<option value="TK">Tokelau</option>
|
||||
<option value="TO">Tonga</option>
|
||||
<option value="TT">Trinidad and Tobago</option>
|
||||
<option value="TN">Tunisia</option>
|
||||
<option value="TR">Turkey</option>
|
||||
<option value="TM">Turkmenistan</option>
|
||||
<option value="TC">Turks and Caicos Islands</option>
|
||||
<option value="TV">Tuvalu</option>
|
||||
<option value="UG">Uganda</option>
|
||||
<option value="UA">Ukraine</option>
|
||||
<option value="AE">United Arab Emirates</option>
|
||||
<option value="UM">United States Minor Outlying Islands</option>
|
||||
<option value="UY">Uruguay</option>
|
||||
<option value="UZ">Uzbekistan</option>
|
||||
<option value="VU">Vanuatu</option>
|
||||
<option value="VE">Venezuela, Bolivarian Republic of</option>
|
||||
<option value="VN">Viet Nam</option>
|
||||
<option value="VG">Virgin Islands, British</option>
|
||||
<option value="VI">Virgin Islands, U.S.</option>
|
||||
<option value="WF">Wallis and Futuna</option>
|
||||
<option value="EH">Western Sahara</option>
|
||||
<option value="YE">Yemen</option>
|
||||
<option value="ZM">Zambia</option>
|
||||
<option value="ZW">Zimbabwe</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="State"></label>
|
||||
<select asp-for="State" class="form-control">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="AL">Alabama</option>
|
||||
<option value="AK">Alaska</option>
|
||||
<option value="AZ">Arizona</option>
|
||||
<option value="AR">Arkansas</option>
|
||||
<option value="CA">California</option>
|
||||
<option value="CO">Colorado</option>
|
||||
<option value="CT">Connecticut</option>
|
||||
<option value="DE">Delaware</option>
|
||||
<option value="DC">District Of Columbia</option>
|
||||
<option value="FL">Florida</option>
|
||||
<option value="GA">Georgia</option>
|
||||
<option value="HI">Hawaii</option>
|
||||
<option value="ID">Idaho</option>
|
||||
<option value="IL">Illinois</option>
|
||||
<option value="IN">Indiana</option>
|
||||
<option value="IA">Iowa</option>
|
||||
<option value="KS">Kansas</option>
|
||||
<option value="KY">Kentucky</option>
|
||||
<option value="LA">Louisiana</option>
|
||||
<option value="ME">Maine</option>
|
||||
<option value="MD">Maryland</option>
|
||||
<option value="MA">Massachusetts</option>
|
||||
<option value="MI">Michigan</option>
|
||||
<option value="MN">Minnesota</option>
|
||||
<option value="MS">Mississippi</option>
|
||||
<option value="MO">Missouri</option>
|
||||
<option value="MT">Montana</option>
|
||||
<option value="NE">Nebraska</option>
|
||||
<option value="NV">Nevada</option>
|
||||
<option value="NH">New Hampshire</option>
|
||||
<option value="NJ">New Jersey</option>
|
||||
<option value="NM">New Mexico</option>
|
||||
<option value="NY">New York</option>
|
||||
<option value="NC">North Carolina</option>
|
||||
<option value="ND">North Dakota</option>
|
||||
<option value="OH">Ohio</option>
|
||||
<option value="OK">Oklahoma</option>
|
||||
<option value="OR">Oregon</option>
|
||||
<option value="PA">Pennsylvania</option>
|
||||
<option value="RI">Rhode Island</option>
|
||||
<option value="SC">South Carolina</option>
|
||||
<option value="SD">South Dakota</option>
|
||||
<option value="TN">Tennessee</option>
|
||||
<option value="TX">Texas</option>
|
||||
<option value="UT">Utah</option>
|
||||
<option value="VT">Vermont</option>
|
||||
<option value="VA">Virginia</option>
|
||||
<option value="WA">Washington</option>
|
||||
<option value="WV">West Virginia</option>
|
||||
<option value="WI">Wisconsin</option>
|
||||
<option value="WY">Wyoming</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="PostalCode">Postal Code</label>
|
||||
<input type="text" class="form-control" asp-for="PostalCode" required maxlength="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Rate">Tax Rate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="Rate" pattern="^\d{0,3}.\d{0,3}$" required>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Save")</button>
|
||||
</form>
|
@ -1,11 +1,15 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@inject IWebHostEnvironment HostingEnvironment
|
||||
@model UserEditModel
|
||||
@{
|
||||
ViewData["Title"] = "User: " + Model.User.Email;
|
||||
|
||||
var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View);
|
||||
var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) &&
|
||||
GlobalSettings.EnableNewDeviceVerification;
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View);
|
||||
var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View);
|
||||
var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View);
|
||||
@ -47,13 +51,13 @@
|
||||
|
||||
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment()
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")';
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")';
|
||||
window.open(`${url}/customers/${customerId.value}/`, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment()
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
window.open(`${url}/${customerId.value}`, '_blank');
|
||||
}
|
||||
});
|
||||
@ -67,13 +71,13 @@
|
||||
|
||||
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")'
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")'
|
||||
window.open(`${url}/subscriptions/${subId.value}`, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
window.open(`${url}/subscriptions/${subId.value}`, '_blank');
|
||||
}
|
||||
});
|
||||
@ -88,11 +92,40 @@
|
||||
<h2>User Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model.User)
|
||||
}
|
||||
@if (canViewNewDeviceException)
|
||||
{
|
||||
<h2>New Device Verification </h2>
|
||||
<dl class="row">
|
||||
<dt class="col d-flex">
|
||||
<form asp-action="ToggleNewDeviceVerification" asp-route-id="@Model.User.Id" method="post">
|
||||
@if (Model.ActiveNewDeviceVerificationException)
|
||||
{
|
||||
<p>Status: Bypassed</p>
|
||||
<button type="submit" class="btn btn-success" id="new-device-verification-exception">Require New
|
||||
Device Verification</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Status: Required</p>
|
||||
<button type="submit" class="btn btn-outline-danger" id="new-device-verification-exception">Bypass New
|
||||
Device Verification</button>
|
||||
}
|
||||
</form>
|
||||
|
||||
</dt>
|
||||
</dl>
|
||||
}
|
||||
@if (canViewBillingInformation)
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, UserId = Model.User.Id, Entity = "User" })
|
||||
new BillingInformationModel
|
||||
{
|
||||
BillingInfo = Model.BillingInfo,
|
||||
BillingHistoryInfo = Model.BillingHistoryInfo,
|
||||
UserId = Model.User.Id,
|
||||
Entity = "User"
|
||||
})
|
||||
}
|
||||
@if (canViewGeneral)
|
||||
{
|
||||
@ -109,7 +142,7 @@
|
||||
<label class="form-check-label" asp-for="EmailVerified"></label>
|
||||
</div>
|
||||
}
|
||||
<form method="post" id="edit-form">
|
||||
<form method="post" id="edit-form">
|
||||
@if (canViewPremium)
|
||||
{
|
||||
<h2>Premium</h2>
|
||||
@ -139,54 +172,56 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="PremiumExpirationDate" class="form-label"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'>
|
||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate"
|
||||
readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (canViewBilling)
|
||||
{
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
@if (canViewBilling)
|
||||
{
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId"
|
||||
readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
|
@ -12,9 +12,10 @@
|
||||
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
||||
|
||||
@if(Model.DomainVerified.HasValue){
|
||||
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
|
||||
@if(Model.ClaimedAccount.HasValue)
|
||||
{
|
||||
<dt class="col-sm-4 col-lg-3">Claimed Account</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.ClaimedAccount.Value ? "Yes" : "No")</dd>
|
||||
}
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||
|
647
src/Admin/package-lock.json
generated
647
src/Admin/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user