mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
merge branch 'master' into 'encrypted-string-perf'
This commit is contained in:
parent
e0b1f9544b
commit
a20e127c9c
36
.github/workflows/build.yml
vendored
36
.github/workflows/build.yml
vendored
@ -4,10 +4,10 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- 'l10n_master'
|
||||
- 'gh-pages'
|
||||
- "l10n_master"
|
||||
- "gh-pages"
|
||||
paths-ignore:
|
||||
- '.github/workflows/**'
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
|
||||
@ -27,7 +27,6 @@ jobs:
|
||||
- name: Print lines of code
|
||||
run: cloc --include-lang C#,SQL,Razor,"Bourne Shell",PowerShell,HTML,CSS,Sass,JavaScript,TypeScript --vcs git
|
||||
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-20.04
|
||||
@ -38,7 +37,6 @@ jobs:
|
||||
- name: Verify Format
|
||||
run: dotnet format --verify-no-changes
|
||||
|
||||
|
||||
testing:
|
||||
name: Testing
|
||||
runs-on: windows-2022
|
||||
@ -48,7 +46,7 @@ jobs:
|
||||
- name: Set up dotnet
|
||||
uses: actions/setup-dotnet@9211491ffb35dd6a6657ca4f45d43dfe6e97c829
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: "6.0.x"
|
||||
- name: Set up MSBuild
|
||||
uses: microsoft/setup-msbuild@ab534842b4bdf384b8aaf93765dc6f721d9f5fab
|
||||
|
||||
@ -83,7 +81,6 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test/Commercial.Core.Test --configuration Debug --no-build
|
||||
shell: pwsh
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: Build artifacts
|
||||
runs-on: ubuntu-20.04
|
||||
@ -126,11 +123,11 @@ jobs:
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0
|
||||
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: '**/package-lock.json'
|
||||
node-version: '16'
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
node-version: "16"
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -176,7 +173,6 @@ jobs:
|
||||
path: ${{ matrix.base_path }}/${{ matrix.service_name }}/${{ matrix.service_name }}.zip
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
build-docker:
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-20.04
|
||||
@ -321,13 +317,13 @@ jobs:
|
||||
github.ref == 'refs/heads/rc' ||
|
||||
github.ref == 'refs/heads/hotfix-rc')
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "docker-password,
|
||||
docker-username,
|
||||
dct-delegate-2-repo-passphrase,
|
||||
dct-delegate-2-key"
|
||||
docker-username,
|
||||
dct-delegate-2-repo-passphrase,
|
||||
dct-delegate-2-key"
|
||||
|
||||
- name: Log into Docker
|
||||
if: |
|
||||
@ -385,7 +381,6 @@ jobs:
|
||||
docker logout
|
||||
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
||||
|
||||
|
||||
upload:
|
||||
name: Upload
|
||||
runs-on: ubuntu-20.04
|
||||
@ -454,7 +449,7 @@ jobs:
|
||||
cd ../..
|
||||
env:
|
||||
ASPNETCORE_ENVIRONMENT: Production
|
||||
swaggerGen: 'True'
|
||||
swaggerGen: "True"
|
||||
DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
|
||||
|
||||
- name: Upload Swagger artifact
|
||||
@ -464,7 +459,6 @@ jobs:
|
||||
path: swagger.json
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
check-failures:
|
||||
name: Check for failures
|
||||
if: always()
|
||||
@ -512,14 +506,14 @@ jobs:
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: Azure/get-keyvault-secrets@b5c723b9ac7870c022b8c35befe620b7009b336f
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||
if: failure()
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Notify Slack on failure
|
||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33 # v1.2.2
|
||||
uses: act10ns/slack@da3191ebe2e67f49b46880b4633f5591a96d1d33
|
||||
if: failure()
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
|
||||
|
2
.github/workflows/qa-deploy.yml
vendored
2
.github/workflows/qa-deploy.yml
vendored
@ -101,7 +101,7 @@ jobs:
|
||||
description: 'Deploy from ${{ env.branch_name }} branch'
|
||||
|
||||
- name: Download latest ${{ matrix.name }} asset from ${{ env.branch_name }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||
env:
|
||||
branch_name: ${{ steps.setup.outputs.branch_name }}
|
||||
with:
|
||||
|
56
.github/workflows/release.yml
vendored
56
.github/workflows/release.yml
vendored
@ -87,7 +87,7 @@ jobs:
|
||||
|
||||
- name: Download latest Release ${{ matrix.name }} asset
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Download latest Release ${{ matrix.name }} asset
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@ -179,21 +179,38 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- service_name: Admin
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Api
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Attachments
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Events
|
||||
prod_acr: true
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: EventsProcessor
|
||||
prod_acr: true
|
||||
origin_docker_repo: bitwardenqa.azurecr.io
|
||||
- service_name: Icons
|
||||
origin_docker_repo: bitwarden
|
||||
prod_acr: true
|
||||
- service_name: Identity
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: K8S-Proxy
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: MsSql
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Nginx
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Notifications
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Server
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Setup
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Sso
|
||||
origin_docker_repo: bitwarden
|
||||
- service_name: Scim
|
||||
origin_docker_repo: bitwarden
|
||||
skip_dct: true
|
||||
steps:
|
||||
- name: Print environment
|
||||
@ -220,6 +237,7 @@ jobs:
|
||||
########## DockerHub ##########
|
||||
- name: Setup DCT
|
||||
id: setup-dct
|
||||
if: matrix.origin_docker_repo == 'bitwarden'
|
||||
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
|
||||
with:
|
||||
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
@ -227,6 +245,7 @@ jobs:
|
||||
|
||||
- name: Check for DCT value
|
||||
id: check-matrix-dct
|
||||
if: matrix.origin_docker_repo == 'bitwarden'
|
||||
run: |
|
||||
if [[ "${{ matrix.skip_dct }}" == "true" ]]; then
|
||||
echo "::set-output name=dct_enabled::0"
|
||||
@ -235,6 +254,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Pull latest selfhost image
|
||||
if: matrix.origin_docker_repo == 'bitwarden'
|
||||
env:
|
||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
||||
run: |
|
||||
@ -245,6 +265,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Tag version and latest
|
||||
if: matrix.origin_docker_repo == 'bitwarden'
|
||||
env:
|
||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
||||
run: |
|
||||
@ -255,7 +276,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Push version and latest image
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' && matrix.origin_docker_repo == 'bitwarden' }}
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST: ${{ steps.check-matrix-dct.outputs.dct_enabled }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
|
||||
@ -264,6 +285,7 @@ jobs:
|
||||
docker push bitwarden/$SERVICE_NAME:$_RELEASE_VERSION
|
||||
|
||||
- name: Log out of Docker and disable Docker Notary
|
||||
if: matrix.origin_docker_repo == 'bitwarden'
|
||||
run: |
|
||||
docker logout
|
||||
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
|
||||
@ -277,15 +299,28 @@ jobs:
|
||||
- name: Login to Azure ACR
|
||||
run: az acr login -n bitwardenqa
|
||||
|
||||
- name: Tag version and latest
|
||||
- name: Pull latest selfhost image
|
||||
if: matrix.origin_docker_repo == 'bitwardenqa.azurecr.io'
|
||||
env:
|
||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
||||
REGISTRY: bitwardenqa.azurecr.io
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||
docker tag bitwarden/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun
|
||||
docker pull $REGISTRY/$SERVICE_NAME:latest
|
||||
else
|
||||
docker tag bitwarden/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION
|
||||
docker pull $REGISTRY/$SERVICE_NAME:$_BRANCH_NAME
|
||||
fi
|
||||
|
||||
- name: Tag version and latest
|
||||
env:
|
||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
||||
REGISTRY: bitwardenqa.azurecr.io
|
||||
ORIGIN_REGISTY: ${{ matrix.origin_docker_repo }}
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||
docker tag $ORIGIN_REGISTY/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun
|
||||
else
|
||||
docker tag $ORIGIN_REGISTY/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION
|
||||
fi
|
||||
|
||||
- name: Push version and latest image
|
||||
@ -315,11 +350,12 @@ jobs:
|
||||
env:
|
||||
SERVICE_NAME: ${{ steps.setup.outputs.service_name }}
|
||||
REGISTRY: bitwardenprod.azurecr.io
|
||||
ORIGIN_REGISTY: ${{ matrix.origin_docker_repo }}
|
||||
run: |
|
||||
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
|
||||
docker tag bitwarden/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun
|
||||
docker tag $ORIGIN_REGISTY/$SERVICE_NAME:latest $REGISTRY/$SERVICE_NAME:dryrun
|
||||
else
|
||||
docker tag bitwarden/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION
|
||||
docker tag $ORIGIN_REGISTY/$SERVICE_NAME:$_BRANCH_NAME $REGISTRY/$SERVICE_NAME:$_RELEASE_VERSION
|
||||
fi
|
||||
|
||||
- name: Push version and latest image
|
||||
@ -344,7 +380,7 @@ jobs:
|
||||
steps:
|
||||
- name: Download latest Release docker-stub
|
||||
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
@ -355,7 +391,7 @@ jobs:
|
||||
|
||||
- name: Download latest Release docker-stub
|
||||
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
|
||||
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
|
||||
uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a
|
||||
with:
|
||||
workflow: build.yml
|
||||
workflow_conclusion: success
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<!--2022.6.2-->
|
||||
<Version>2022.9.0</Version>
|
||||
<Version>2022.9.5</Version>
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -98,6 +98,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{EC2D422A-6060-48E2-AAD2-37220D759F03}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroBenchmarks", "perf\MicroBenchmarks\MicroBenchmarks.csproj", "{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}"
|
||||
@ -246,6 +248,10 @@ Global
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -285,6 +291,7 @@ Global
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61} = {EC2D422A-6060-48E2-AAD2-37220D759F03}
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -5,6 +5,8 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Queries.Users.Interfaces;
|
||||
using Bit.Scim.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
@ -13,6 +15,7 @@ namespace Bit.Scim.Controllers.v2;
|
||||
|
||||
[Authorize("Scim")]
|
||||
[Route("v2/{organizationId}/users")]
|
||||
[ExceptionHandlerFilter]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
@ -21,6 +24,7 @@ public class UsersController : Controller
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IScimContext _scimContext;
|
||||
private readonly ScimSettings _scimSettings;
|
||||
private readonly IGetUserQuery _getUserQuery;
|
||||
private readonly ILogger<UsersController> _logger;
|
||||
|
||||
public UsersController(
|
||||
@ -30,6 +34,7 @@ public class UsersController : Controller
|
||||
IOrganizationService organizationService,
|
||||
IScimContext scimContext,
|
||||
IOptions<ScimSettings> scimSettings,
|
||||
IGetUserQuery getUserQuery,
|
||||
ILogger<UsersController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
@ -38,22 +43,15 @@ public class UsersController : Controller
|
||||
_organizationService = organizationService;
|
||||
_scimContext = scimContext;
|
||||
_scimSettings = scimSettings?.Value;
|
||||
_getUserQuery = getUserQuery;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(Guid organizationId, Guid id)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
return new NotFoundObjectResult(new ScimErrorResponseModel
|
||||
{
|
||||
Status = 404,
|
||||
Detail = "User not found."
|
||||
});
|
||||
}
|
||||
return new ObjectResult(new ScimUserResponseModel(orgUser));
|
||||
var scimUserResponseModel = await _getUserQuery.GetUserAsync(organizationId, id);
|
||||
return Ok(scimUserResponseModel);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -262,7 +260,7 @@ public class UsersController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete(Guid organizationId, Guid id, [FromBody] ScimUserRequestModel model)
|
||||
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Scim;
|
||||
|
||||
@ -13,7 +12,7 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
|
||||
@ -24,7 +23,7 @@ public class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= LogEventLevel.Warning;
|
||||
return e.Level >= globalSettings.MinLogLevel.ScimSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
27
bitwarden_license/src/Scim/Queries/Users/GetUserQuery.cs
Normal file
27
bitwarden_license/src/Scim/Queries/Users/GetUserQuery.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Models;
|
||||
using Bit.Scim.Queries.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Queries.Users;
|
||||
|
||||
public class GetUserQuery : IGetUserQuery
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public GetUserQuery(IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
public async Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException("User not found.");
|
||||
}
|
||||
|
||||
return new ScimUserResponseModel(orgUser);
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Scim.Models;
|
||||
|
||||
namespace Bit.Scim.Queries.Users.Interfaces;
|
||||
|
||||
public interface IGetUserQuery
|
||||
{
|
||||
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
|
||||
}
|
@ -75,6 +75,8 @@ public class Startup
|
||||
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
|
||||
});
|
||||
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
|
||||
|
||||
services.AddScimUserQueries();
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
@ -0,0 +1,35 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Scim.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Bit.Scim.Utilities;
|
||||
|
||||
public class ExceptionHandlerFilterAttribute : ExceptionFilterAttribute
|
||||
{
|
||||
public override void OnException(ExceptionContext context)
|
||||
{
|
||||
var exception = context.Exception;
|
||||
if (exception == null)
|
||||
{
|
||||
// Should never happen.
|
||||
return;
|
||||
}
|
||||
|
||||
int statusCode = StatusCodes.Status500InternalServerError;
|
||||
var scimErrorResponseModel = new ScimErrorResponseModel
|
||||
{
|
||||
Detail = exception.Message
|
||||
};
|
||||
|
||||
if (exception is NotFoundException)
|
||||
{
|
||||
statusCode = StatusCodes.Status404NotFound;
|
||||
}
|
||||
|
||||
scimErrorResponseModel.Status = statusCode;
|
||||
|
||||
context.HttpContext.Response.StatusCode = statusCode;
|
||||
context.Result = new ObjectResult(scimErrorResponseModel);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using Bit.Scim.Queries.Users;
|
||||
using Bit.Scim.Queries.Users.Interfaces;
|
||||
|
||||
namespace Bit.Scim.Utilities;
|
||||
|
||||
public static class ScimServiceCollectionExtensions
|
||||
{
|
||||
public static void AddScimUserQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IGetUserQuery, GetUserQuery>();
|
||||
}
|
||||
}
|
@ -483,9 +483,9 @@ public class AccountController : Controller
|
||||
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
|
||||
if (orgUser == null && organization.Seats.HasValue)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId);
|
||||
var occupiedSeats = await _organizationService.GetOccupiedSeatCount(organization);
|
||||
var initialSeatCount = organization.Seats.Value;
|
||||
var availableSeats = initialSeatCount - userCount;
|
||||
var availableSeats = initialSeatCount - occupiedSeats;
|
||||
var prorationDate = DateTime.UtcNow;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Sso.IdentityServer;
|
||||
|
||||
public class OidcIdentityClient : Client
|
||||
{
|
||||
@ -11,8 +11,8 @@ public class OidcIdentityClient : Client
|
||||
ClientId = "oidc-identity";
|
||||
RequireClientSecret = true;
|
||||
RequirePkce = true;
|
||||
ClientSecrets = new List<Secret> { new Secret(globalSettings.OidcIdentityClientKey.Sha256()) };
|
||||
AllowedScopes = new string[]
|
||||
ClientSecrets = new List<Secret> { new(globalSettings.OidcIdentityClientKey.Sha256()) };
|
||||
AllowedScopes = new[]
|
||||
{
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
IdentityServerConstants.StandardScopes.Profile
|
@ -1,6 +1,5 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Sso;
|
||||
|
||||
@ -15,7 +14,7 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Properties.ContainsKey("RequestPath") &&
|
||||
@ -24,7 +23,7 @@ public class Program
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return e.Level >= LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.SsoSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Bit.Core.Business.Sso;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Bit.Sso.IdentityServer;
|
||||
using Bit.Sso.Models;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.ResponseHandling;
|
||||
|
@ -0,0 +1,66 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Queries.Users;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Scim.Test.Queries.Users;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetUserQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUser_Success(SutProvider<GetUserQuery> sutProvider, OrganizationUserUserDetails organizationUserUserDetails)
|
||||
{
|
||||
var expectedResult = new Models.ScimUserResponseModel
|
||||
{
|
||||
Id = organizationUserUserDetails.Id.ToString(),
|
||||
UserName = organizationUserUserDetails.Email,
|
||||
Name = new Models.BaseScimUserModel.NameModel(organizationUserUserDetails.Name),
|
||||
Emails = new List<Models.BaseScimUserModel.EmailModel> { new Models.BaseScimUserModel.EmailModel(organizationUserUserDetails.Email) },
|
||||
DisplayName = organizationUserUserDetails.Name,
|
||||
Active = organizationUserUserDetails.Status != Core.Enums.OrganizationUserStatusType.Revoked ? true : false,
|
||||
Groups = new List<string>(),
|
||||
ExternalId = organizationUserUserDetails.ExternalId,
|
||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetDetailsByIdAsync(organizationUserUserDetails.Id)
|
||||
.Returns(organizationUserUserDetails);
|
||||
|
||||
var result = await sutProvider.Sut.GetUserAsync(organizationUserUserDetails.OrganizationId, organizationUserUserDetails.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(organizationUserUserDetails.Id);
|
||||
AssertHelper.AssertPropertyEqual(expectedResult, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUser_NotFound_Throws(SutProvider<GetUserQuery> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetUserAsync(organizationId, organizationUserId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetUser_MismatchingOrganizationId_Throws(SutProvider<GetUserQuery> sutProvider, Guid organizationId, Guid organizationUserId)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUserId)
|
||||
.Returns(new OrganizationUser
|
||||
{
|
||||
Id = organizationUserId,
|
||||
OrganizationId = Guid.NewGuid()
|
||||
});
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetUserAsync(organizationId, organizationUserId));
|
||||
}
|
||||
}
|
25
bitwarden_license/test/Scim.Test/Scim.Test.csproj
Normal file
25
bitwarden_license/test/Scim.Test/Scim.Test.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstitueVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Scim\Scim.csproj" />
|
||||
<ProjectReference Include="..\..\..\test\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
3249
bitwarden_license/test/Scim.Test/packages.lock.json
Normal file
3249
bitwarden_license/test/Scim.Test/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,6 @@ version: "3.9"
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/azure-sql-edge:latest
|
||||
restart: always
|
||||
environment:
|
||||
ACCEPT_EULA: Y
|
||||
MSSQL_SA_PASSWORD: ${MSSQL_PASSWORD}
|
||||
@ -41,7 +40,6 @@ services:
|
||||
|
||||
postgres:
|
||||
image: postgres:14
|
||||
restart: always
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
@ -58,7 +56,6 @@ services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
container_name: bw-mysql
|
||||
restart: always
|
||||
ports:
|
||||
- "3306:3306"
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
|
@ -33,22 +33,30 @@ BEGIN
|
||||
CREATE DATABASE $DATABASE;
|
||||
END;
|
||||
|
||||
GO
|
||||
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
|
||||
BEGIN
|
||||
CREATE DATABASE migrations_$DATABASE;
|
||||
END;
|
||||
|
||||
GO
|
||||
IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
|
||||
"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
echo "Return code: $?"
|
||||
|
||||
# Create migrations table if it does not already exist
|
||||
QUERY="IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
|
||||
[Id] INT IDENTITY(1,1) PRIMARY KEY,
|
||||
[Filename] NVARCHAR(MAX) NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NULL,
|
||||
);
|
||||
END;"
|
||||
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
END;
|
||||
GO
|
||||
"
|
||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||
echo "Return code: $?"
|
||||
|
||||
should_migrate () {
|
||||
local file=$(basename $1)
|
||||
|
25
renovate.json
Normal file
25
renovate.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:monthly",
|
||||
":maintainLockFilesMonthly",
|
||||
":preserveSemverRanges",
|
||||
":rebaseStalePrs",
|
||||
":disableMajorUpdates"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"nuget"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchManagers": ["nuget"],
|
||||
"groupName": "Nuget updates",
|
||||
"groupSlug": "nuget",
|
||||
"matchUpdateTypes": [
|
||||
"minor",
|
||||
"patch"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Admin.IdentityServer;
|
||||
using Bit.Admin.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -1,11 +1,9 @@
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public class PasswordlessSignInManager<TUser> : SignInManager<TUser> where TUser : class
|
||||
{
|
@ -1,8 +1,7 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
@ -14,7 +13,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var usersCsv = _configuration["adminSettings:admins"];
|
||||
if (!CoreHelpers.SettingHasValue(usersCsv))
|
||||
@ -59,7 +58,7 @@ public class ReadOnlyEnvIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
}
|
||||
|
||||
public override Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FindByEmailAsync(userId, cancellationToken);
|
||||
}
|
@ -1,108 +1,107 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public abstract class ReadOnlyIdentityUserStore :
|
||||
IUserStore<IdentityUser>,
|
||||
IUserEmailStore<IdentityUser>,
|
||||
IUserSecurityStampStore<IdentityUser>
|
||||
{
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<IdentityResult> CreateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> DeleteAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public abstract Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public abstract Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken));
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await FindByEmailAsync(normalizedUserName, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<string> GetEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<bool> GetEmailConfirmedAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.EmailConfirmed);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedEmailAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetNormalizedUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task<string> GetUserIdAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Id);
|
||||
}
|
||||
|
||||
public Task<string> GetUserNameAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(user.Email);
|
||||
}
|
||||
|
||||
public Task SetEmailAsync(IdentityUser user, string email,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetEmailConfirmedAsync(IdentityUser user, bool confirmed,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SetNormalizedEmailAsync(IdentityUser user, string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
user.NormalizedEmail = normalizedEmail;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetNormalizedUserNameAsync(IdentityUser user, string normalizedName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
user.NormalizedUserName = normalizedName;
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SetUserNameAsync(IdentityUser user, string userName,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IdentityResult> UpdateAsync(IdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
44
src/Admin/IdentityServer/ServiceCollectionExtensions.cs
Normal file
44
src/Admin/IdentityServer/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Admin.IdentityServer;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static Tuple<IdentityBuilder, IdentityBuilder> AddPasswordlessIdentityServices<TUserStore>(
|
||||
this IServiceCollection services, GlobalSettings globalSettings) where TUserStore : class
|
||||
{
|
||||
services.TryAddTransient<ILookupNormalizer, LowerInvariantLookupNormalizer>();
|
||||
services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
{
|
||||
options.TokenLifespan = TimeSpan.FromMinutes(15);
|
||||
});
|
||||
|
||||
var passwordlessIdentityBuilder = services.AddIdentity<IdentityUser, Role>()
|
||||
.AddUserStore<TUserStore>()
|
||||
.AddRoleStore<RoleStore>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
var regularIdentityBuilder = services.AddIdentityCore<User>()
|
||||
.AddUserStore<UserStore>();
|
||||
|
||||
services.TryAddScoped<PasswordlessSignInManager<IdentityUser>, PasswordlessSignInManager<IdentityUser>>();
|
||||
|
||||
services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/login";
|
||||
options.LogoutPath = "/";
|
||||
options.AccessDeniedPath = "/login?accessDenied=true";
|
||||
options.Cookie.Name = $"Bitwarden_{globalSettings.ProjectName}";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(2);
|
||||
options.ReturnUrlParameter = "returnUrl";
|
||||
options.SlidingExpiration = true;
|
||||
});
|
||||
|
||||
return new Tuple<IdentityBuilder, IdentityBuilder>(passwordlessIdentityBuilder, regularIdentityBuilder);
|
||||
}
|
||||
}
|
27
src/Admin/Jobs/DeleteAuthRequestsJob.cs
Normal file
27
src/Admin/Jobs/DeleteAuthRequestsJob.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Jobs;
|
||||
using Bit.Core.Repositories;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Admin.Jobs;
|
||||
|
||||
public class DeleteAuthRequestsJob : BaseJob
|
||||
{
|
||||
private readonly IAuthRequestRepository _authRepo;
|
||||
|
||||
public DeleteAuthRequestsJob(
|
||||
IAuthRequestRepository authrepo,
|
||||
ILogger<DeleteAuthRequestsJob> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_authRepo = authrepo;
|
||||
}
|
||||
|
||||
protected async override Task ExecuteJobAsync(IJobExecutionContext context)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteAuthRequestsJob: Start");
|
||||
var count = await _authRepo.DeleteExpiredAsync();
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, $"{count} records deleted from AuthRequests.");
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteAuthRequestsJob: End");
|
||||
}
|
||||
}
|
@ -59,6 +59,11 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 0 * * ?")
|
||||
.Build();
|
||||
var everyFifteenMinutesTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("everyFifteenMinutesTrigger")
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 */15 * ? * *")
|
||||
.Build();
|
||||
|
||||
var jobs = new List<Tuple<Type, ITrigger>>
|
||||
{
|
||||
@ -67,7 +72,8 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteCiphersJob), everyDayAtMidnightUtc),
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger)
|
||||
new Tuple<Type, ITrigger>(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger),
|
||||
};
|
||||
|
||||
if (!_globalSettings.SelfHosted)
|
||||
@ -91,5 +97,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
services.AddTransient<DatabaseExpiredSponsorshipsJob>();
|
||||
services.AddTransient<DeleteSendsJob>();
|
||||
services.AddTransient<DeleteCiphersJob>();
|
||||
services.AddTransient<DeleteAuthRequestsJob>();
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ public class OrganizationViewModel
|
||||
UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited);
|
||||
UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted);
|
||||
UserConfirmedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Confirmed);
|
||||
UserCount = orgUsers.Count();
|
||||
OccupiedSeatCount = orgUsers.Count(u => u.OccupiesOrganizationSeat);
|
||||
CipherCount = ciphers.Count();
|
||||
CollectionCount = collections.Count();
|
||||
GroupCount = groups?.Count() ?? 0;
|
||||
@ -40,7 +40,7 @@ public class OrganizationViewModel
|
||||
public int UserInvitedCount { get; set; }
|
||||
public int UserConfirmedCount { get; set; }
|
||||
public int UserAcceptedCount { get; set; }
|
||||
public int UserCount { get; set; }
|
||||
public int OccupiedSeatCount { get; set; }
|
||||
public int CipherCount { get; set; }
|
||||
public int CollectionCount { get; set; }
|
||||
public int GroupCount { get; set; }
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Admin;
|
||||
|
||||
@ -18,7 +17,7 @@ public class Program
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Properties.ContainsKey("RequestPath") &&
|
||||
@ -27,7 +26,7 @@ public class Program
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return e.Level >= LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.AdminSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Bit.Admin.IdentityServer;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Users</dt>
|
||||
<dd class="col-sm-8 col-lg-9">
|
||||
@Model.UserCount / @(Model.Organization.Seats?.ToString() ?? "-")
|
||||
@Model.OccupiedSeatCount / @(Model.Organization.Seats?.ToString() ?? "-")
|
||||
(<span title="Invited">@Model.UserInvitedCount</span> /
|
||||
<span title="Accepted">@Model.UserAcceptedCount</span> /
|
||||
<span title="Confirmed">@Model.UserConfirmedCount</span>)
|
||||
|
@ -35,6 +35,7 @@ public class AccountsController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly ISendService _sendService;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
|
||||
public AccountsController(
|
||||
GlobalSettings globalSettings,
|
||||
@ -47,7 +48,8 @@ public class AccountsController : Controller
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
ISendRepository sendRepository,
|
||||
ISendService sendService)
|
||||
ISendService sendService,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
@ -60,11 +62,12 @@ public class AccountsController : Controller
|
||||
_userService = userService;
|
||||
_sendRepository = sendRepository;
|
||||
_sendService = sendService;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
}
|
||||
|
||||
#region DEPRECATED (Moved to Identity Service)
|
||||
|
||||
[Obsolete("2022-01-12 Moved to Identity, left for backwards compatability with older clients")]
|
||||
[Obsolete("TDL-136 Moved to Identity (2022-01-12 cloud, 2022-09-19 self-hosted), left for backwards compatability with older clients.")]
|
||||
[HttpPost("prelogin")]
|
||||
[AllowAnonymous]
|
||||
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
|
||||
@ -81,17 +84,19 @@ public class AccountsController : Controller
|
||||
return new PreloginResponseModel(kdfInformation);
|
||||
}
|
||||
|
||||
[Obsolete("2022-01-12 Moved to Identity, left for backwards compatability with older clients")]
|
||||
[Obsolete("TDL-136 Moved to Identity (2022-01-12 cloud, 2022-09-19 self-hosted), left for backwards compatability with older clients.")]
|
||||
[HttpPost("register")]
|
||||
[AllowAnonymous]
|
||||
[CaptchaProtected]
|
||||
public async Task PostRegister([FromBody] RegisterRequestModel model)
|
||||
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
||||
{
|
||||
var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash,
|
||||
var user = model.ToUser();
|
||||
var result = await _userService.RegisterUserAsync(user, model.MasterPasswordHash,
|
||||
model.Token, model.OrganizationUserId);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
return new RegisterResponseModel(captchaBypassToken);
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||
|
146
src/Api/Controllers/AuthRequestsController.cs
Normal file
146
src/Api/Controllers/AuthRequestsController.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("auth-requests")]
|
||||
[Authorize("Application")]
|
||||
public class AuthRequestsController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public AuthRequestsController(
|
||||
IUserRepository userRepository,
|
||||
IDeviceRepository deviceRepository,
|
||||
IUserService userService,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
ICurrentContext currentContext,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_deviceRepository = deviceRepository;
|
||||
_userService = userService;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_currentContext = currentContext;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> Get()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);
|
||||
var responses = authRequests.Select(a => new AuthRequestResponseModel(a, _globalSettings.BaseServiceUri.Vault)).ToList();
|
||||
return new ListResponseModel<AuthRequestResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<AuthRequestResponseModel> Get(string id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || authRequest.UserId != userId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/response")]
|
||||
[AllowAnonymous]
|
||||
public async Task<AuthRequestResponseModel> GetResponse(string id, [FromQuery] string code)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || code != authRequest.AccessCode || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[AllowAnonymous]
|
||||
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!_currentContext.DeviceType.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Device type not provided.");
|
||||
}
|
||||
if (_globalSettings.PasswordlessAuth.KnownDevicesOnly)
|
||||
{
|
||||
var devices = await _deviceRepository.GetManyByUserIdAsync(user.Id);
|
||||
if (devices == null || !devices.Any(d => d.Identifier == model.DeviceIdentifier))
|
||||
{
|
||||
throw new BadRequestException("Login with device is only available on devices that have been previously logged in.");
|
||||
}
|
||||
}
|
||||
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
RequestDeviceIdentifier = model.DeviceIdentifier,
|
||||
RequestDeviceType = _currentContext.DeviceType.Value,
|
||||
RequestIpAddress = _currentContext.IpAddress,
|
||||
AccessCode = model.AccessCode,
|
||||
PublicKey = model.PublicKey,
|
||||
UserId = user.Id,
|
||||
Type = model.Type.Value,
|
||||
RequestFingerprint = model.FingerprintPhrase
|
||||
};
|
||||
authRequest = await _authRequestRepository.CreateAsync(authRequest);
|
||||
await _pushNotificationService.PushAuthRequestAsync(authRequest);
|
||||
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||
return r;
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<AuthRequestResponseModel> Put(string id, [FromBody] AuthRequestUpdateRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier);
|
||||
if (device == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid device.");
|
||||
}
|
||||
|
||||
if (model.RequestApproved)
|
||||
{
|
||||
authRequest.Key = model.Key;
|
||||
authRequest.MasterPasswordHash = model.MasterPasswordHash;
|
||||
authRequest.ResponseDeviceId = device.Id;
|
||||
authRequest.ResponseDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(authRequest);
|
||||
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
|
||||
}
|
||||
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
|
||||
}
|
||||
}
|
@ -16,15 +16,18 @@ public class DevicesController : Controller
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public DevicesController(
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService)
|
||||
IUserService userService,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_deviceRepository = deviceRepository;
|
||||
_deviceService = deviceService;
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -126,4 +129,23 @@ public class DevicesController : Controller
|
||||
|
||||
await _deviceService.DeleteAsync(device);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("knowndevice/{email}/{identifier}")]
|
||||
public async Task<bool> GetByIdentifier(string email, string identifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
throw new BadRequestException("Please provide an email and device identifier");
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByEmailAsync(email);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, user.Id);
|
||||
return device != null;
|
||||
}
|
||||
}
|
||||
|
@ -537,7 +537,7 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("{id}/api-key-information/{type?}")]
|
||||
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id, OrganizationApiKeyType? type)
|
||||
public async Task<ListResponseModel<OrganizationApiKeyInformation>> ApiKeyInformation(Guid id, [FromRoute] OrganizationApiKeyType? type)
|
||||
{
|
||||
if (!await HasApiKeyAccessAsync(id, type))
|
||||
{
|
||||
|
@ -229,6 +229,7 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("get-webauthn-challenge")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Disable Swagger due to CredentialCreateOptions not converting properly
|
||||
public async Task<CredentialCreateOptions> GetWebAuthnChallenge([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
|
32
src/Api/Models/Request/AuthRequestRequestModel.cs
Normal file
32
src/Api/Models/Request/AuthRequestRequestModel.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
public class AuthRequestCreateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Email { get; set; }
|
||||
[Required]
|
||||
public string PublicKey { get; set; }
|
||||
[Required]
|
||||
public string DeviceIdentifier { get; set; }
|
||||
[Required]
|
||||
[StringLength(25)]
|
||||
public string AccessCode { get; set; }
|
||||
[Required]
|
||||
public AuthRequestType? Type { get; set; }
|
||||
[Required]
|
||||
public string FingerprintPhrase { get; set; }
|
||||
}
|
||||
|
||||
public class AuthRequestUpdateRequestModel
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
[Required]
|
||||
public string DeviceIdentifier { get; set; }
|
||||
[Required]
|
||||
public bool RequestApproved { get; set; }
|
||||
}
|
43
src/Api/Models/Response/AuthRequestResponseModel.cs
Normal file
43
src/Api/Models/Response/AuthRequestResponseModel.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
public class AuthRequestResponseModel : ResponseModel
|
||||
{
|
||||
public AuthRequestResponseModel(AuthRequest authRequest, string vaultUri, string obj = "auth-request")
|
||||
: base(obj)
|
||||
{
|
||||
if (authRequest == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(authRequest));
|
||||
}
|
||||
|
||||
Id = authRequest.Id.ToString();
|
||||
PublicKey = authRequest.PublicKey;
|
||||
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
RequestIpAddress = authRequest.RequestIpAddress;
|
||||
RequestFingerprint = authRequest.RequestFingerprint;
|
||||
Key = authRequest.Key;
|
||||
MasterPasswordHash = authRequest.MasterPasswordHash;
|
||||
CreationDate = authRequest.CreationDate;
|
||||
RequestApproved = !string.IsNullOrWhiteSpace(Key) &&
|
||||
(authRequest.Type == AuthRequestType.Unlock || !string.IsNullOrWhiteSpace(MasterPasswordHash));
|
||||
Origin = new Uri(vaultUri).Host;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string RequestDeviceType { get; set; }
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string RequestFingerprint { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool RequestApproved { get; set; }
|
||||
public string Origin { get; set; }
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Api;
|
||||
|
||||
@ -16,7 +15,7 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Exception != null &&
|
||||
@ -26,19 +25,19 @@ public class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.Level == LogEventLevel.Information &&
|
||||
if (
|
||||
context.Contains(typeof(IpRateLimitMiddleware).FullName))
|
||||
{
|
||||
return true;
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IpRateLimit;
|
||||
}
|
||||
|
||||
if (context.Contains("IdentityServer4.Validation.TokenValidator") ||
|
||||
context.Contains("IdentityServer4.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level > LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IdentityToken;
|
||||
}
|
||||
|
||||
return e.Level >= LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
@ -28,7 +28,7 @@ public static class ServiceCollectionExtensions
|
||||
});
|
||||
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
|
||||
|
||||
config.AddSecurityDefinition("OAuth2 Client Credentials", new OpenApiSecurityScheme
|
||||
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Flows = new OpenApiOAuthFlows
|
||||
@ -52,7 +52,7 @@ public static class ServiceCollectionExtensions
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "OAuth2 Client Credentials"
|
||||
Id = "oauth2-client-credentials"
|
||||
},
|
||||
},
|
||||
new[] { "api.organization" }
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Billing;
|
||||
|
||||
@ -13,13 +12,12 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Level == LogEventLevel.Information &&
|
||||
(context.StartsWith("\"Bit.Billing.Jobs") || context.StartsWith("\"Bit.Core.Jobs")))
|
||||
if (context.StartsWith("\"Bit.Billing.Jobs") || context.StartsWith("\"Bit.Core.Jobs"))
|
||||
{
|
||||
return true;
|
||||
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs;
|
||||
}
|
||||
|
||||
if (e.Properties.ContainsKey("RequestPath") &&
|
||||
@ -29,7 +27,7 @@ public class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= LogEventLevel.Warning;
|
||||
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
41
src/Core/Entities/AuthRequest.cs
Normal file
41
src/Core/Entities/AuthRequest.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class AuthRequest : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public Enums.AuthRequestType Type { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string RequestDeviceIdentifier { get; set; }
|
||||
public Enums.DeviceType RequestDeviceType { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string RequestFingerprint { get; set; }
|
||||
public Guid? ResponseDeviceId { get; set; }
|
||||
[MaxLength(25)]
|
||||
public string AccessCode { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ResponseDate { get; set; }
|
||||
public DateTime? AuthenticationDate { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public bool IsSpent()
|
||||
{
|
||||
return ResponseDate.HasValue || AuthenticationDate.HasValue || GetExpirationDate() < DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public DateTime GetExpirationDate()
|
||||
{
|
||||
return CreationDate.AddMinutes(15);
|
||||
}
|
||||
}
|
7
src/Core/Enums/AuthRequestType.cs
Normal file
7
src/Core/Enums/AuthRequestType.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum AuthRequestType : byte
|
||||
{
|
||||
AuthenticateAndUnlock = 0,
|
||||
Unlock = 1
|
||||
}
|
@ -20,4 +20,7 @@ public enum PushType : byte
|
||||
SyncSendCreate = 12,
|
||||
SyncSendUpdate = 13,
|
||||
SyncSendDelete = 14,
|
||||
|
||||
AuthRequest = 15,
|
||||
AuthRequestResponse = 16,
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ namespace Bit.Core.Exceptions;
|
||||
|
||||
public class BadRequestException : Exception
|
||||
{
|
||||
public BadRequestException() : base()
|
||||
{ }
|
||||
|
||||
public BadRequestException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
3
src/Core/Exceptions/ConflictException.cs
Normal file
3
src/Core/Exceptions/ConflictException.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Exceptions;
|
||||
|
||||
public class ConflictException : Exception { }
|
@ -1,3 +1,11 @@
|
||||
namespace Bit.Core.Exceptions;
|
||||
|
||||
public class NotFoundException : Exception { }
|
||||
public class NotFoundException : Exception
|
||||
{
|
||||
public NotFoundException() : base()
|
||||
{ }
|
||||
|
||||
public NotFoundException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
}
|
||||
|
@ -1,38 +0,0 @@
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Identity;
|
||||
|
||||
public class ReadOnlyDatabaseIdentityUserStore : ReadOnlyIdentityUserStore
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public ReadOnlyDatabaseIdentityUserStore(
|
||||
IUserService userService,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByEmailAsync(string normalizedEmail,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(normalizedEmail);
|
||||
return user?.ToIdentityUser(await _userService.TwoFactorIsEnabledAsync(user));
|
||||
}
|
||||
|
||||
public override async Task<IdentityUser> FindByIdAsync(string userId,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
if (!Guid.TryParse(userId, out var userIdGuid))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await _userRepository.GetByIdAsync(userIdGuid);
|
||||
return user?.ToIdentityUser(await _userService.TwoFactorIsEnabledAsync(user));
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.Api.Response.Accounts;
|
||||
|
||||
public interface ICaptchaProtectedResponseModel
|
||||
{
|
||||
public string CaptchaBypassToken { get; set; }
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace Bit.Core.Models.Api.Response.Accounts;
|
||||
|
||||
public class RegisterResponseModel : ResponseModel, ICaptchaProtectedResponseModel
|
||||
{
|
||||
public RegisterResponseModel(string captchaBypassToken)
|
||||
: base("register")
|
||||
{
|
||||
CaptchaBypassToken = captchaBypassToken;
|
||||
}
|
||||
|
||||
public string CaptchaBypassToken { get; set; }
|
||||
}
|
@ -56,4 +56,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
|
||||
{
|
||||
return Premium.GetValueOrDefault(false);
|
||||
}
|
||||
|
||||
public bool OccupiesOrganizationSeat
|
||||
{
|
||||
get
|
||||
{
|
||||
return Status != OrganizationUserStatusType.Revoked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,3 +44,9 @@ public class SyncSendPushNotification
|
||||
public Guid UserId { get; set; }
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
public class AuthRequestPushNotification
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures;
|
||||
|
||||
@ -70,7 +71,8 @@ public static class OrganizationServiceCollectionExtensions
|
||||
new DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>(
|
||||
OrganizationSponsorshipOfferTokenable.ClearTextPrefix,
|
||||
OrganizationSponsorshipOfferTokenable.DataProtectorPurpose,
|
||||
serviceProvider.GetDataProtectionProvider())
|
||||
serviceProvider.GetDataProtectionProvider(),
|
||||
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>>())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
|
||||
@ -71,7 +70,7 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel
|
||||
}
|
||||
var syncedSponsorships = new List<OrganizationSponsorshipData>();
|
||||
|
||||
foreach (var orgSponsorshipsBatch in CoreHelpers.Batch(organizationSponsorshipsDict.Values, 1000))
|
||||
foreach (var orgSponsorshipsBatch in organizationSponsorshipsDict.Values.Chunk(1000))
|
||||
{
|
||||
var response = await SendAsync<OrganizationSponsorshipSyncRequestModel, OrganizationSponsorshipSyncResponseModel>(HttpMethod.Post, "organization/sponsorship/sync", new OrganizationSponsorshipSyncRequestModel
|
||||
{
|
||||
|
9
src/Core/Repositories/IAuthRequestRepository.cs
Normal file
9
src/Core/Repositories/IAuthRequestRepository.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IAuthRequestRepository : IRepository<AuthRequest, Guid>
|
||||
{
|
||||
Task<int> DeleteExpiredAsync();
|
||||
Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);
|
||||
}
|
@ -64,4 +64,5 @@ public interface IOrganizationService
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
Task<int> GetOccupiedSeatCount(Organization organization);
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ public interface IPushNotificationService
|
||||
Task PushSyncSendCreateAsync(Send send);
|
||||
Task PushSyncSendUpdateAsync(Send send);
|
||||
Task PushSyncSendDeleteAsync(Send send);
|
||||
Task PushAuthRequestAsync(AuthRequest authRequest);
|
||||
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
|
||||
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, string deviceId = null);
|
||||
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
|
||||
string deviceId = null);
|
||||
|
@ -130,6 +130,27 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
||||
await SendMessageAsync(type, message, false);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
|
@ -403,7 +403,7 @@ public class CipherService : ICipherService
|
||||
|
||||
var events = deletingCiphers.Select(c =>
|
||||
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Deleted, null));
|
||||
foreach (var eventsBatch in events.Batch(100))
|
||||
foreach (var eventsBatch in events.Chunk(100))
|
||||
{
|
||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||
}
|
||||
@ -574,7 +574,7 @@ public class CipherService : ICipherService
|
||||
|
||||
var events = cipherInfos.Select(c =>
|
||||
new Tuple<Cipher, EventType, DateTime?>(c.cipher, EventType.Cipher_Shared, null));
|
||||
foreach (var eventsBatch in events.Batch(100))
|
||||
foreach (var eventsBatch in events.Chunk(100))
|
||||
{
|
||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||
}
|
||||
@ -791,7 +791,7 @@ public class CipherService : ICipherService
|
||||
|
||||
var events = deletingCiphers.Select(c =>
|
||||
new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_SoftDeleted, null));
|
||||
foreach (var eventsBatch in events.Batch(100))
|
||||
foreach (var eventsBatch in events.Chunk(100))
|
||||
{
|
||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||
}
|
||||
@ -840,7 +840,7 @@ public class CipherService : ICipherService
|
||||
c.DeletedDate = null;
|
||||
return new Tuple<Cipher, EventType, DateTime?>(c, EventType.Cipher_Restored, null);
|
||||
});
|
||||
foreach (var eventsBatch in events.Batch(100))
|
||||
foreach (var eventsBatch in events.Chunk(100))
|
||||
{
|
||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||
}
|
||||
|
@ -133,6 +133,18 @@ public class MultiServicePushNotificationService : IPushNotificationService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
PushToServices((s) => s.PushAuthRequestAsync(authRequest));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
PushToServices((s) => s.PushAuthRequestResponseAsync(authRequest));
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushSyncSendDeleteAsync(Send send)
|
||||
{
|
||||
PushToServices((s) => s.PushSyncSendDeleteAsync(send));
|
||||
|
@ -167,6 +167,27 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
|
||||
{
|
||||
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));
|
||||
|
@ -137,6 +137,27 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
||||
await SendMessageAsync(type, message, false);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendMessageAsync(type, message, true);
|
||||
}
|
||||
|
||||
public async Task PushSyncSendCreateAsync(Send send)
|
||||
{
|
||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
||||
|
@ -44,7 +44,6 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILogger<OrganizationService> _logger;
|
||||
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -199,10 +198,10 @@ public class OrganizationService : IOrganizationService
|
||||
(newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0));
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if (userCount > newPlanSeats)
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
if (occupiedSeats > newPlanSeats)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
$"Your new plan only has ({newPlanSeats}) seats. Remove some users.");
|
||||
}
|
||||
}
|
||||
@ -494,10 +493,10 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if (userCount > newSeatTotal)
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
if (occupiedSeats > newSeatTotal)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
$"Your new plan only has ({newSeatTotal}) seats. Remove some users.");
|
||||
}
|
||||
}
|
||||
@ -861,10 +860,10 @@ public class OrganizationService : IOrganizationService
|
||||
if (license.Seats.HasValue &&
|
||||
(!organization.Seats.HasValue || organization.Seats.Value > license.Seats.Value))
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organization.Id);
|
||||
if (userCount > license.Seats.Value)
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
if (occupiedSeats > license.Seats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Your organization currently has {userCount} seats filled. " +
|
||||
throw new BadRequestException($"Your organization currently has {occupiedSeats} seats filled. " +
|
||||
$"Your new license only has ({license.Seats.Value}) seats. Remove some users.");
|
||||
}
|
||||
}
|
||||
@ -1138,8 +1137,8 @@ public class OrganizationService : IOrganizationService
|
||||
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
||||
var availableSeats = organization.Seats.Value - userCount;
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
var availableSeats = organization.Seats.Value - occupiedSeats;
|
||||
newSeatsRequired = invites.Sum(i => i.invite.Emails.Count()) - existingEmails.Count() - availableSeats;
|
||||
}
|
||||
|
||||
@ -1559,7 +1558,7 @@ public class OrganizationService : IOrganizationService
|
||||
organization.MaxAutoscaleSeats.HasValue &&
|
||||
organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd)
|
||||
{
|
||||
return (false, $"Cannot invite new users. Seat limit has been reached.");
|
||||
return (false, $"Seat limit has been reached.");
|
||||
}
|
||||
|
||||
return (true, failureReason);
|
||||
@ -1951,8 +1950,8 @@ public class OrganizationService : IOrganizationService
|
||||
var enoughSeatsAvailable = true;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(organizationId);
|
||||
seatsAvailable = organization.Seats.Value - userCount;
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
seatsAvailable = organization.Seats.Value - occupiedSeats;
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
|
||||
@ -2324,6 +2323,14 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
await AutoAddSeatsAsync(organization, 1, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
|
||||
|
||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
@ -2345,6 +2352,12 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await GetOccupiedSeatCount(organization);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await AutoAddSeatsAsync(organization, newSeatsRequired, DateTime.UtcNow);
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (restoringUserId.HasValue)
|
||||
{
|
||||
@ -2455,4 +2468,10 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
public async Task<int> GetOccupiedSeatCount(Organization organization)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organization.Id);
|
||||
return orgUsers.Count(ou => ou.OccupiesOrganizationSeat);
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,27 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
||||
}
|
||||
|
||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
||||
}
|
||||
|
||||
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
|
||||
{
|
||||
var message = new AuthRequestPushNotification
|
||||
{
|
||||
Id = authRequest.Id,
|
||||
UserId = authRequest.UserId
|
||||
};
|
||||
|
||||
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
|
||||
}
|
||||
|
||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
|
||||
{
|
||||
var request = new PushSendRequestModel
|
||||
|
@ -81,6 +81,16 @@ public class NoopPushNotificationService : IPushNotificationService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
|
||||
string deviceId = null)
|
||||
{
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Settings;
|
||||
using Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public class GlobalSettings : IGlobalSettings
|
||||
{
|
||||
@ -23,6 +25,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
set => _logDirectory = value;
|
||||
}
|
||||
public virtual long? LogRollBySizeLimit { get; set; }
|
||||
public virtual bool EnableDevLogging { get; set; } = false;
|
||||
public virtual string LicenseDirectory
|
||||
{
|
||||
get => BuildDirectory(_licenseDirectory, "/core/licenses");
|
||||
@ -58,6 +61,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual DocumentDbSettings DocumentDb { get; set; } = new DocumentDbSettings();
|
||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
||||
public virtual NotificationHubSettings NotificationHub { get; set; } = new NotificationHubSettings();
|
||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||
@ -71,6 +75,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual ITwoFactorAuthSettings TwoFactorAuth { get; set; } = new TwoFactorAuthSettings();
|
||||
public virtual DistributedIpRateLimitingSettings DistributedIpRateLimiting { get; set; } =
|
||||
new DistributedIpRateLimitingSettings();
|
||||
public virtual IPasswordlessAuthSettings PasswordlessAuth { get; set; } = new PasswordlessAuthSettings();
|
||||
|
||||
public string BuildExternalUri(string explicitValue, string name)
|
||||
{
|
||||
@ -453,6 +458,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
get => string.IsNullOrWhiteSpace(_apiUri) ? "https://api.bitwarden.com" : _apiUri;
|
||||
set => _apiUri = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AmazonSettings
|
||||
@ -519,4 +525,8 @@ public class GlobalSettings : IGlobalSettings
|
||||
public int SlidingWindowSeconds { get; set; } = 120;
|
||||
}
|
||||
|
||||
public class PasswordlessAuthSettings : IPasswordlessAuthSettings
|
||||
{
|
||||
public bool KnownDevicesOnly { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
@ -15,4 +15,6 @@ public interface IGlobalSettings
|
||||
IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||
ITwoFactorAuthSettings TwoFactorAuth { get; set; }
|
||||
ISsoSettings Sso { get; set; }
|
||||
ILogLevelSettings MinLogLevel { get; set; }
|
||||
IPasswordlessAuthSettings PasswordlessAuth { get; set; }
|
||||
}
|
||||
|
74
src/Core/Settings/ILogLevelSettings.cs
Normal file
74
src/Core/Settings/ILogLevelSettings.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface ILogLevelSettings
|
||||
{
|
||||
IBillingLogLevelSettings BillingSettings { get; set; }
|
||||
IApiLogLevelSettings ApiSettings { get; set; }
|
||||
IIdentityLogLevelSettings IdentitySettings { get; set; }
|
||||
IScimLogLevelSettings ScimSettings { get; set; }
|
||||
ISsoLogLevelSettings SsoSettings { get; set; }
|
||||
IAdminLogLevelSettings AdminSettings { get; set; }
|
||||
IEventsLogLevelSettings EventsSettings { get; set; }
|
||||
IEventsProcessorLogLevelSettings EventsProcessorSettings { get; set; }
|
||||
IIconsLogLevelSettings IconsSettings { get; set; }
|
||||
INotificationsLogLevelSettings NotificationsSettings { get; set; }
|
||||
}
|
||||
|
||||
public interface IBillingLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel Jobs { get; set; }
|
||||
}
|
||||
|
||||
public interface IApiLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
LogEventLevel IpRateLimit { get; set; }
|
||||
}
|
||||
|
||||
public interface IIdentityLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
LogEventLevel IpRateLimit { get; set; }
|
||||
}
|
||||
|
||||
public interface IScimLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface ISsoLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IAdminLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IEventsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
}
|
||||
|
||||
public interface IEventsProcessorLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IIconsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface INotificationsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
}
|
6
src/Core/Settings/IPasswordlessAuthSettings.cs
Normal file
6
src/Core/Settings/IPasswordlessAuthSettings.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface IPasswordlessAuthSettings
|
||||
{
|
||||
bool KnownDevicesOnly { get; set; }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class AdminLogLevelSettings : IAdminLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
10
src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs
Normal file
10
src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class ApiLogLevelSettings : IApiLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
public LogEventLevel IpRateLimit { get; set; } = LogEventLevel.Information;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class BillingLogLevelSettings : IBillingLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
public LogEventLevel Jobs { get; set; } = LogEventLevel.Information;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class EventsLogLevelSettings : IEventsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class EventsProcessorLogLevelSettings : IEventsProcessorLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class IconsLogLevelSettings : IIconsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class IdentityLogLevelSettings : IIdentityLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
public LogEventLevel IpRateLimit { get; set; } = LogEventLevel.Information;
|
||||
}
|
16
src/Core/Settings/LoggingSettings/LogLevelSettings.cs
Normal file
16
src/Core/Settings/LoggingSettings/LogLevelSettings.cs
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class LogLevelSettings : ILogLevelSettings
|
||||
{
|
||||
public IBillingLogLevelSettings BillingSettings { get; set; } = new BillingLogLevelSettings();
|
||||
public IApiLogLevelSettings ApiSettings { get; set; } = new ApiLogLevelSettings();
|
||||
public IIdentityLogLevelSettings IdentitySettings { get; set; } = new IdentityLogLevelSettings();
|
||||
public IScimLogLevelSettings ScimSettings { get; set; } = new ScimLogLevelSettings();
|
||||
public ISsoLogLevelSettings SsoSettings { get; set; } = new SsoLogLevelSettings();
|
||||
public IAdminLogLevelSettings AdminSettings { get; set; } = new AdminLogLevelSettings();
|
||||
public IEventsLogLevelSettings EventsSettings { get; set; } = new EventsLogLevelSettings();
|
||||
public IEventsProcessorLogLevelSettings EventsProcessorSettings { get; set; } = new EventsProcessorLogLevelSettings();
|
||||
public IIconsLogLevelSettings IconsSettings { get; set; } = new IconsLogLevelSettings();
|
||||
public INotificationsLogLevelSettings NotificationsSettings { get; set; } = new NotificationsLogLevelSettings();
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class NotificationsLogLevelSettings : INotificationsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class ScimLogLevelSettings : IScimLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
}
|
8
src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs
Normal file
8
src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class SsoLogLevelSettings : ISsoLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Tokens;
|
||||
|
||||
@ -6,15 +7,17 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
|
||||
{
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly string _clearTextPrefix;
|
||||
private readonly ILogger<DataProtectorTokenFactory<T>> _logger;
|
||||
|
||||
public DataProtectorTokenFactory(string clearTextPrefix, string purpose, IDataProtectionProvider dataProtectionProvider)
|
||||
public DataProtectorTokenFactory(string clearTextPrefix, string purpose, IDataProtectionProvider dataProtectionProvider, ILogger<DataProtectorTokenFactory<T>> logger)
|
||||
{
|
||||
_dataProtector = dataProtectionProvider.CreateProtector(purpose);
|
||||
_clearTextPrefix = clearTextPrefix;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string Protect(T data) =>
|
||||
data.ToToken().ProtectWith(_dataProtector).WithPrefix(_clearTextPrefix).ToString();
|
||||
data.ToToken().ProtectWith(_dataProtector, _logger).WithPrefix(_clearTextPrefix).ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Unprotect token
|
||||
@ -24,7 +27,7 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
|
||||
/// <returns>The parsed tokenable</returns>
|
||||
/// <exception>Throws CryptographicException if fails to unprotect</exception>
|
||||
public T Unprotect(string token) =>
|
||||
Tokenable.FromToken<T>(new Token(token).RemovePrefix(_clearTextPrefix).UnprotectWith(_dataProtector).ToString());
|
||||
Tokenable.FromToken<T>(new Token(token).RemovePrefix(_clearTextPrefix).UnprotectWith(_dataProtector, _logger).ToString());
|
||||
|
||||
public bool TokenValid(string token)
|
||||
{
|
||||
@ -45,8 +48,9 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
|
||||
data = Unprotect(token);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogInformation(ex, "Failed to unprotect token: {rawToken}", token);
|
||||
data = default;
|
||||
return false;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Tokens;
|
||||
|
||||
@ -26,11 +27,28 @@ public class Token
|
||||
return new Token(_token[expectedPrefix.Length..]);
|
||||
}
|
||||
|
||||
public Token ProtectWith(IDataProtector dataProtector) =>
|
||||
new(dataProtector.Protect(ToString()));
|
||||
|
||||
public Token UnprotectWith(IDataProtector dataProtector) =>
|
||||
new(dataProtector.Unprotect(ToString()));
|
||||
public Token ProtectWith(IDataProtector dataProtector, ILogger logger)
|
||||
{
|
||||
logger.LogDebug("Protecting token: {token}", this);
|
||||
return new(dataProtector.Protect(ToString()));
|
||||
}
|
||||
|
||||
public Token UnprotectWith(IDataProtector dataProtector, ILogger logger)
|
||||
{
|
||||
var unprotected = "";
|
||||
try
|
||||
{
|
||||
unprotected = dataProtector.Unprotect(ToString());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogInformation(e, "Failed to unprotect token: {token}", this);
|
||||
throw;
|
||||
}
|
||||
logger.LogDebug("Unprotected token: {token} to {decryptedToken}", this, unprotected);
|
||||
return new(unprotected);
|
||||
}
|
||||
|
||||
public override string ToString() => _token;
|
||||
}
|
||||
|
@ -70,32 +70,6 @@ public static class CoreHelpers
|
||||
return new Guid(guidArray);
|
||||
}
|
||||
|
||||
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
|
||||
{
|
||||
T[] bucket = null;
|
||||
var count = 0;
|
||||
foreach (var item in source)
|
||||
{
|
||||
if (bucket == null)
|
||||
{
|
||||
bucket = new T[size];
|
||||
}
|
||||
bucket[count++] = item;
|
||||
if (count != size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
yield return bucket.Select(x => x);
|
||||
bucket = null;
|
||||
count = 0;
|
||||
}
|
||||
// Return the last bucket with all remaining elements
|
||||
if (bucket != null && count > 0)
|
||||
{
|
||||
yield return bucket.Take(count);
|
||||
}
|
||||
}
|
||||
|
||||
public static string CleanCertificateThumbprint(string thumbprint)
|
||||
{
|
||||
// Clean possible garbage characters from thumbprint copy/paste
|
||||
|
@ -20,7 +20,7 @@ public static class LoggerFactoryExtensions
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
if (env.IsDevelopment() && !globalSettings.EnableDevLogging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -31,9 +31,12 @@ public static class LoggerFactoryExtensions
|
||||
public static ILoggingBuilder AddSerilog(
|
||||
this ILoggingBuilder builder,
|
||||
WebHostBuilderContext context,
|
||||
Func<LogEvent, bool> filter = null)
|
||||
Func<LogEvent, IGlobalSettings, bool> filter = null)
|
||||
{
|
||||
if (context.HostingEnvironment.IsDevelopment())
|
||||
var globalSettings = new GlobalSettings();
|
||||
ConfigurationBinder.Bind(context.Configuration.GetSection("GlobalSettings"), globalSettings);
|
||||
|
||||
if (context.HostingEnvironment.IsDevelopment() && !globalSettings.EnableDevLogging)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
@ -49,13 +52,11 @@ public static class LoggerFactoryExtensions
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return filter(e);
|
||||
return filter(e, globalSettings);
|
||||
}
|
||||
|
||||
var globalSettings = new GlobalSettings();
|
||||
ConfigurationBinder.Bind(context.Configuration.GetSection("GlobalSettings"), globalSettings);
|
||||
|
||||
var config = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.FromLogContext()
|
||||
.Filter.ByIncludingOnly(inclusionPredicate);
|
||||
|
||||
|
@ -3,7 +3,6 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Events.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -95,7 +94,7 @@ public class CollectController : Controller
|
||||
}
|
||||
if (cipherEvents.Any())
|
||||
{
|
||||
foreach (var eventsBatch in cipherEvents.Batch(50))
|
||||
foreach (var eventsBatch in cipherEvents.Chunk(50))
|
||||
{
|
||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Events;
|
||||
|
||||
@ -14,13 +13,13 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.Contains("IdentityServer4.Validation.TokenValidator") ||
|
||||
context.Contains("IdentityServer4.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level > LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.EventsSettings.IdentityToken;
|
||||
}
|
||||
|
||||
if (e.Properties.ContainsKey("RequestPath") &&
|
||||
@ -30,7 +29,7 @@ public class Program
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= LogEventLevel.Error;
|
||||
return e.Level >= globalSettings.MinLogLevel.EventsSettings.Default;
|
||||
}));
|
||||
})
|
||||
.Build()
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.EventsProcessor;
|
||||
|
||||
@ -13,7 +12,7 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e => e.Level >= LogEventLevel.Warning));
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.EventsProcessorSettings.Default));
|
||||
})
|
||||
.Build()
|
||||
.Run();
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Icons;
|
||||
|
||||
@ -13,7 +12,7 @@ public class Program
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, e => e.Level >= LogEventLevel.Error));
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.IconsSettings.Default));
|
||||
})
|
||||
.Build()
|
||||
.Run();
|
||||
|
@ -18,27 +18,32 @@ public class AccountsController : Controller
|
||||
private readonly ILogger<AccountsController> _logger;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
|
||||
public AccountsController(
|
||||
ILogger<AccountsController> logger,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService)
|
||||
IUserService userService,
|
||||
ICaptchaValidationService captchaValidationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
}
|
||||
|
||||
// Moved from API, If you modify this endpoint, please update API as well.
|
||||
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
|
||||
[HttpPost("register")]
|
||||
[CaptchaProtected]
|
||||
public async Task PostRegister([FromBody] RegisterRequestModel model)
|
||||
public async Task<RegisterResponseModel> PostRegister([FromBody] RegisterRequestModel model)
|
||||
{
|
||||
var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash,
|
||||
var user = model.ToUser();
|
||||
var result = await _userService.RegisterUserAsync(user, model.MasterPasswordHash,
|
||||
model.Token, model.OrganizationUserId);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
return new RegisterResponseModel(captchaBypassToken);
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||
@ -50,7 +55,7 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// Moved from API, If you modify this endpoint, please update API as well.
|
||||
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
|
||||
[HttpPost("prelogin")]
|
||||
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
|
||||
{
|
||||
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Settings;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class ApiClient : Client
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class ApiResources
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using IdentityServer4.Models;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class ApiScopes
|
||||
{
|
@ -4,9 +4,8 @@ using IdentityServer4.Models;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using IdentityServer4.Stores.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
// ref: https://raw.githubusercontent.com/IdentityServer/IdentityServer4/3.1.3/src/IdentityServer4/src/Stores/Default/DefaultAuthorizationCodeStore.cs
|
||||
public class AuthorizationCodeStore : DefaultGrantStore<AuthorizationCode>, IAuthorizationCodeStore
|
@ -2,6 +2,7 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -15,9 +16,8 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
@ -10,7 +10,7 @@ using IdentityModel;
|
||||
using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class ClientStore : IClientStore
|
||||
{
|
@ -9,9 +9,8 @@ using IdentityModel;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
ICustomTokenRequestValidator
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class CustomValidatorRequestContext
|
||||
{
|
@ -3,7 +3,7 @@ using IdentityServer4.Models;
|
||||
using IdentityServer4.Stores;
|
||||
using Grant = Bit.Core.Entities.Grant;
|
||||
|
||||
namespace Bit.Core.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public class PersistedGrantStore : IPersistedGrantStore
|
||||
{
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user