1
0
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:
Justin Baur 2022-10-07 09:53:33 -04:00
parent e0b1f9544b
commit a20e127c9c
No known key found for this signature in database
GPG Key ID: 720FC4DCE0D4CDA5
149 changed files with 9934 additions and 394 deletions

View File

@ -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 }}

View File

@ -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:

View File

@ -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

View File

@ -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>

View File

@ -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}

View File

@ -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)

View File

@ -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()

View 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);
}
}

View File

@ -0,0 +1,8 @@
using Bit.Scim.Models;
namespace Bit.Scim.Queries.Users.Interfaces;
public interface IGetUserQuery
{
Task<ScimUserResponseModel> GetUserAsync(Guid organizationId, Guid id);
}

View File

@ -75,6 +75,8 @@ public class Startup
config.Filters.Add(new LoggingExceptionHandlerFilterAttribute());
});
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
services.AddScimUserQueries();
}
public void Configure(

View File

@ -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);
}
}

View File

@ -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>();
}
}

View File

@ -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)
{

View File

@ -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

View File

@ -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()

View File

@ -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;

View File

@ -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));
}
}

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
View 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"
]
}
]
}

View File

@ -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;

View File

@ -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
{

View File

@ -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);
}

View File

@ -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);
}

View 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);
}
}

View 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");
}
}

View File

@ -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>();
}
}

View File

@ -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; }

View File

@ -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()

View File

@ -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;

View File

@ -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>)

View File

@ -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"))

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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))
{

View File

@ -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);

View 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; }
}

View 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; }
}

View File

@ -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()

View File

@ -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" }

View File

@ -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()

View 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);
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums;
public enum AuthRequestType : byte
{
AuthenticateAndUnlock = 0,
Unlock = 1
}

View File

@ -20,4 +20,7 @@ public enum PushType : byte
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
AuthRequest = 15,
AuthRequestResponse = 16,
}

View File

@ -4,6 +4,9 @@ namespace Bit.Core.Exceptions;
public class BadRequestException : Exception
{
public BadRequestException() : base()
{ }
public BadRequestException(string message)
: base(message)
{ }

View File

@ -0,0 +1,3 @@
namespace Bit.Core.Exceptions;
public class ConflictException : Exception { }

View File

@ -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)
{ }
}

View File

@ -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));
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Models.Api.Response.Accounts;
public interface ICaptchaProtectedResponseModel
{
public string CaptchaBypassToken { get; set; }
}

View File

@ -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; }
}

View File

@ -56,4 +56,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
{
return Premium.GetValueOrDefault(false);
}
public bool OccupiesOrganizationSeat
{
get
{
return Status != OrganizationUserStatusType.Revoked;
}
}
}

View File

@ -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; }
}

View File

@ -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>>>())
);
}
}

View File

@ -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
{

View 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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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));

View File

@ -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));

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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; }
}

View 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; }
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Settings;
public interface IPasswordlessAuthSettings
{
bool KnownDevicesOnly { get; set; }
}

View File

@ -0,0 +1,8 @@
using Serilog.Events;
namespace Bit.Core.Settings.LoggingSettings;
public class AdminLogLevelSettings : IAdminLogLevelSettings
{
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
}

View 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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
using Serilog.Events;
namespace Bit.Core.Settings.LoggingSettings;
public class EventsProcessorLogLevelSettings : IEventsProcessorLogLevelSettings
{
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
}

View File

@ -0,0 +1,8 @@
using Serilog.Events;
namespace Bit.Core.Settings.LoggingSettings;
public class IconsLogLevelSettings : IIconsLogLevelSettings
{
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
}

View File

@ -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;
}

View 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();
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
using Serilog.Events;
namespace Bit.Core.Settings.LoggingSettings;
public class ScimLogLevelSettings : IScimLogLevelSettings
{
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
}

View File

@ -0,0 +1,8 @@
using Serilog.Events;
namespace Bit.Core.Settings.LoggingSettings;
public class SsoLogLevelSettings : ISsoLogLevelSettings
{
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);

View File

@ -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);
}

View File

@ -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()

View File

@ -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();

View File

@ -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();

View File

@ -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)
{

View File

@ -1,7 +1,7 @@
using Bit.Core.Settings;
using IdentityServer4.Models;
namespace Bit.Core.IdentityServer;
namespace Bit.Identity.IdentityServer;
public class ApiClient : Client
{

View File

@ -1,7 +1,7 @@
using IdentityModel;
using IdentityServer4.Models;
namespace Bit.Core.IdentityServer;
namespace Bit.Identity.IdentityServer;
public class ApiResources
{

View File

@ -1,6 +1,6 @@
using IdentityServer4.Models;
namespace Bit.Core.IdentityServer;
namespace Bit.Identity.IdentityServer;
public class ApiScopes
{

View File

@ -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

View File

@ -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
{

View File

@ -10,7 +10,7 @@ using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
namespace Bit.Core.IdentityServer;
namespace Bit.Identity.IdentityServer;
public class ClientStore : IClientStore
{

View File

@ -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

View File

@ -1,7 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Models.Business;
namespace Bit.Core.IdentityServer;
namespace Bit.Identity.IdentityServer;
public class CustomValidatorRequestContext
{

View File

@ -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