mirror of
https://github.com/bitwarden/server.git
synced 2025-06-20 02:48:03 -05:00
Merge branch 'km/signing-api-changes' into km/signing-upgrade-rotation
This commit is contained in:
commit
91d68f1481
125
.github/workflows/repository-management.yml
vendored
125
.github/workflows/repository-management.yml
vendored
@ -22,6 +22,8 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
setup:
|
setup:
|
||||||
name: Setup
|
name: Setup
|
||||||
@ -44,49 +46,11 @@ jobs:
|
|||||||
|
|
||||||
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
cut_branch:
|
|
||||||
name: Cut branch
|
|
||||||
if: ${{ needs.setup.outputs.branch != 'none' }}
|
|
||||||
needs: setup
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- name: Generate GH App token
|
|
||||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
|
||||||
id: app-token
|
|
||||||
with:
|
|
||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
|
||||||
|
|
||||||
- name: Check out target ref
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
ref: ${{ inputs.target_ref }}
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
|
||||||
env:
|
|
||||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
|
||||||
run: |
|
|
||||||
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
|
||||||
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Cut branch
|
|
||||||
env:
|
|
||||||
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
|
||||||
run: |
|
|
||||||
git switch --quiet --create $BRANCH_NAME
|
|
||||||
git push --quiet --set-upstream origin $BRANCH_NAME
|
|
||||||
|
|
||||||
|
|
||||||
bump_version:
|
bump_version:
|
||||||
name: Bump Version
|
name: Bump Version
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- cut_branch
|
|
||||||
- setup
|
- setup
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
@ -187,14 +151,13 @@ jobs:
|
|||||||
- name: Push changes
|
- name: Push changes
|
||||||
run: git push
|
run: git push
|
||||||
|
|
||||||
|
cut_branch:
|
||||||
cherry_pick:
|
name: Cut branch
|
||||||
name: Cherry-Pick Commit(s)
|
|
||||||
if: ${{ needs.setup.outputs.branch != 'none' }}
|
if: ${{ needs.setup.outputs.branch != 'none' }}
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
needs:
|
||||||
- bump_version
|
|
||||||
- setup
|
- setup
|
||||||
|
- bump_version
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Generate GH App token
|
- name: Generate GH App token
|
||||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||||
@ -203,78 +166,30 @@ jobs:
|
|||||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
|
|
||||||
- name: Check out main branch
|
- name: Check out target ref
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
ref: ${{ inputs.target_ref }}
|
||||||
ref: main
|
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
- name: Configure Git
|
- name: Check if ${{ needs.setup.outputs.branch }} branch exists
|
||||||
run: |
|
|
||||||
git config --local user.email "actions@github.com"
|
|
||||||
git config --local user.name "Github Actions"
|
|
||||||
|
|
||||||
- name: Install xmllint
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libxml2-utils
|
|
||||||
|
|
||||||
- name: Perform cherry-pick(s)
|
|
||||||
env:
|
env:
|
||||||
CUT_BRANCH: ${{ needs.setup.outputs.branch }}
|
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||||
run: |
|
run: |
|
||||||
# Function for cherry-picking
|
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
|
||||||
cherry_pick () {
|
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
|
||||||
local source_branch=$1
|
exit 1
|
||||||
local destination_branch=$2
|
|
||||||
|
|
||||||
# Get project commit/version from source branch
|
|
||||||
git switch $source_branch
|
|
||||||
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
|
|
||||||
SOURCE_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
|
||||||
|
|
||||||
# Get project commit/version from destination branch
|
|
||||||
git switch $destination_branch
|
|
||||||
DESTINATION_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
|
||||||
|
|
||||||
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
|
|
||||||
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
|
|
||||||
git push -u origin $destination_branch
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# If we are cutting 'hotfix-rc':
|
|
||||||
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
|
|
||||||
|
|
||||||
# If the 'rc' branch exists:
|
|
||||||
if [[ $(git ls-remote --heads origin rc) ]]; then
|
|
||||||
|
|
||||||
# Chery-pick from 'rc' into 'hotfix-rc'
|
|
||||||
cherry_pick rc hotfix-rc
|
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'rc'
|
|
||||||
cherry_pick main rc
|
|
||||||
|
|
||||||
# If the 'rc' branch does not exist:
|
|
||||||
else
|
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'hotfix-rc'
|
|
||||||
cherry_pick main hotfix-rc
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If we are cutting 'rc':
|
|
||||||
elif [[ "$CUT_BRANCH" == "rc" ]]; then
|
|
||||||
|
|
||||||
# Cherry-pick from 'main' into 'rc'
|
|
||||||
cherry_pick main rc
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Cut branch
|
||||||
|
env:
|
||||||
|
BRANCH_NAME: ${{ needs.setup.outputs.branch }}
|
||||||
|
run: |
|
||||||
|
git switch --quiet --create $BRANCH_NAME
|
||||||
|
git push --quiet --set-upstream origin $BRANCH_NAME
|
||||||
|
|
||||||
move_future_db_scripts:
|
move_future_db_scripts:
|
||||||
name: Move finalization database scripts
|
name: Move finalization database scripts
|
||||||
needs: cherry_pick
|
needs: cut_branch
|
||||||
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
uses: ./.github/workflows/_move_finalization_db_scripts.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.6.1</Version>
|
<Version>2025.6.2</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -403,16 +403,15 @@ public class OrganizationUsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/confirm")]
|
[HttpPost("{id}/confirm")]
|
||||||
public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model)
|
public async Task Confirm(Guid orgId, Guid id, [FromBody] OrganizationUserConfirmRequestModel model)
|
||||||
{
|
{
|
||||||
var orgGuidId = new Guid(orgId);
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
|
var result = await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, id, model.Key, userId.Value, model.DefaultUserCollectionName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("confirm")]
|
[HttpPost("confirm")]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
@ -60,6 +60,10 @@ public class OrganizationUserConfirmRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
[EncryptedString]
|
||||||
|
[EncryptedStringLength(1000)]
|
||||||
|
public string DefaultUserCollectionName { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrganizationUserBulkConfirmRequestModelEntry
|
public class OrganizationUserBulkConfirmRequestModelEntry
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Api.Dirt.Models;
|
using Bit.Api.Dirt.Models;
|
||||||
using Bit.Api.Dirt.Models.Response;
|
using Bit.Api.Dirt.Models.Response;
|
||||||
|
using Bit.Api.Tools.Models.Response;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Dirt.Reports.Entities;
|
using Bit.Core.Dirt.Reports.Entities;
|
||||||
using Bit.Core.Dirt.Reports.Models.Data;
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
@ -17,21 +18,24 @@ namespace Bit.Api.Dirt.Controllers;
|
|||||||
public class ReportsController : Controller
|
public class ReportsController : Controller
|
||||||
{
|
{
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IMemberAccessCipherDetailsQuery _memberAccessCipherDetailsQuery;
|
private readonly IMemberAccessReportQuery _memberAccessReportQuery;
|
||||||
|
private readonly IRiskInsightsReportQuery _riskInsightsReportQuery;
|
||||||
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
|
private readonly IAddPasswordHealthReportApplicationCommand _addPwdHealthReportAppCommand;
|
||||||
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
|
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
|
||||||
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
|
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
|
||||||
|
|
||||||
public ReportsController(
|
public ReportsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IMemberAccessCipherDetailsQuery memberAccessCipherDetailsQuery,
|
IMemberAccessReportQuery memberAccessReportQuery,
|
||||||
|
IRiskInsightsReportQuery riskInsightsReportQuery,
|
||||||
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
|
IAddPasswordHealthReportApplicationCommand addPasswordHealthReportApplicationCommand,
|
||||||
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
|
IGetPasswordHealthReportApplicationQuery getPasswordHealthReportApplicationQuery,
|
||||||
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
|
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_memberAccessCipherDetailsQuery = memberAccessCipherDetailsQuery;
|
_memberAccessReportQuery = memberAccessReportQuery;
|
||||||
|
_riskInsightsReportQuery = riskInsightsReportQuery;
|
||||||
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
|
_addPwdHealthReportAppCommand = addPasswordHealthReportApplicationCommand;
|
||||||
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
|
_getPwdHealthReportAppQuery = getPasswordHealthReportApplicationQuery;
|
||||||
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
|
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
|
||||||
@ -54,9 +58,9 @@ public class ReportsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
|
var riskDetails = await GetRiskInsightsReportDetails(new RiskInsightsReportRequest { OrganizationId = orgId });
|
||||||
|
|
||||||
var responses = memberCipherDetails.Select(x => new MemberCipherDetailsResponseModel(x));
|
var responses = riskDetails.Select(x => new MemberCipherDetailsResponseModel(x));
|
||||||
|
|
||||||
return responses;
|
return responses;
|
||||||
}
|
}
|
||||||
@ -69,16 +73,16 @@ public class ReportsController : Controller
|
|||||||
/// <returns>IEnumerable of MemberAccessReportResponseModel</returns>
|
/// <returns>IEnumerable of MemberAccessReportResponseModel</returns>
|
||||||
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
/// <exception cref="NotFoundException">If Access reports permission is not assigned</exception>
|
||||||
[HttpGet("member-access/{orgId}")]
|
[HttpGet("member-access/{orgId}")]
|
||||||
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
public async Task<IEnumerable<MemberAccessDetailReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.AccessReports(orgId))
|
if (!await _currentContext.AccessReports(orgId))
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var memberCipherDetails = await GetMemberCipherDetails(new MemberAccessCipherDetailsRequest { OrganizationId = orgId });
|
var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId });
|
||||||
|
|
||||||
var responses = memberCipherDetails.Select(x => new MemberAccessReportResponseModel(x));
|
var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x));
|
||||||
|
|
||||||
return responses;
|
return responses;
|
||||||
}
|
}
|
||||||
@ -87,13 +91,28 @@ public class ReportsController : Controller
|
|||||||
/// Contains the organization member info, the cipher ids associated with the member,
|
/// Contains the organization member info, the cipher ids associated with the member,
|
||||||
/// and details on their collections, groups, and permissions
|
/// and details on their collections, groups, and permissions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="request">Request to the MemberAccessCipherDetailsQuery</param>
|
/// <param name="request">Request parameters</param>
|
||||||
/// <returns>IEnumerable of MemberAccessCipherDetails</returns>
|
/// <returns>
|
||||||
private async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberCipherDetails(MemberAccessCipherDetailsRequest request)
|
/// List of a user's permissions at a group and collection level as well as the number of ciphers
|
||||||
|
/// associated with that group/collection
|
||||||
|
/// </returns>
|
||||||
|
private async Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessDetails(
|
||||||
|
MemberAccessReportRequest request)
|
||||||
{
|
{
|
||||||
var memberCipherDetails =
|
var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request);
|
||||||
await _memberAccessCipherDetailsQuery.GetMemberAccessCipherDetails(request);
|
return accessDetails;
|
||||||
return memberCipherDetails;
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request parameters</param>
|
||||||
|
/// <returns>A list of risk insights data associating the user to cipher ids</returns>
|
||||||
|
private async Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(
|
||||||
|
RiskInsightsReportRequest request)
|
||||||
|
{
|
||||||
|
var riskDetails = await _riskInsightsReportQuery.GetRiskInsightsReportDetails(request);
|
||||||
|
return riskDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Api.Tools.Models.Response;
|
||||||
|
|
||||||
|
public class MemberAccessDetailReportResponseModel
|
||||||
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public bool TwoFactorEnabled { get; set; }
|
||||||
|
public bool AccountRecoveryEnabled { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
public Guid? CollectionId { get; set; }
|
||||||
|
public Guid? GroupId { get; set; }
|
||||||
|
public string GroupName { get; set; }
|
||||||
|
public string CollectionName { get; set; }
|
||||||
|
public bool? ReadOnly { get; set; }
|
||||||
|
public bool? HidePasswords { get; set; }
|
||||||
|
public bool? Manage { get; set; }
|
||||||
|
public IEnumerable<Guid> CipherIds { get; set; }
|
||||||
|
|
||||||
|
public MemberAccessDetailReportResponseModel(MemberAccessReportDetail reportDetail)
|
||||||
|
{
|
||||||
|
UserGuid = reportDetail.UserGuid;
|
||||||
|
UserName = reportDetail.UserName;
|
||||||
|
Email = reportDetail.Email;
|
||||||
|
TwoFactorEnabled = reportDetail.TwoFactorEnabled;
|
||||||
|
AccountRecoveryEnabled = reportDetail.AccountRecoveryEnabled;
|
||||||
|
UsesKeyConnector = reportDetail.UsesKeyConnector;
|
||||||
|
CollectionId = reportDetail.CollectionId;
|
||||||
|
GroupId = reportDetail.GroupId;
|
||||||
|
GroupName = reportDetail.GroupName;
|
||||||
|
CollectionName = reportDetail.CollectionName;
|
||||||
|
ReadOnly = reportDetail.ReadOnly;
|
||||||
|
HidePasswords = reportDetail.HidePasswords;
|
||||||
|
Manage = reportDetail.Manage;
|
||||||
|
CipherIds = reportDetail.CipherIds;
|
||||||
|
}
|
||||||
|
}
|
@ -15,12 +15,12 @@ public class MemberCipherDetailsResponseModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> CipherIds { get; set; }
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
|
||||||
public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails)
|
public MemberCipherDetailsResponseModel(RiskInsightsReportDetail reportDetail)
|
||||||
{
|
{
|
||||||
this.UserGuid = memberAccessCipherDetails.UserGuid;
|
this.UserGuid = reportDetail.UserGuid;
|
||||||
this.UserName = memberAccessCipherDetails.UserName;
|
this.UserName = reportDetail.UserName;
|
||||||
this.Email = memberAccessCipherDetails.Email;
|
this.Email = reportDetail.Email;
|
||||||
this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector;
|
this.UsesKeyConnector = reportDetail.UsesKeyConnector;
|
||||||
this.CipherIds = memberAccessCipherDetails.CipherIds;
|
this.CipherIds = reportDetail.CipherIds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;
|
using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement.Controllers;
|
namespace Bit.Api.KeyManagement.Controllers;
|
||||||
|
|
||||||
[Route("users")]
|
[Route("users")]
|
||||||
@ -22,11 +24,10 @@ public class UsersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/public-key")]
|
[HttpGet("{id}/public-key")]
|
||||||
public async Task<UserKeyResponseModel> GetPublicKeyAsync(string id)
|
public async Task<UserKeyResponseModel> GetPublicKeyAsync([FromRoute] Guid id)
|
||||||
{
|
{
|
||||||
var guidId = new Guid(id);
|
var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException();
|
||||||
var key = await _userRepository.GetPublicKeyAsync(guidId) ?? throw new NotFoundException();
|
return new UserKeyResponseModel(id, key);
|
||||||
return new UserKeyResponseModel(guidId, key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/keys")]
|
[HttpGet("{id}/keys")]
|
||||||
|
@ -14,6 +14,10 @@ namespace Bit.Api.KeyManagement.Models.Response;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PrivateKeysResponseModel : ResponseModel
|
public class PrivateKeysResponseModel : ResponseModel
|
||||||
{
|
{
|
||||||
|
// Not all accounts have signature keys, but all accounts have public encryption keys.
|
||||||
|
public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; }
|
||||||
|
public required PublicKeyEncryptionKeyPairModel PublicKeyEncryptionKeyPair { get; set; }
|
||||||
|
|
||||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||||
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
||||||
{
|
{
|
||||||
@ -33,9 +37,4 @@ public class PrivateKeysResponseModel : ResponseModel
|
|||||||
SignatureKeyPair = signatureKeyPair;
|
SignatureKeyPair = signatureKeyPair;
|
||||||
PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair));
|
PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not all accounts have signature keys, but all accounts have public encryption keys.
|
|
||||||
public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; }
|
|
||||||
public required PublicKeyEncryptionKeyPairModel PublicKeyEncryptionKeyPair { get; set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,8 @@ public class PublicKeysResponseModel : ResponseModel
|
|||||||
public PublicKeysResponseModel(UserAccountKeysData accountKeys)
|
public PublicKeysResponseModel(UserAccountKeysData accountKeys)
|
||||||
: base("publicKeys")
|
: base("publicKeys")
|
||||||
{
|
{
|
||||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
|
||||||
ArgumentNullException.ThrowIfNull(accountKeys);
|
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||||
|
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||||
|
|
||||||
if (accountKeys.SignatureKeyPairData != null)
|
if (accountKeys.SignatureKeyPairData != null)
|
||||||
{
|
{
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
|
||||||
using Bit.Core.KeyManagement.Queries;
|
|
||||||
|
|
||||||
namespace Bit.Api.KeyManagement;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
public static class Registrations
|
|
||||||
{
|
|
||||||
public static void AddKeyManagementQueries(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddTransient<IUserAccountKeysQuery, UserAccountKeysQuery>();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,11 @@
|
|||||||
using Bit.Core.Context;
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.NotificationHub;
|
using Bit.Core.NotificationHub;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Platform.Push.Internal;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -20,14 +23,14 @@ namespace Bit.Api.Platform.Push;
|
|||||||
public class PushController : Controller
|
public class PushController : Controller
|
||||||
{
|
{
|
||||||
private readonly IPushRegistrationService _pushRegistrationService;
|
private readonly IPushRegistrationService _pushRegistrationService;
|
||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushRelayer _pushRelayer;
|
||||||
private readonly IWebHostEnvironment _environment;
|
private readonly IWebHostEnvironment _environment;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
|
||||||
public PushController(
|
public PushController(
|
||||||
IPushRegistrationService pushRegistrationService,
|
IPushRegistrationService pushRegistrationService,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushRelayer pushRelayer,
|
||||||
IWebHostEnvironment environment,
|
IWebHostEnvironment environment,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IGlobalSettings globalSettings)
|
IGlobalSettings globalSettings)
|
||||||
@ -35,7 +38,7 @@ public class PushController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_environment = environment;
|
_environment = environment;
|
||||||
_pushRegistrationService = pushRegistrationService;
|
_pushRegistrationService = pushRegistrationService;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushRelayer = pushRelayer;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,31 +77,50 @@ public class PushController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("send")]
|
[HttpPost("send")]
|
||||||
public async Task SendAsync([FromBody] PushSendRequestModel model)
|
public async Task SendAsync([FromBody] PushSendRequestModel<JsonElement> model)
|
||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(model.InstallationId))
|
NotificationTarget target;
|
||||||
|
Guid targetId;
|
||||||
|
|
||||||
|
if (model.InstallationId.HasValue)
|
||||||
{
|
{
|
||||||
if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!)
|
if (_currentContext.InstallationId!.Value != model.InstallationId.Value)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("InstallationId does not match current context.");
|
throw new BadRequestException("InstallationId does not match current context.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _pushNotificationService.SendPayloadToInstallationAsync(
|
target = NotificationTarget.Installation;
|
||||||
_currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier),
|
targetId = _currentContext.InstallationId.Value;
|
||||||
Prefix(model.DeviceId), model.ClientType);
|
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(model.UserId))
|
else if (model.UserId.HasValue)
|
||||||
{
|
{
|
||||||
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
|
target = NotificationTarget.User;
|
||||||
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
|
targetId = model.UserId.Value;
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
|
else if (model.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
|
target = NotificationTarget.Organization;
|
||||||
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
|
targetId = model.OrganizationId.Value;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new UnreachableException("Model validation should have prevented getting here.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var notification = new RelayedNotification
|
||||||
|
{
|
||||||
|
Type = model.Type,
|
||||||
|
Target = target,
|
||||||
|
TargetId = targetId,
|
||||||
|
Payload = model.Payload,
|
||||||
|
Identifier = model.Identifier,
|
||||||
|
DeviceId = model.DeviceId,
|
||||||
|
ClientType = model.ClientType,
|
||||||
|
};
|
||||||
|
|
||||||
|
await _pushRelayer.RelayAsync(_currentContext.InstallationId.Value, notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string Prefix(string value)
|
private string Prefix(string value)
|
||||||
|
@ -34,8 +34,6 @@ using Bit.Core.Tools.ImportFeatures;
|
|||||||
using Bit.Core.Auth.Models.Api.Request;
|
using Bit.Core.Auth.Models.Api.Request;
|
||||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
using Bit.Core.Tools.SendFeatures;
|
using Bit.Core.Tools.SendFeatures;
|
||||||
using Bit.Api.KeyManagement;
|
|
||||||
|
|
||||||
|
|
||||||
#if !OSS
|
#if !OSS
|
||||||
using Bit.Commercial.Core.SecretsManager;
|
using Bit.Commercial.Core.SecretsManager;
|
||||||
@ -187,7 +185,6 @@ public class Startup
|
|||||||
services.AddPhishingDomainServices(globalSettings);
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
services.AddBillingQueries();
|
services.AddBillingQueries();
|
||||||
services.AddKeyManagementQueries();
|
|
||||||
services.AddSendServices();
|
services.AddSendServices();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public interface IIntegrationMessage
|
public interface IIntegrationMessage
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public class IntegrationHandlerResult
|
public class IntegrationHandlerResult
|
||||||
{
|
{
|
@ -3,7 +3,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public class IntegrationMessage : IIntegrationMessage
|
public class IntegrationMessage : IIntegrationMessage
|
||||||
{
|
{
|
@ -5,7 +5,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public class IntegrationTemplateContext(EventMessage eventMessage)
|
public class IntegrationTemplateContext(EventMessage eventMessage)
|
||||||
{
|
{
|
@ -1,5 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public record SlackIntegration(string token);
|
public record SlackIntegration(string token);
|
@ -1,5 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public record SlackIntegrationConfiguration(string channelId);
|
public record SlackIntegrationConfiguration(string channelId);
|
@ -1,5 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
@ -1,5 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public record WebhookIntegrationConfiguration(string url);
|
public record WebhookIntegrationConfiguration(string url);
|
@ -1,5 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public record WebhookIntegrationConfigurationDetails(string url);
|
public record WebhookIntegrationConfigurationDetails(string url);
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -28,6 +29,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
|
|
||||||
public ConfirmOrganizationUserCommand(
|
public ConfirmOrganizationUserCommand(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -41,7 +43,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -55,10 +58,11 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_collectionRepository = collectionRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
||||||
Guid confirmingUserId)
|
Guid confirmingUserId, string defaultUserCollectionName = null)
|
||||||
{
|
{
|
||||||
var result = await ConfirmUsersAsync(
|
var result = await ConfirmUsersAsync(
|
||||||
organizationId,
|
organizationId,
|
||||||
@ -75,6 +79,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
{
|
{
|
||||||
throw new BadRequestException(error);
|
throw new BadRequestException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await HandleConfirmationSideEffectsAsync(organizationId, orgUser, defaultUserCollectionName);
|
||||||
|
|
||||||
return orgUser;
|
return orgUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,4 +220,54 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|||||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
.Select(d => d.Id.ToString());
|
.Select(d => d.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleConfirmationSideEffectsAsync(Guid organizationId, OrganizationUser organizationUser, string defaultUserCollectionName)
|
||||||
|
{
|
||||||
|
// Create DefaultUserCollection type collection for the user if the PersonalOwnership policy is enabled for the organization
|
||||||
|
var requiresDefaultCollection = await OrganizationRequiresDefaultCollectionAsync(organizationId, organizationUser.UserId.Value, defaultUserCollectionName);
|
||||||
|
if (requiresDefaultCollection)
|
||||||
|
{
|
||||||
|
await CreateDefaultCollectionAsync(organizationId, organizationUser.Id, defaultUserCollectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> OrganizationRequiresDefaultCollectionAsync(Guid organizationId, Guid userId, string defaultUserCollectionName)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if no collection name provided (backwards compatibility)
|
||||||
|
if (string.IsNullOrWhiteSpace(defaultUserCollectionName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var personalOwnershipRequirement = await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(userId);
|
||||||
|
return personalOwnershipRequirement.RequiresDefaultCollection(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateDefaultCollectionAsync(Guid organizationId, Guid organizationUserId, string defaultCollectionName)
|
||||||
|
{
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
Name = defaultCollectionName,
|
||||||
|
Type = CollectionType.DefaultUserCollection
|
||||||
|
};
|
||||||
|
|
||||||
|
var userAccess = new List<CollectionAccessSelection>
|
||||||
|
{
|
||||||
|
new CollectionAccessSelection
|
||||||
|
{
|
||||||
|
Id = organizationUserId,
|
||||||
|
ReadOnly = false,
|
||||||
|
HidePasswords = false,
|
||||||
|
Manage = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await _collectionRepository.CreateAsync(collection, groups: null, users: userAccess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,10 @@ public interface IConfirmOrganizationUserCommand
|
|||||||
/// <param name="organizationUserId">The ID of the organization user to confirm.</param>
|
/// <param name="organizationUserId">The ID of the organization user to confirm.</param>
|
||||||
/// <param name="key">The encrypted organization key for the user.</param>
|
/// <param name="key">The encrypted organization key for the user.</param>
|
||||||
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
/// <param name="confirmingUserId">The ID of the user performing the confirmation.</param>
|
||||||
|
/// <param name="defaultUserCollectionName">Optional encrypted collection name for creating a default collection.</param>
|
||||||
/// <returns>The confirmed organization user.</returns>
|
/// <returns>The confirmed organization user.</returns>
|
||||||
/// <exception cref="BadRequestException">Thrown when the user is not valid or cannot be confirmed.</exception>
|
/// <exception cref="BadRequestException">Thrown when the user is not valid or cannot be confirmed.</exception>
|
||||||
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
|
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, string defaultUserCollectionName = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Confirms multiple organization users who have accepted their invitations.
|
/// Confirms multiple organization users who have accepted their invitations.
|
||||||
|
@ -3,15 +3,55 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
|||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the personal ownership policy state.
|
||||||
|
/// </summary>
|
||||||
|
public enum PersonalOwnershipState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Personal ownership is allowed - users can save items to their personal vault.
|
||||||
|
/// </summary>
|
||||||
|
Allowed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Personal ownership is restricted - members are required to save items to an organization.
|
||||||
|
/// </summary>
|
||||||
|
Restricted
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy requirements for the Disable Personal Ownership policy.
|
/// Policy requirements for the Disable Personal Ownership policy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
|
public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
|
||||||
{
|
{
|
||||||
|
private readonly IEnumerable<Guid> _organizationIdsWithPolicyEnabled;
|
||||||
|
|
||||||
|
/// <param name="personalOwnershipState">
|
||||||
|
/// The personal ownership state for the user.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="organizationIdsWithPolicyEnabled">
|
||||||
|
/// The collection of Organization IDs that have the Disable Personal Ownership policy enabled.
|
||||||
|
/// </param>
|
||||||
|
public PersonalOwnershipPolicyRequirement(
|
||||||
|
PersonalOwnershipState personalOwnershipState,
|
||||||
|
IEnumerable<Guid> organizationIdsWithPolicyEnabled)
|
||||||
|
{
|
||||||
|
_organizationIdsWithPolicyEnabled = organizationIdsWithPolicyEnabled ?? [];
|
||||||
|
State = personalOwnershipState;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization.
|
/// The personal ownership policy state for the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DisablePersonalOwnership { get; init; }
|
public PersonalOwnershipState State { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns true if the Disable Personal Ownership policy is enforced in that organization.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresDefaultCollection(Guid organizationId)
|
||||||
|
{
|
||||||
|
return _organizationIdsWithPolicyEnabled.Contains(organizationId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement>
|
public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement>
|
||||||
@ -20,7 +60,13 @@ public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFa
|
|||||||
|
|
||||||
public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
{
|
{
|
||||||
var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() };
|
var personalOwnershipState = policyDetails.Any()
|
||||||
return result;
|
? PersonalOwnershipState.Restricted
|
||||||
|
: PersonalOwnershipState.Allowed;
|
||||||
|
var organizationIdsWithPolicyEnabled = policyDetails.Select(p => p.OrganizationId).ToHashSet();
|
||||||
|
|
||||||
|
return new PersonalOwnershipPolicyRequirement(
|
||||||
|
personalOwnershipState,
|
||||||
|
organizationIdsWithPolicyEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Azure.Messaging.ServiceBus;
|
using Azure.Messaging.ServiceBus;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
|
|
||||||
|
@ -33,6 +33,13 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
|
|||||||
await _processor.StartProcessingAsync(cancellationToken);
|
await _processor.StartProcessingAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _processor.StopProcessingAsync(cancellationToken);
|
||||||
|
await _processor.DisposeAsync();
|
||||||
|
await base.StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
internal Task ProcessErrorAsync(ProcessErrorEventArgs args)
|
internal Task ProcessErrorAsync(ProcessErrorEventArgs args)
|
||||||
{
|
{
|
||||||
_logger.LogError(
|
_logger.LogError(
|
||||||
@ -49,16 +56,4 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
|
|||||||
await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId);
|
await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId);
|
||||||
await args.CompleteMessageAsync(args.Message);
|
await args.CompleteMessageAsync(args.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await _processor.StopProcessingAsync(cancellationToken);
|
|
||||||
await base.StopAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
_processor.DisposeAsync().GetAwaiter().GetResult();
|
|
||||||
base.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
using Azure.Messaging.ServiceBus;
|
using Azure.Messaging.ServiceBus;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.AdminConsole.Utilities;
|
using Bit.Core.AdminConsole.Utilities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
@ -0,0 +1,375 @@
|
|||||||
|
# Design goals
|
||||||
|
|
||||||
|
The main goal of event integrations is to easily enable adding new integrations over time without the need
|
||||||
|
for a lot of custom work to expose events to a new integration. The ability of fan-out offered by AMQP
|
||||||
|
(either in RabbitMQ or in Azure Service Bus) gives us a way to attach any number of new integrations to the
|
||||||
|
existing event system without needing to add special handling. By adding a new listener to the existing
|
||||||
|
pipeline, it gains an independent stream of events without the need for additional broadcast code.
|
||||||
|
|
||||||
|
We want to enable robust handling of failures and retries. By utilizing the two-tier approach
|
||||||
|
([described below](#two-tier-exchange)), we build in support at the service level for retries. When we add
|
||||||
|
new integrations, they can focus solely on the integration-specific logic and reporting status, with all the
|
||||||
|
process of retries and delays managed by the messaging system.
|
||||||
|
|
||||||
|
Another goal is to not only support this functionality in the cloud version, but offer it as well to
|
||||||
|
self-hosted instances. RabbitMQ provides a lightweight way for self-hosted instances to tie into the event system
|
||||||
|
using the same robust architecture for integrations without the need for Azure Service Bus.
|
||||||
|
|
||||||
|
Finally, we want to offer organization admins flexibility and control over what events are significant, where
|
||||||
|
to send events, and the data to be included in the message. The configuration architecture allows Organizations
|
||||||
|
to customize details of a specific integration; see [Integrations and integration
|
||||||
|
configurations](#integrations-and-integration-configurations) below for more details on the configuration piece.
|
||||||
|
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
The entry point for the event integrations is the `IEventWriteService`. By configuring the
|
||||||
|
`EventIntegrationEventWriteService` as the `EventWriteService`, all events sent to the
|
||||||
|
service are broadcast on the RabbitMQ or Azure Service Bus message exchange. To abstract away
|
||||||
|
the specifics of publishing to a specific AMQP provider, an `IEventIntegrationPublisher`
|
||||||
|
is injected into `EventIntegrationEventWriteService` to handle the publishing of events to the
|
||||||
|
RabbitMQ or Azure Service Bus service.
|
||||||
|
|
||||||
|
## Two-tier exchange
|
||||||
|
|
||||||
|
When `EventIntegrationEventWriteService` publishes, it posts to the first tier of our two-tier
|
||||||
|
approach to handling messages. Each tier is represented in the AMQP stack by a separate exchange
|
||||||
|
(in RabbitMQ terminology) or topic (in Azure Service Bus).
|
||||||
|
|
||||||
|
``` mermaid
|
||||||
|
flowchart TD
|
||||||
|
B1[EventService]
|
||||||
|
B2[EventIntegrationEventWriteService]
|
||||||
|
B3[Event Exchange / Topic]
|
||||||
|
B4[EventRepositoryHandler]
|
||||||
|
B5[WebhookIntegrationHandler]
|
||||||
|
B6[Events in Database / Azure Tables]
|
||||||
|
B7[HTTP Server]
|
||||||
|
B8[SlackIntegrationHandler]
|
||||||
|
B9[Slack]
|
||||||
|
B10[EventIntegrationHandler]
|
||||||
|
B12[Integration Exchange / Topic]
|
||||||
|
|
||||||
|
B1 -->|IEventWriteService| B2 --> B3
|
||||||
|
B3-->|EventListenerService| B4 --> B6
|
||||||
|
B3-->|EventListenerService| B10
|
||||||
|
B3-->|EventListenerService| B10
|
||||||
|
B10 --> B12
|
||||||
|
B12 -->|IntegrationListenerService| B5
|
||||||
|
B12 -->|IntegrationListenerService| B8
|
||||||
|
B5 -->|HTTP POST| B7
|
||||||
|
B8 -->|HTTP POST| B9
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event tier
|
||||||
|
|
||||||
|
In the first tier, events are broadcast in a fan-out to a series of listeners. The message body
|
||||||
|
is a JSON representation of an individual `EventMessage` or an array of `EventMessage`. Handlers at
|
||||||
|
this level are responsible for handling each event or array of events. There are currently two handlers
|
||||||
|
at this level:
|
||||||
|
- `EventRepositoryHandler`
|
||||||
|
- The `EventRepositoryHandler` is responsible for long term storage of events. It receives all events
|
||||||
|
and stores them via an injected `IEventRepository` into the database.
|
||||||
|
- This mirrors the behavior of when event integrations are turned off - cloud stores to Azure Tables
|
||||||
|
and self-hosted is stored to the database.
|
||||||
|
- `EventIntegrationHandler`
|
||||||
|
- The `EventIntegrationHandler` is a generic class that is customized to each integration (via the
|
||||||
|
configuration details of the integration) and is responsible for determining if there's a configuration
|
||||||
|
for this event / organization / integration, fetching that configuration, and parsing the details of the
|
||||||
|
event into a template string.
|
||||||
|
- The `EventIntegrationHandler` uses the injected `IOrganizationIntegrationConfigurationRepository` to pull
|
||||||
|
the specific set of configuration and template based on the event type, organization, and integration type.
|
||||||
|
This configuration is what determines if an integration should be sent, what details are necessary for sending
|
||||||
|
it, and the actual message to send.
|
||||||
|
- The output of `EventIntegrationHandler` is a new `IntegrationMessage`, with the details of this
|
||||||
|
the configuration necessary to interact with the integration and the message to send (with all the event
|
||||||
|
details incorporated), published to the integration level of the message bus.
|
||||||
|
|
||||||
|
### Integration tier
|
||||||
|
|
||||||
|
At the integration level, messages are JSON representations of `IIntegrationMessage` - specifically they
|
||||||
|
will be concrete types of the generic `IntegrationMessage<T>` where `<T>` is the configuration details of the
|
||||||
|
specific integration for which they've been sent. These messages represent the details required for
|
||||||
|
sending a specific event to a specific integration, including handling retries and delays.
|
||||||
|
|
||||||
|
Handlers at the integration level are tied directly to the integration (e.g. `SlackIntegrationHandler`,
|
||||||
|
`WebhookIntegrationHandler`). These handlers take in `IntegrationMessage<T>` and output
|
||||||
|
`IntegrationHandlerResult`, which tells the listener the outcome of the integration (e.g. success / fail,
|
||||||
|
if it can be retried and any minimum delay that should occur). This makes them easy to unit test in isolation
|
||||||
|
without any of the concerns of AMQP or messaging.
|
||||||
|
|
||||||
|
The listeners at this level are responsible for firing off the handler when a new message comes in and then
|
||||||
|
taking the correct action based on the result. Successful results simply acknowledge the message and resolve.
|
||||||
|
Failures will either be sent to the dead letter queue (DLQ) or re-published for retry after the correct amount of delay.
|
||||||
|
|
||||||
|
### Retries
|
||||||
|
|
||||||
|
One of the goals of introducing the integration level is to simplify and enable the process of multiple retries
|
||||||
|
for a specific event integration. For instance, if a service is temporarily down, we don't want one of our handlers
|
||||||
|
blocking the rest of the queue while it waits to retry. In addition, we don't want to retry _all_ integrations for a
|
||||||
|
specific event if only one integration fails nor do we want to re-lookup the configuration details. By splitting
|
||||||
|
out the `IntegrationMessage<T>` with the configuration, message, and details around retries, we can process each
|
||||||
|
event / integration individually and retry easily.
|
||||||
|
|
||||||
|
When the `IntegrationHandlerResult.Success` is set to `false` (indicating that the integration attempt failed) the
|
||||||
|
`Retryable` flag tells the listener whether this failure is temporary or final. If the `Retryable` is `false`, then
|
||||||
|
the message is immediately sent to the DLQ. If it is `true`, the listener uses the `ApplyRetry(DateTime)` method
|
||||||
|
in `IntegrationMessage` which handles both incrementing the `RetryCount` and updating the `DelayUntilDate` using
|
||||||
|
the provided DateTime, but also adding exponential backoff (based on `RetryCount`) and jitter. The listener compares
|
||||||
|
the `RetryCount` in the `IntegrationMessage` to see if it's over the `MaxRetries` defined in Global Settings. If it
|
||||||
|
is over the `MaxRetries`, the message is sent to the DLQ. Otherwise, it is scheduled for retry.
|
||||||
|
|
||||||
|
``` mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Success == false] --> B{Retryable?}
|
||||||
|
B -- No --> C[Send to Dead Letter Queue DLQ]
|
||||||
|
B -- Yes --> D[Check RetryCount vs MaxRetries]
|
||||||
|
D -->|RetryCount >= MaxRetries| E[Send to Dead Letter Queue DLQ]
|
||||||
|
D -->|RetryCount < MaxRetries| F[Schedule for Retry]
|
||||||
|
```
|
||||||
|
|
||||||
|
Azure Service Bus supports scheduling messages as part of its core functionality. Retries are scheduled to a specific
|
||||||
|
time and then ASB holds the message and publishes it at the correct time.
|
||||||
|
|
||||||
|
#### RabbitMQ retry options
|
||||||
|
|
||||||
|
For RabbitMQ (which will be used by self-host only), we have two different options. The `useDelayPlugin` flag in
|
||||||
|
`GlobalSettings.RabbitMqSettings` determines which one is used. If it is set to `true`, we use the delay plugin. It
|
||||||
|
defaults to `false` which indicates we should use retry queues with a timing check.
|
||||||
|
|
||||||
|
1. Delay plugin
|
||||||
|
- [Delay plugin GitHub repo](https://github.com/rabbitmq/rabbitmq-delayed-message-exchange)
|
||||||
|
- This plugin enables a delayed message exchange in RabbitMQ that supports delaying a message for an amount
|
||||||
|
of time specified in a special header.
|
||||||
|
- This allows us to forego using any retry queues and rely instead on the delay exchange. When a message is
|
||||||
|
marked with the header it gets published to the exchange and the exchange handles all the functionality of
|
||||||
|
holding it until the appropriate time (similar to ASB's built-in support).
|
||||||
|
- The plugin must be setup and enabled before turning this option on (which is why it defaults to off).
|
||||||
|
|
||||||
|
2. Retry queues + timing check
|
||||||
|
- If the delay plugin setting is off, we push the message to a retry queue which has a fixed amount of time before
|
||||||
|
it gets re-published back to the main queue.
|
||||||
|
- When a message comes off the queue, we check to see if the `DelayUntilDate` has already passed.
|
||||||
|
- If it has passed, we then handle the integration normally and retry the request.
|
||||||
|
- If it is still in the future, we put the message back on the retry queue for an additional wait.
|
||||||
|
- While this does use extra processing, it gives us better support for honoring the delays even if the delay plugin
|
||||||
|
isn't enabled. Since this solution is only intended for self-host, it should be a pretty minimal impact with short
|
||||||
|
delays and a small number of retries.
|
||||||
|
|
||||||
|
## Listener / Handler pattern
|
||||||
|
|
||||||
|
To make it easy to support multiple AMQP services (RabbitMQ and Azure Service Bus), the act
|
||||||
|
of listening to the stream of messages is decoupled from the act of responding to a message.
|
||||||
|
|
||||||
|
### Listeners
|
||||||
|
|
||||||
|
- Listeners handle the details of the communication platform (i.e. RabbitMQ and Azure Service Bus).
|
||||||
|
- There is one listener for each platform (RabbitMQ / ASB) for each of the two levels - i.e. one event listener
|
||||||
|
and one integration listener.
|
||||||
|
- Perform all the aspects of setup / teardown, subscription, message acknowledgement, etc. for the messaging platform,
|
||||||
|
but do not directly process any events themselves. Instead, they delegate to the handler with which they
|
||||||
|
are configured.
|
||||||
|
- Multiple instances can be configured to run independently, each with its own handler and
|
||||||
|
subscription / queue.
|
||||||
|
|
||||||
|
### Handlers
|
||||||
|
|
||||||
|
- One handler per queue / subscription (e.g. per integration at the integration level).
|
||||||
|
- Completely isolated from and know nothing of the messaging platform in use. This allows them to be
|
||||||
|
freely reused across different communication platforms.
|
||||||
|
- Perform all aspects of handling an event.
|
||||||
|
- Allows them to be highly testable as they are isolated and decoupled from the more complicated
|
||||||
|
aspects of messaging.
|
||||||
|
|
||||||
|
This combination allows for a configuration inside of `ServiceCollectionExtensions.cs` that pairs
|
||||||
|
instances of the listener service for the currently running messaging platform with any number of
|
||||||
|
handlers. It also allows for quick development of new handlers as they are focused only on the
|
||||||
|
task of handling a specific event.
|
||||||
|
|
||||||
|
## Publishers and Services
|
||||||
|
|
||||||
|
Listeners (and `EventIntegrationHandler`) interact with the messaging system via the `IEventPublisher` interface,
|
||||||
|
which is backed by a RabbitMQ and ASB specific service. By placing most of the messaging platform details in the
|
||||||
|
service layer, we are able to handle common things like configuring the connection, binding or creating a specific
|
||||||
|
queue, etc. in one place. The `IRabbitMqService` and `IAzureServiceBusService` implement the `IEventPublisher`
|
||||||
|
interface and therefore can also handle directly all the message publishing functionality.
|
||||||
|
|
||||||
|
## Integrations and integration configurations
|
||||||
|
|
||||||
|
Organizations can configure integration configurations to send events to different endpoints -- each
|
||||||
|
handler maps to a specific integration and checks for the configuration when it receives an event.
|
||||||
|
Currently, there are integrations / handlers for Slack and webhooks (as mentioned above).
|
||||||
|
|
||||||
|
### `OrganizationIntegration`
|
||||||
|
|
||||||
|
- The top-level object that enables a specific integration for the organization.
|
||||||
|
- Includes any properties that apply to the entire integration across all events.
|
||||||
|
- For Slack, it consists of the token: `{ "token": "xoxb-token-from-slack" }`
|
||||||
|
- For webhooks, it is `null`. However, even though there is no configuration, an organization must
|
||||||
|
have a webhook `OrganizationIntegration` to enable configuration via `OrganizationIntegrationConfiguration`.
|
||||||
|
|
||||||
|
### `OrganizationIntegrationConfiguration`
|
||||||
|
|
||||||
|
- This contains the configurations specific to each `EventType` for the integration.
|
||||||
|
- `Configuration` contains the event-specific configuration.
|
||||||
|
- For Slack, this would contain what channel to send the message to: `{ "channelId": "C123456" }`
|
||||||
|
- For Webhook, this is the URL the request should be sent to: `{ "url": "https://api.example.com" }`
|
||||||
|
- `Template` contains a template string that is expected to be filled in with the contents of the actual event.
|
||||||
|
- The tokens in the string are wrapped in `#` characters. For instance, the UserId would be `#UserId#`.
|
||||||
|
- The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with introspected values from
|
||||||
|
the provided `EventMessage`.
|
||||||
|
- The template does not enforce any structure — it could be a freeform text message to send via Slack, or a
|
||||||
|
JSON body to send via webhook; it is simply stored and used as a string for the most flexibility.
|
||||||
|
|
||||||
|
### `OrganizationIntegrationConfigurationDetails`
|
||||||
|
|
||||||
|
- This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into
|
||||||
|
a single object. The combined contents tell the integration's handler all the details needed to send to an
|
||||||
|
external service.
|
||||||
|
- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from
|
||||||
|
the database to determine what to publish at the integration level.
|
||||||
|
|
||||||
|
# Building a new integration
|
||||||
|
|
||||||
|
These are all the pieces required in the process of building out a new integration. For
|
||||||
|
clarity in naming, these assume a new integration called "Example".
|
||||||
|
|
||||||
|
## IntegrationType
|
||||||
|
|
||||||
|
Add a new type to `IntegrationType` for the new integration.
|
||||||
|
|
||||||
|
## Configuration Models
|
||||||
|
|
||||||
|
The configuration models are the classes that will determine what is stored in the database for
|
||||||
|
`OrganizationIntegration` and `OrganizationIntegrationConfiguration`. The `Configuration` columns are the
|
||||||
|
serialized version of the corresponding objects and represent the coonfiguration details for this integration
|
||||||
|
and event type.
|
||||||
|
|
||||||
|
1. `ExampleIntegration`
|
||||||
|
- Configuration details for the whole integration (e.g. a token in Slack).
|
||||||
|
- Applies to every event type configuration defined for this integration.
|
||||||
|
- Maps to the JSON structure stored in `Configuration` in ``OrganizationIntegration`.
|
||||||
|
2. `ExampleIntegrationConfiguration`
|
||||||
|
- Configuration details that could change from event to event (e.g. channelId in Slack).
|
||||||
|
- Maps to the JSON structure stored in `Configuration` in `OrganizationIntegrationConfiguration`.
|
||||||
|
3. `ExampleIntegrationConfigurationDetails`
|
||||||
|
- Combined configuration of both Integration _and_ IntegrationConfiguration.
|
||||||
|
- This will be the deserialized version of the `MergedConfiguration` in
|
||||||
|
`OrganizationIntegrationConfigurationDetails`.
|
||||||
|
|
||||||
|
## Request Models
|
||||||
|
|
||||||
|
1. Add a new case to the switch method in `OrganizationIntegrationRequestModel.Validate`.
|
||||||
|
2. Add a new case to the switch method in `OrganizationIntegrationConfigurationRequestModel.IsValidForType`.
|
||||||
|
|
||||||
|
## Integration Handler
|
||||||
|
|
||||||
|
e.g. `ExampleIntegrationHandler`
|
||||||
|
- This is where the actual code will go to perform the integration (i.e. send an HTTP request, etc.).
|
||||||
|
- Handlers receive an `IntegrationMessage<T>` where `<T>` is the `ExampleIntegrationConfigurationDetails`
|
||||||
|
defined above. This has the Configuration as well as the rendered template message to be sent.
|
||||||
|
- Handlers return an `IntegrationHandlerResult` with details about if the request - success / failure,
|
||||||
|
if it can be retried, when it should be delayed until, etc.
|
||||||
|
- The scope of the handler is simply to do the integration and report the result.
|
||||||
|
Everything else (such as how many times to retry, when to retry, what to do with failures)
|
||||||
|
is done in the Listener.
|
||||||
|
|
||||||
|
## GlobalSettings
|
||||||
|
|
||||||
|
### RabbitMQ
|
||||||
|
Add the queue names for the integration. These are typically set with a default value so
|
||||||
|
that they will be created when first accessed in code by RabbitMQ.
|
||||||
|
|
||||||
|
1. `ExampleEventQueueName`
|
||||||
|
2. `ExampleIntegrationQueueName`
|
||||||
|
3. `ExampleIntegrationRetryQueueName`
|
||||||
|
|
||||||
|
### Azure Service Bus
|
||||||
|
Add the subscription names to use for ASB for this integration. Similar to RabbitMQ a
|
||||||
|
default value is provided so that we don't require configuring it in secrets but allow
|
||||||
|
it to be overridden. **However**, unlike RabbitMQ these subscriptions must exist prior
|
||||||
|
to the code accessing them. They will not be created on the fly. See [Deploying a new
|
||||||
|
integration](#deploying-a-new-integration) below
|
||||||
|
|
||||||
|
1. `ExmpleEventSubscriptionName`
|
||||||
|
2. `ExmpleIntegrationSubscriptionName`
|
||||||
|
|
||||||
|
#### Service Bus Emulator, local config
|
||||||
|
In order to create ASB resources locally, we need to also update the `servicebusemulator_config.json` file
|
||||||
|
to include any new subscriptions.
|
||||||
|
- Under the existing event topic (`event-logging`) add a subscription for the event level for this
|
||||||
|
new integration (`events-example-subscription`).
|
||||||
|
- Under the existing integration topic (`event-integrations`) add a new subscription for the integration
|
||||||
|
level messages (`integration-example-subscription`).
|
||||||
|
- Copy the correlation filter from the other integration level subscriptions. It should filter based on
|
||||||
|
the `IntegrationType.ToRoutingKey`, or in this example `example`.
|
||||||
|
|
||||||
|
These names added here are what must match the values provided in the secrets or the defaults provided
|
||||||
|
in Global Settings. This must be in place (and the local ASB emulator restarted) before you can use any
|
||||||
|
code locally that accesses ASB resources.
|
||||||
|
|
||||||
|
## ServiceCollectionExtensions
|
||||||
|
In our `ServiceCollectionExtensions`, we pull all the above pieces together to start listeners on each message
|
||||||
|
tier with handlers to process the integration. There are a number of helper methods in here to make this simple
|
||||||
|
to add a new integration - one call per platform.
|
||||||
|
|
||||||
|
Also note that if an integration needs a custom singleton / service defined, the add listeners method is a
|
||||||
|
good place to set that up. For instance, `SlackIntegrationHandler` needs a `SlackService`, so the singleton
|
||||||
|
declaration is right above the add integration method for slack. Same thing for webhooks when it comes to
|
||||||
|
defining a custom HttpClient by name.
|
||||||
|
|
||||||
|
1. In `AddRabbitMqListeners` add the integration:
|
||||||
|
``` csharp
|
||||||
|
services.AddRabbitMqIntegration<ExampleIntegrationConfigurationDetails, ExampleIntegrationHandler>(
|
||||||
|
globalSettings.EventLogging.RabbitMq.ExampleEventsQueueName,
|
||||||
|
globalSettings.EventLogging.RabbitMq.ExampleIntegrationQueueName,
|
||||||
|
globalSettings.EventLogging.RabbitMq.ExampleIntegrationRetryQueueName,
|
||||||
|
globalSettings.EventLogging.RabbitMq.MaxRetries,
|
||||||
|
IntegrationType.Example);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `AddAzureServiceBusListeners` add the integration:
|
||||||
|
``` csharp
|
||||||
|
services.AddAzureServiceBusIntegration<ExampleIntegrationConfigurationDetails, ExampleIntegrationHandler>(
|
||||||
|
eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleEventSubscriptionName,
|
||||||
|
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.ExampleIntegrationSubscriptionName,
|
||||||
|
integrationType: IntegrationType.Example,
|
||||||
|
globalSettings: globalSettings);
|
||||||
|
```
|
||||||
|
|
||||||
|
# Deploying a new integration
|
||||||
|
|
||||||
|
## RabbitMQ
|
||||||
|
|
||||||
|
RabbitMQ dynamically creates queues and exchanges when they are first accessed in code.
|
||||||
|
Therefore, there is no need to manually create queues when deploying a new integration.
|
||||||
|
They can be created and configured ahead of time, but it's not required. Note that once
|
||||||
|
they are created, if any configurations need to be changed, the queue or exchange must be
|
||||||
|
deleted and recreated.
|
||||||
|
|
||||||
|
## Azure Service Bus
|
||||||
|
|
||||||
|
Unlike RabbitMQ, ASB resources **must** be allocated before the code accesses them and
|
||||||
|
will not be created on the fly. This means that any subscriptions needed for a new
|
||||||
|
integration must be created in ASB before that code is deployed.
|
||||||
|
|
||||||
|
The two subscriptions created above in Global Settings and `servicebusemulator_config.json`
|
||||||
|
need to be created in the Azure portal or CLI for the environment before deploying the
|
||||||
|
code.
|
||||||
|
|
||||||
|
1. `ExmpleEventSubscriptionName`
|
||||||
|
- This subscription is a fan-out subscription from the main event topic.
|
||||||
|
- As such, it will start receiving all the events as soon as it is declared.
|
||||||
|
- This can create a backlog before the integration-specific handler is declared and deployed.
|
||||||
|
- One strategy to avoid this is to create the subscription with a false filter (e.g. `1 = 0`).
|
||||||
|
- This will create the subscription, but the filter will ensure that no messages
|
||||||
|
actually land in the subscription.
|
||||||
|
- Code can be deployed that references the subscription, because the subscription
|
||||||
|
legitimately exists (it is simply empty).
|
||||||
|
- When the code is in place, and we're ready to start receiving messages on the new
|
||||||
|
integration, we simply remove the filter to return the subscription to receiving
|
||||||
|
all messages via fan-out.
|
||||||
|
2. `ExmpleIntegrationSubscriptionName`
|
||||||
|
- This subscription must be created before the new integration code can be deployed.
|
||||||
|
- However, it is not fan-out, but rather a filter based on the `IntegrationType.ToRoutingKey`.
|
||||||
|
- Therefore, it won't start receiving messages until organizations have active configurations.
|
||||||
|
This means there's no risk of building up a backlog by declaring it ahead of time.
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
@ -20,6 +20,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
|||||||
private readonly Lazy<Task<IChannel>> _lazyChannel;
|
private readonly Lazy<Task<IChannel>> _lazyChannel;
|
||||||
private readonly IRabbitMqService _rabbitMqService;
|
private readonly IRabbitMqService _rabbitMqService;
|
||||||
private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
|
private readonly ILogger<RabbitMqIntegrationListenerService> _logger;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
|
public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
|
||||||
string routingKey,
|
string routingKey,
|
||||||
@ -27,7 +28,8 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
|||||||
string retryQueueName,
|
string retryQueueName,
|
||||||
int maxRetries,
|
int maxRetries,
|
||||||
IRabbitMqService rabbitMqService,
|
IRabbitMqService rabbitMqService,
|
||||||
ILogger<RabbitMqIntegrationListenerService> logger)
|
ILogger<RabbitMqIntegrationListenerService> logger,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_handler = handler;
|
_handler = handler;
|
||||||
_routingKey = routingKey;
|
_routingKey = routingKey;
|
||||||
@ -35,6 +37,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
|||||||
_queueName = queueName;
|
_queueName = queueName;
|
||||||
_rabbitMqService = rabbitMqService;
|
_rabbitMqService = rabbitMqService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
_maxRetries = maxRetries;
|
_maxRetries = maxRetries;
|
||||||
_lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
|
_lazyChannel = new Lazy<Task<IChannel>>(() => _rabbitMqService.CreateChannelAsync());
|
||||||
}
|
}
|
||||||
@ -74,7 +77,7 @@ public class RabbitMqIntegrationListenerService : BackgroundService
|
|||||||
var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json);
|
var integrationMessage = JsonSerializer.Deserialize<IntegrationMessage>(json);
|
||||||
if (integrationMessage is not null &&
|
if (integrationMessage is not null &&
|
||||||
integrationMessage.DelayUntilDate.HasValue &&
|
integrationMessage.DelayUntilDate.HasValue &&
|
||||||
integrationMessage.DelayUntilDate.Value > DateTime.UtcNow)
|
integrationMessage.DelayUntilDate.Value > _timeProvider.GetUtcNow().UtcDateTime)
|
||||||
{
|
{
|
||||||
await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);
|
await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea);
|
||||||
await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
@ -1,6 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
@ -3,13 +3,15 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
|
public class WebhookIntegrationHandler(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
TimeProvider timeProvider)
|
||||||
: IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
: IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||||
@ -39,7 +41,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
|
|||||||
if (int.TryParse(value, out var seconds))
|
if (int.TryParse(value, out var seconds))
|
||||||
{
|
{
|
||||||
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
|
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
|
||||||
result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds);
|
result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
|
||||||
}
|
}
|
||||||
else if (DateTimeOffset.TryParseExact(value,
|
else if (DateTimeOffset.TryParseExact(value,
|
||||||
"r", // "r" is the round-trip format: RFC1123
|
"r", // "r" is the round-trip format: RFC1123
|
@ -107,11 +107,11 @@ public static class FeatureFlagKeys
|
|||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||||
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
||||||
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
|
|
||||||
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
|
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
|
||||||
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
||||||
public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript";
|
public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript";
|
||||||
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
|
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
|
||||||
|
public const string CreateDefaultLocation = "pm-19467-create-default-location";
|
||||||
|
|
||||||
/* Auth Team */
|
/* Auth Team */
|
||||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||||
@ -181,6 +181,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
||||||
public const string MobileErrorReporting = "mobile-error-reporting";
|
public const string MobileErrorReporting = "mobile-error-reporting";
|
||||||
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
||||||
|
public const string UserManagedPrivilegedApps = "pm-18970-user-managed-privileged-apps";
|
||||||
public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings";
|
public const string EnablePMPreloginSettings = "enable-pm-prelogin-settings";
|
||||||
public const string AppIntents = "app-intents";
|
public const string AppIntents = "app-intents";
|
||||||
|
|
||||||
@ -205,6 +206,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||||
public const string PhishingDetection = "phishing-detection";
|
public const string PhishingDetection = "phishing-detection";
|
||||||
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
|
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
|
||||||
|
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
namespace Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
|
public class MemberAccessReportDetail
|
||||||
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public bool TwoFactorEnabled { get; set; }
|
||||||
|
public bool AccountRecoveryEnabled { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
public Guid? CollectionId { get; set; }
|
||||||
|
public Guid? GroupId { get; set; }
|
||||||
|
public string GroupName { get; set; }
|
||||||
|
public string CollectionName { get; set; }
|
||||||
|
public bool? ReadOnly { get; set; }
|
||||||
|
public bool? HidePasswords { get; set; }
|
||||||
|
public bool? Manage { get; set; }
|
||||||
|
public IEnumerable<Guid> CipherIds { get; set; }
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
namespace Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
|
public class OrganizationMemberBaseDetail
|
||||||
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public string TwoFactorProviders { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
public string ResetPasswordKey { get; set; }
|
||||||
|
public Guid? CollectionId { get; set; }
|
||||||
|
public Guid? GroupId { get; set; }
|
||||||
|
public string GroupName { get; set; }
|
||||||
|
public string CollectionName { get; set; }
|
||||||
|
public bool? ReadOnly { get; set; }
|
||||||
|
public bool? HidePasswords { get; set; }
|
||||||
|
public bool? Manage { get; set; }
|
||||||
|
public Guid CipherId { get; set; }
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
namespace Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
|
public class RiskInsightsReportDetail
|
||||||
|
{
|
||||||
|
public Guid? UserGuid { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Email { get; set; }
|
||||||
|
public bool UsesKeyConnector { get; set; }
|
||||||
|
public IEnumerable<string> CipherIds { get; set; }
|
||||||
|
}
|
@ -1,206 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
|
||||||
using Bit.Core.Dirt.Reports.Models.Data;
|
|
||||||
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
|
||||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
|
||||||
using Bit.Core.Entities;
|
|
||||||
using Bit.Core.Models.Data;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Vault.Models.Data;
|
|
||||||
using Bit.Core.Vault.Queries;
|
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
|
||||||
|
|
||||||
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
|
||||||
|
|
||||||
public class MemberAccessCipherDetailsQuery : IMemberAccessCipherDetailsQuery
|
|
||||||
{
|
|
||||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
|
||||||
private readonly IGroupRepository _groupRepository;
|
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
|
||||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
|
||||||
|
|
||||||
public MemberAccessCipherDetailsQuery(
|
|
||||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
|
||||||
IGroupRepository groupRepository,
|
|
||||||
ICollectionRepository collectionRepository,
|
|
||||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
|
||||||
IApplicationCacheService applicationCacheService,
|
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
|
||||||
_groupRepository = groupRepository;
|
|
||||||
_collectionRepository = collectionRepository;
|
|
||||||
_organizationCiphersQuery = organizationCiphersQuery;
|
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request)
|
|
||||||
{
|
|
||||||
var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
|
||||||
new OrganizationUserUserDetailsQueryRequest
|
|
||||||
{
|
|
||||||
OrganizationId = request.OrganizationId,
|
|
||||||
IncludeCollections = true,
|
|
||||||
IncludeGroups = true
|
|
||||||
});
|
|
||||||
|
|
||||||
var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(request.OrganizationId);
|
|
||||||
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);
|
|
||||||
var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(request.OrganizationId);
|
|
||||||
var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(request.OrganizationId);
|
|
||||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
|
||||||
|
|
||||||
var memberAccessCipherDetails = GenerateAccessDataParallel(
|
|
||||||
orgGroups,
|
|
||||||
orgCollectionsWithAccess,
|
|
||||||
orgItems,
|
|
||||||
organizationUsersTwoFactorEnabled,
|
|
||||||
orgAbility);
|
|
||||||
|
|
||||||
return memberAccessCipherDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates a report for all members of an organization. Containing summary information
|
|
||||||
/// such as item, collection, and group counts. Including the cipherIds a member is assigned.
|
|
||||||
/// Child collection includes detailed information on the user and group collections along
|
|
||||||
/// with their permissions.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="orgGroups">Organization groups collection</param>
|
|
||||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
|
||||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
|
||||||
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
|
||||||
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
|
||||||
/// <returns>List of the MemberAccessCipherDetailsModel</returns>;
|
|
||||||
private IEnumerable<MemberAccessCipherDetails> GenerateAccessDataParallel(
|
|
||||||
ICollection<Group> orgGroups,
|
|
||||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
|
||||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
|
||||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
|
||||||
OrganizationAbility orgAbility)
|
|
||||||
{
|
|
||||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user).ToList();
|
|
||||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
|
||||||
var collectionItems = orgItems
|
|
||||||
.SelectMany(x => x.CollectionIds,
|
|
||||||
(cipher, collectionId) => new { Cipher = cipher, CollectionId = collectionId })
|
|
||||||
.GroupBy(y => y.CollectionId,
|
|
||||||
(key, ciphers) => new { CollectionId = key, Ciphers = ciphers });
|
|
||||||
var itemLookup = collectionItems.ToDictionary(x => x.CollectionId.ToString(), x => x.Ciphers.Select(c => c.Cipher.Id.ToString()).ToList());
|
|
||||||
|
|
||||||
var memberAccessCipherDetails = new ConcurrentBag<MemberAccessCipherDetails>();
|
|
||||||
|
|
||||||
Parallel.ForEach(orgUsers, user =>
|
|
||||||
{
|
|
||||||
var groupAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
var userCollectionAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
|
|
||||||
foreach (var tCollect in orgCollectionsWithAccess)
|
|
||||||
{
|
|
||||||
if (itemLookup.TryGetValue(tCollect.Item1.Id.ToString(), out var items))
|
|
||||||
{
|
|
||||||
var itemCounts = items.Count;
|
|
||||||
|
|
||||||
if (tCollect.Item2.Groups.Any())
|
|
||||||
{
|
|
||||||
var groupDetails = tCollect.Item2.Groups
|
|
||||||
.Where(tCollectGroups => user.Groups.Contains(tCollectGroups.Id))
|
|
||||||
.Select(x => new MemberAccessDetails
|
|
||||||
{
|
|
||||||
CollectionId = tCollect.Item1.Id,
|
|
||||||
CollectionName = tCollect.Item1.Name,
|
|
||||||
GroupId = x.Id,
|
|
||||||
GroupName = groupNameDictionary[x.Id],
|
|
||||||
ReadOnly = x.ReadOnly,
|
|
||||||
HidePasswords = x.HidePasswords,
|
|
||||||
Manage = x.Manage,
|
|
||||||
ItemCount = itemCounts,
|
|
||||||
CollectionCipherIds = items
|
|
||||||
});
|
|
||||||
|
|
||||||
groupAccessDetails.AddRange(groupDetails);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tCollect.Item2.Users.Any())
|
|
||||||
{
|
|
||||||
var userCollectionDetails = tCollect.Item2.Users
|
|
||||||
.Where(tCollectUser => tCollectUser.Id == user.Id)
|
|
||||||
.Select(x => new MemberAccessDetails
|
|
||||||
{
|
|
||||||
CollectionId = tCollect.Item1.Id,
|
|
||||||
CollectionName = tCollect.Item1.Name,
|
|
||||||
ReadOnly = x.ReadOnly,
|
|
||||||
HidePasswords = x.HidePasswords,
|
|
||||||
Manage = x.Manage,
|
|
||||||
ItemCount = itemCounts,
|
|
||||||
CollectionCipherIds = items
|
|
||||||
});
|
|
||||||
|
|
||||||
userCollectionAccessDetails.AddRange(userCollectionDetails);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var report = new MemberAccessCipherDetails
|
|
||||||
{
|
|
||||||
UserName = user.Name,
|
|
||||||
Email = user.Email,
|
|
||||||
TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
|
|
||||||
AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
|
|
||||||
UserGuid = user.Id,
|
|
||||||
UsesKeyConnector = user.UsesKeyConnector
|
|
||||||
};
|
|
||||||
|
|
||||||
var userAccessDetails = new List<MemberAccessDetails>();
|
|
||||||
if (user.Groups.Any())
|
|
||||||
{
|
|
||||||
var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
|
|
||||||
userAccessDetails.AddRange(userGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
|
|
||||||
if (groupsWithoutCollections.Any())
|
|
||||||
{
|
|
||||||
var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessDetails
|
|
||||||
{
|
|
||||||
GroupId = x,
|
|
||||||
GroupName = groupNameDictionary[x],
|
|
||||||
ItemCount = 0
|
|
||||||
});
|
|
||||||
userAccessDetails.AddRange(emptyGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.Collections.Any())
|
|
||||||
{
|
|
||||||
var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
|
|
||||||
userAccessDetails.AddRange(userCollections);
|
|
||||||
}
|
|
||||||
report.AccessDetails = userAccessDetails;
|
|
||||||
|
|
||||||
var userCiphers = report.AccessDetails
|
|
||||||
.Where(x => x.ItemCount > 0)
|
|
||||||
.SelectMany(y => y.CollectionCipherIds)
|
|
||||||
.Distinct();
|
|
||||||
report.CipherIds = userCiphers;
|
|
||||||
report.TotalItemCount = userCiphers.Count();
|
|
||||||
|
|
||||||
var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
|
|
||||||
report.CollectionsCount = distinctItems.Count();
|
|
||||||
report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
|
|
||||||
|
|
||||||
memberAccessCipherDetails.Add(report);
|
|
||||||
});
|
|
||||||
|
|
||||||
return memberAccessCipherDetails;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,65 @@
|
|||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
|
public class MemberAccessReportQuery(
|
||||||
|
IOrganizationMemberBaseDetailRepository organizationMemberBaseDetailRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IApplicationCacheService applicationCacheService)
|
||||||
|
: IMemberAccessReportQuery
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessReportsAsync(
|
||||||
|
MemberAccessReportRequest request)
|
||||||
|
{
|
||||||
|
var baseDetails =
|
||||||
|
await organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(
|
||||||
|
request.OrganizationId);
|
||||||
|
|
||||||
|
var orgUsers = baseDetails.Select(x => x.UserGuid.GetValueOrDefault()).Distinct();
|
||||||
|
var orgUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||||
|
|
||||||
|
var orgAbility = await applicationCacheService.GetOrganizationAbilityAsync(request.OrganizationId);
|
||||||
|
|
||||||
|
var accessDetails = baseDetails
|
||||||
|
.GroupBy(b => new
|
||||||
|
{
|
||||||
|
b.UserGuid,
|
||||||
|
b.UserName,
|
||||||
|
b.Email,
|
||||||
|
b.TwoFactorProviders,
|
||||||
|
b.ResetPasswordKey,
|
||||||
|
b.UsesKeyConnector,
|
||||||
|
b.GroupId,
|
||||||
|
b.GroupName,
|
||||||
|
b.CollectionId,
|
||||||
|
b.CollectionName,
|
||||||
|
b.ReadOnly,
|
||||||
|
b.HidePasswords,
|
||||||
|
b.Manage
|
||||||
|
})
|
||||||
|
.Select(g => new MemberAccessReportDetail
|
||||||
|
{
|
||||||
|
UserGuid = g.Key.UserGuid,
|
||||||
|
UserName = g.Key.UserName,
|
||||||
|
Email = g.Key.Email,
|
||||||
|
TwoFactorEnabled = orgUsersTwoFactorEnabled.FirstOrDefault(x => x.userId == g.Key.UserGuid).twoFactorIsEnabled,
|
||||||
|
AccountRecoveryEnabled = !string.IsNullOrWhiteSpace(g.Key.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||||
|
UsesKeyConnector = g.Key.UsesKeyConnector,
|
||||||
|
GroupId = g.Key.GroupId,
|
||||||
|
GroupName = g.Key.GroupName,
|
||||||
|
CollectionId = g.Key.CollectionId,
|
||||||
|
CollectionName = g.Key.CollectionName,
|
||||||
|
ReadOnly = g.Key.ReadOnly,
|
||||||
|
HidePasswords = g.Key.HidePasswords,
|
||||||
|
Manage = g.Key.Manage,
|
||||||
|
CipherIds = g.Select(c => c.CipherId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessDetails;
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
|||||||
|
|
||||||
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
|
||||||
public interface IMemberAccessCipherDetailsQuery
|
public interface IMemberAccessReportQuery
|
||||||
{
|
{
|
||||||
Task<IEnumerable<MemberAccessCipherDetails>> GetMemberAccessCipherDetails(MemberAccessCipherDetailsRequest request);
|
Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessReportsAsync(MemberAccessReportRequest request);
|
||||||
}
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
|
||||||
|
public interface IRiskInsightsReportQuery
|
||||||
|
{
|
||||||
|
Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(RiskInsightsReportRequest request);
|
||||||
|
}
|
@ -8,7 +8,8 @@ public static class ReportingServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
public static void AddReportingServices(this IServiceCollection services)
|
public static void AddReportingServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IMemberAccessCipherDetailsQuery, MemberAccessCipherDetailsQuery>();
|
services.AddScoped<IRiskInsightsReportQuery, RiskInsightsReportQuery>();
|
||||||
|
services.AddScoped<IMemberAccessReportQuery, MemberAccessReportQuery>();
|
||||||
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
|
services.AddScoped<IAddPasswordHealthReportApplicationCommand, AddPasswordHealthReportApplicationCommand>();
|
||||||
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
|
services.AddScoped<IGetPasswordHealthReportApplicationQuery, GetPasswordHealthReportApplicationQuery>();
|
||||||
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();
|
services.AddScoped<IDropPasswordHealthReportApplicationCommand, DropPasswordHealthReportApplicationCommand>();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
public class MemberAccessCipherDetailsRequest
|
public class MemberAccessReportRequest
|
||||||
{
|
{
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
}
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
|
||||||
|
public class RiskInsightsReportRequest
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.OrganizationReportMembers.Interfaces;
|
||||||
|
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Dirt.Reports.ReportFeatures;
|
||||||
|
|
||||||
|
public class RiskInsightsReportQuery : IRiskInsightsReportQuery
|
||||||
|
{
|
||||||
|
private readonly IOrganizationMemberBaseDetailRepository _organizationMemberBaseDetailRepository;
|
||||||
|
|
||||||
|
public RiskInsightsReportQuery(IOrganizationMemberBaseDetailRepository repository)
|
||||||
|
{
|
||||||
|
_organizationMemberBaseDetailRepository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<RiskInsightsReportDetail>> GetRiskInsightsReportDetails(
|
||||||
|
RiskInsightsReportRequest request)
|
||||||
|
{
|
||||||
|
var baseDetails =
|
||||||
|
await _organizationMemberBaseDetailRepository.GetOrganizationMemberBaseDetailsByOrganizationId(
|
||||||
|
request.OrganizationId);
|
||||||
|
|
||||||
|
var insightsDetails = baseDetails
|
||||||
|
.GroupBy(b => new { b.UserGuid, b.UserName, b.Email, b.UsesKeyConnector })
|
||||||
|
.Select(g => new RiskInsightsReportDetail
|
||||||
|
{
|
||||||
|
UserGuid = g.Key.UserGuid,
|
||||||
|
UserName = g.Key.UserName,
|
||||||
|
Email = g.Key.Email,
|
||||||
|
UsesKeyConnector = g.Key.UsesKeyConnector,
|
||||||
|
CipherIds = g
|
||||||
|
.Select(x => x.CipherId.ToString())
|
||||||
|
.Distinct()
|
||||||
|
});
|
||||||
|
|
||||||
|
return insightsDetails;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
|
||||||
|
public interface IOrganizationMemberBaseDetailRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(Guid organizationId);
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.KeyManagement.Commands;
|
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||||
|
using Bit.Core.KeyManagement.Commands;
|
||||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||||
|
using Bit.Core.KeyManagement.Queries;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Core.KeyManagement;
|
namespace Bit.Core.KeyManagement;
|
||||||
@ -9,10 +11,16 @@ public static class KeyManagementServiceCollectionExtensions
|
|||||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddKeyManagementCommands();
|
services.AddKeyManagementCommands();
|
||||||
|
services.AddKeyManagementQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddKeyManagementCommands(this IServiceCollection services)
|
private static void AddKeyManagementCommands(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddKeyManagementQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,22 @@ using Bit.Core.Enums;
|
|||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
public class PushSendRequestModel : IValidatableObject
|
public class PushSendRequestModel<T> : IValidatableObject
|
||||||
{
|
{
|
||||||
public string? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
public string? OrganizationId { get; set; }
|
public Guid? OrganizationId { get; set; }
|
||||||
public string? DeviceId { get; set; }
|
public Guid? DeviceId { get; set; }
|
||||||
public string? Identifier { get; set; }
|
public string? Identifier { get; set; }
|
||||||
public required PushType Type { get; set; }
|
public required PushType Type { get; set; }
|
||||||
public required object Payload { get; set; }
|
public required T Payload { get; set; }
|
||||||
public ClientType? ClientType { get; set; }
|
public ClientType? ClientType { get; set; }
|
||||||
public string? InstallationId { get; set; }
|
public Guid? InstallationId { get; set; }
|
||||||
|
|
||||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(UserId) &&
|
if (!UserId.HasValue &&
|
||||||
string.IsNullOrWhiteSpace(OrganizationId) &&
|
!OrganizationId.HasValue &&
|
||||||
string.IsNullOrWhiteSpace(InstallationId))
|
!InstallationId.HasValue)
|
||||||
{
|
{
|
||||||
yield return new ValidationResult(
|
yield return new ValidationResult(
|
||||||
$"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required.");
|
$"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required.");
|
||||||
|
@ -1,21 +1,17 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Platform.Push.Internal;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Notification = Bit.Core.NotificationCenter.Entities.Notification;
|
|
||||||
|
|
||||||
namespace Bit.Core.NotificationHub;
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
@ -26,52 +22,32 @@ namespace Bit.Core.NotificationHub;
|
|||||||
/// Used by Cloud-Hosted environments.
|
/// Used by Cloud-Hosted environments.
|
||||||
/// Received by Firebase for Android or APNS for iOS.
|
/// Received by Firebase for Android or APNS for iOS.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer
|
||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly bool _enableTracing = false;
|
private readonly bool _enableTracing = false;
|
||||||
private readonly INotificationHubPool _notificationHubPool;
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
|
||||||
private readonly TimeProvider _timeProvider;
|
|
||||||
|
|
||||||
public NotificationHubPushNotificationService(
|
public NotificationHubPushNotificationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
INotificationHubPool notificationHubPool,
|
INotificationHubPool notificationHubPool,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<NotificationHubPushNotificationService> logger,
|
ILogger<NotificationHubPushNotificationService> logger,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings)
|
||||||
TimeProvider timeProvider)
|
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_notificationHubPool = notificationHubPool;
|
_notificationHubPool = notificationHubPool;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_timeProvider = timeProvider;
|
|
||||||
if (globalSettings.Installation.Id == Guid.Empty)
|
if (globalSettings.Installation.Id == Guid.Empty)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
|
||||||
{
|
{
|
||||||
if (cipher.OrganizationId.HasValue)
|
if (cipher.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
@ -93,311 +69,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
CollectionIds = collectionIds,
|
CollectionIds = collectionIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
|
await PushAsync(new PushNotification<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = type,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = cipher.UserId.Value,
|
||||||
|
Payload = message,
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
|
||||||
{
|
|
||||||
var message = new SyncFolderPushNotification
|
|
||||||
{
|
|
||||||
Id = folder.Id,
|
|
||||||
UserId = folder.UserId,
|
|
||||||
RevisionDate = folder.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(folder.UserId, type, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncVault);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendCreateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendDeleteAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushSendAsync(Send send, PushType type)
|
|
||||||
{
|
|
||||||
if (send.UserId.HasValue)
|
|
||||||
{
|
|
||||||
var message = new SyncSendPushNotification
|
|
||||||
{
|
|
||||||
Id = send.Id,
|
|
||||||
UserId = send.UserId.Value,
|
|
||||||
RevisionDate = send.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(message.UserId, type, message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushAuthRequestAsync(AuthRequest authRequest)
|
|
||||||
{
|
|
||||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
|
||||||
{
|
|
||||||
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationAsync(Notification notification)
|
|
||||||
{
|
|
||||||
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
|
|
||||||
? _globalSettings.Installation.Id
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = installationId,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.Global)
|
|
||||||
{
|
|
||||||
if (installationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Invalid global notification id {NotificationId} push notification. No installation id provided.",
|
|
||||||
notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (notification.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.OrganizationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
|
|
||||||
true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
|
||||||
{
|
|
||||||
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
|
|
||||||
? _globalSettings.Installation.Id
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = installationId,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate,
|
|
||||||
ReadDate = notificationStatus.ReadDate,
|
|
||||||
DeletedDate = notificationStatus.DeletedDate
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.Global)
|
|
||||||
{
|
|
||||||
if (installationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Invalid global notification status id {NotificationId} push notification. No installation id provided.",
|
|
||||||
notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (notification.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.OrganizationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus,
|
|
||||||
message, true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload,
|
|
||||||
bool excludeCurrentContext, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
await SendPayloadToInstallationAsync(installationId.ToString(), type, payload,
|
|
||||||
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
|
|
||||||
ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
|
|
||||||
clientType: clientType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
|
|
||||||
bool excludeCurrentContext, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
|
|
||||||
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload,
|
|
||||||
string? identifier, string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType);
|
|
||||||
await SendPayloadAsync(tag, type, payload);
|
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
|
||||||
{
|
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType);
|
|
||||||
await SendPayloadAsync(tag, type, payload);
|
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
|
||||||
{
|
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType);
|
|
||||||
await SendPayloadAsync(tag, type, payload);
|
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
|
||||||
{
|
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
var message = new OrganizationStatusPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Enabled = organization.Enabled
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
|
||||||
await SendPayloadToOrganizationAsync(
|
|
||||||
organization.Id,
|
|
||||||
PushType.SyncOrganizationCollectionSettingChanged,
|
|
||||||
new OrganizationCollectionManagementPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
|
||||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
|
||||||
LimitItemDeletion = organization.LimitItemDeletion
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
private string? GetContextIdentifier(bool excludeCurrentContext)
|
private string? GetContextIdentifier(bool excludeCurrentContext)
|
||||||
{
|
{
|
||||||
if (!excludeCurrentContext)
|
if (!excludeCurrentContext)
|
||||||
@ -425,13 +107,73 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
return $"({tag})";
|
return $"({tag})";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||||
|
where T : class
|
||||||
{
|
{
|
||||||
|
var initialTag = pushNotification.Target switch
|
||||||
|
{
|
||||||
|
NotificationTarget.User => $"template:payload_userId:{pushNotification.TargetId}",
|
||||||
|
NotificationTarget.Organization => $"template:payload && organizationId:{pushNotification.TargetId}",
|
||||||
|
NotificationTarget.Installation => $"template:payload && installationId:{pushNotification.TargetId}",
|
||||||
|
_ => throw new InvalidOperationException($"Push notification target '{pushNotification.Target}' is not valid."),
|
||||||
|
};
|
||||||
|
|
||||||
|
await PushCoreAsync(
|
||||||
|
initialTag,
|
||||||
|
GetContextIdentifier(pushNotification.ExcludeCurrentContext),
|
||||||
|
pushNotification.Type,
|
||||||
|
pushNotification.ClientType,
|
||||||
|
pushNotification.Payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification)
|
||||||
|
{
|
||||||
|
// Relayed notifications need identifiers prefixed with the installation they are from and a underscore
|
||||||
|
var initialTag = relayedNotification.Target switch
|
||||||
|
{
|
||||||
|
NotificationTarget.User => $"template:payload_userId:{fromInstallation}_{relayedNotification.TargetId}",
|
||||||
|
NotificationTarget.Organization => $"template:payload && organizationId:{fromInstallation}_{relayedNotification.TargetId}",
|
||||||
|
NotificationTarget.Installation => $"template:payload && installationId:{fromInstallation}",
|
||||||
|
_ => throw new InvalidOperationException($"Invalid Notification target {relayedNotification.Target}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
await PushCoreAsync(
|
||||||
|
initialTag,
|
||||||
|
relayedNotification.Identifier,
|
||||||
|
relayedNotification.Type,
|
||||||
|
relayedNotification.ClientType,
|
||||||
|
relayedNotification.Payload
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relayedNotification.DeviceId.HasValue)
|
||||||
|
{
|
||||||
|
await _installationDeviceRepository.UpsertAsync(
|
||||||
|
new InstallationDeviceEntity(fromInstallation, relayedNotification.DeviceId.Value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"A related notification of type '{Type}' came through without a device id from installation {Installation}",
|
||||||
|
relayedNotification.Type,
|
||||||
|
fromInstallation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PushCoreAsync<T>(string initialTag, string? contextId, PushType pushType, ClientType? clientType, T payload)
|
||||||
|
{
|
||||||
|
var finalTag = BuildTag(initialTag, contextId, clientType);
|
||||||
|
|
||||||
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
||||||
new Dictionary<string, string>
|
new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
|
{ "type", ((byte)pushType).ToString() },
|
||||||
}, tag);
|
{ "payload", JsonSerializer.Serialize(payload) },
|
||||||
|
},
|
||||||
|
finalTag
|
||||||
|
);
|
||||||
|
|
||||||
if (_enableTracing)
|
if (_enableTracing)
|
||||||
{
|
{
|
||||||
@ -444,7 +186,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
|
"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
|
||||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
outcome.TrackingId, pushType, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Azure.Storage.Queues;
|
using Azure.Storage.Queues;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -17,12 +13,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace Bit.Core.Platform.Push.Internal;
|
namespace Bit.Core.Platform.Push.Internal;
|
||||||
|
|
||||||
public class AzureQueuePushNotificationService : IPushNotificationService
|
public class AzureQueuePushNotificationService : IPushEngine
|
||||||
{
|
{
|
||||||
private readonly QueueClient _queueClient;
|
private readonly QueueClient _queueClient;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
|
||||||
private readonly TimeProvider _timeProvider;
|
|
||||||
|
|
||||||
public AzureQueuePushNotificationService(
|
public AzureQueuePushNotificationService(
|
||||||
[FromKeyedServices("notifications")] QueueClient queueClient,
|
[FromKeyedServices("notifications")] QueueClient queueClient,
|
||||||
@ -33,30 +27,13 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
|||||||
{
|
{
|
||||||
_queueClient = queueClient;
|
_queueClient = queueClient;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_timeProvider = timeProvider;
|
|
||||||
if (globalSettings.Installation.Id == Guid.Empty)
|
if (globalSettings.Installation.Id == Guid.Empty)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
|
||||||
{
|
{
|
||||||
if (cipher.OrganizationId.HasValue)
|
if (cipher.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
@ -83,166 +60,6 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
|
||||||
{
|
|
||||||
var message = new SyncFolderPushNotification
|
|
||||||
{
|
|
||||||
Id = folder.Id,
|
|
||||||
UserId = folder.UserId,
|
|
||||||
RevisionDate = folder.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncVault);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendDeleteAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationAsync(Notification notification)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(PushType.Notification, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate,
|
|
||||||
ReadDate = notificationStatus.ReadDate,
|
|
||||||
DeletedDate = notificationStatus.DeletedDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(PushType.NotificationStatus, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushSendAsync(Send send, PushType type)
|
|
||||||
{
|
|
||||||
if (send.UserId.HasValue)
|
|
||||||
{
|
|
||||||
var message = new SyncSendPushNotification
|
|
||||||
{
|
|
||||||
Id = send.Id,
|
|
||||||
UserId = send.UserId.Value,
|
|
||||||
RevisionDate = send.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
||||||
{
|
{
|
||||||
var contextId = GetContextIdentifier(excludeCurrentContext);
|
var contextId = GetContextIdentifier(excludeCurrentContext);
|
||||||
@ -263,42 +80,9 @@ public class AzureQueuePushNotificationService : IPushNotificationService
|
|||||||
return currentContext?.DeviceIdentifier;
|
return currentContext?.DeviceIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||||
string? deviceId = null, ClientType? clientType = null) =>
|
where T : class
|
||||||
// Noop
|
|
||||||
Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
{
|
||||||
// Noop
|
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
// Noop
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
var message = new OrganizationStatusPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Enabled = organization.Enabled
|
|
||||||
};
|
|
||||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
|
||||||
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
|
|
||||||
new OrganizationCollectionManagementPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
|
||||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
|
||||||
LimitItemDeletion = organization.LimitItemDeletion
|
|
||||||
}, false);
|
|
||||||
}
|
}
|
||||||
|
13
src/Core/Platform/Push/Services/IPushEngine.cs
Normal file
13
src/Core/Platform/Push/Services/IPushEngine.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Platform.Push;
|
||||||
|
|
||||||
|
public interface IPushEngine
|
||||||
|
{
|
||||||
|
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
|
||||||
|
|
||||||
|
Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||||
|
where T : class;
|
||||||
|
}
|
@ -2,41 +2,410 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Platform.Push;
|
namespace Bit.Core.Platform.Push;
|
||||||
|
|
||||||
public interface IPushNotificationService
|
public interface IPushNotificationService
|
||||||
{
|
{
|
||||||
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
|
Guid InstallationId { get; }
|
||||||
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
|
TimeProvider TimeProvider { get; }
|
||||||
Task PushSyncCipherDeleteAsync(Cipher cipher);
|
ILogger Logger { get; }
|
||||||
Task PushSyncFolderCreateAsync(Folder folder);
|
|
||||||
Task PushSyncFolderUpdateAsync(Folder folder);
|
|
||||||
Task PushSyncFolderDeleteAsync(Folder folder);
|
|
||||||
Task PushSyncCiphersAsync(Guid userId);
|
|
||||||
Task PushSyncVaultAsync(Guid userId);
|
|
||||||
Task PushSyncOrganizationsAsync(Guid userId);
|
|
||||||
Task PushSyncOrgKeysAsync(Guid userId);
|
|
||||||
Task PushSyncSettingsAsync(Guid userId);
|
|
||||||
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false);
|
|
||||||
Task PushSyncSendCreateAsync(Send send);
|
|
||||||
Task PushSyncSendUpdateAsync(Send send);
|
|
||||||
Task PushSyncSendDeleteAsync(Send send);
|
|
||||||
Task PushNotificationAsync(Notification notification);
|
|
||||||
Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus);
|
|
||||||
Task PushAuthRequestAsync(AuthRequest authRequest);
|
|
||||||
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
|
|
||||||
Task PushSyncOrganizationStatusAsync(Organization organization);
|
|
||||||
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization);
|
|
||||||
|
|
||||||
Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
#region Legacy method, to be removed soon.
|
||||||
string? deviceId = null, ClientType? clientType = null);
|
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||||
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
=> PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
||||||
string? deviceId = null, ClientType? clientType = null);
|
|
||||||
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||||
string? deviceId = null, ClientType? clientType = null);
|
=> PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
||||||
Task PushPendingSecurityTasksAsync(Guid userId);
|
|
||||||
|
Task PushSyncCipherDeleteAsync(Cipher cipher)
|
||||||
|
=> PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
||||||
|
|
||||||
|
Task PushSyncFolderCreateAsync(Folder folder)
|
||||||
|
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderCreate,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = folder.UserId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = folder.Id,
|
||||||
|
UserId = folder.UserId,
|
||||||
|
RevisionDate = folder.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncFolderUpdateAsync(Folder folder)
|
||||||
|
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderUpdate,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = folder.UserId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = folder.Id,
|
||||||
|
UserId = folder.UserId,
|
||||||
|
RevisionDate = folder.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncFolderDeleteAsync(Folder folder)
|
||||||
|
=> PushAsync(new PushNotification<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderDelete,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = folder.UserId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = folder.Id,
|
||||||
|
UserId = folder.UserId,
|
||||||
|
RevisionDate = folder.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncCiphersAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCiphers,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncVaultAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncVault,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncOrganizationsAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncOrganizations,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncOrgKeysAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncOrgKeys,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncSettingsAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncSettings,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.LogOut,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = excludeCurrentContextFromPush,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncSendCreateAsync(Send send)
|
||||||
|
{
|
||||||
|
if (send.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncSendCreate,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = send.UserId.Value,
|
||||||
|
Payload = new SyncSendPushNotification
|
||||||
|
{
|
||||||
|
Id = send.Id,
|
||||||
|
UserId = send.UserId.Value,
|
||||||
|
RevisionDate = send.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task PushSyncSendUpdateAsync(Send send)
|
||||||
|
{
|
||||||
|
if (send.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncSendUpdate,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = send.UserId.Value,
|
||||||
|
Payload = new SyncSendPushNotification
|
||||||
|
{
|
||||||
|
Id = send.Id,
|
||||||
|
UserId = send.UserId.Value,
|
||||||
|
RevisionDate = send.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task PushSyncSendDeleteAsync(Send send)
|
||||||
|
{
|
||||||
|
if (send.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return PushAsync(new PushNotification<SyncSendPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncSendDelete,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = send.UserId.Value,
|
||||||
|
Payload = new SyncSendPushNotification
|
||||||
|
{
|
||||||
|
Id = send.Id,
|
||||||
|
UserId = send.UserId.Value,
|
||||||
|
RevisionDate = send.RevisionDate,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task PushNotificationAsync(Notification notification)
|
||||||
|
{
|
||||||
|
var message = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = notification.Id,
|
||||||
|
Priority = notification.Priority,
|
||||||
|
Global = notification.Global,
|
||||||
|
ClientType = notification.ClientType,
|
||||||
|
UserId = notification.UserId,
|
||||||
|
OrganizationId = notification.OrganizationId,
|
||||||
|
InstallationId = notification.Global ? InstallationId : null,
|
||||||
|
TaskId = notification.TaskId,
|
||||||
|
Title = notification.Title,
|
||||||
|
Body = notification.Body,
|
||||||
|
CreationDate = notification.CreationDate,
|
||||||
|
RevisionDate = notification.RevisionDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationTarget target;
|
||||||
|
Guid targetId;
|
||||||
|
|
||||||
|
if (notification.Global)
|
||||||
|
{
|
||||||
|
// TODO: Think about this a bit more
|
||||||
|
target = NotificationTarget.Installation;
|
||||||
|
targetId = InstallationId;
|
||||||
|
}
|
||||||
|
else if (notification.UserId.HasValue)
|
||||||
|
{
|
||||||
|
target = NotificationTarget.User;
|
||||||
|
targetId = notification.UserId.Value;
|
||||||
|
}
|
||||||
|
else if (notification.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
target = NotificationTarget.Organization;
|
||||||
|
targetId = notification.OrganizationId.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PushAsync(new PushNotification<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.Notification,
|
||||||
|
Target = target,
|
||||||
|
TargetId = targetId,
|
||||||
|
Payload = message,
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
ClientType = notification.ClientType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
||||||
|
{
|
||||||
|
var message = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = notification.Id,
|
||||||
|
Priority = notification.Priority,
|
||||||
|
Global = notification.Global,
|
||||||
|
ClientType = notification.ClientType,
|
||||||
|
UserId = notification.UserId,
|
||||||
|
OrganizationId = notification.OrganizationId,
|
||||||
|
InstallationId = notification.Global ? InstallationId : null,
|
||||||
|
TaskId = notification.TaskId,
|
||||||
|
Title = notification.Title,
|
||||||
|
Body = notification.Body,
|
||||||
|
CreationDate = notification.CreationDate,
|
||||||
|
RevisionDate = notification.RevisionDate,
|
||||||
|
ReadDate = notificationStatus.ReadDate,
|
||||||
|
DeletedDate = notificationStatus.DeletedDate,
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationTarget target;
|
||||||
|
Guid targetId;
|
||||||
|
|
||||||
|
if (notification.Global)
|
||||||
|
{
|
||||||
|
// TODO: Think about this a bit more
|
||||||
|
target = NotificationTarget.Installation;
|
||||||
|
targetId = InstallationId;
|
||||||
|
}
|
||||||
|
else if (notification.UserId.HasValue)
|
||||||
|
{
|
||||||
|
target = NotificationTarget.User;
|
||||||
|
targetId = notification.UserId.Value;
|
||||||
|
}
|
||||||
|
else if (notification.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
target = NotificationTarget.Organization;
|
||||||
|
targetId = notification.OrganizationId.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PushAsync(new PushNotification<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.NotificationStatus,
|
||||||
|
Target = target,
|
||||||
|
TargetId = targetId,
|
||||||
|
Payload = message,
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
ClientType = notification.ClientType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Task PushAuthRequestAsync(AuthRequest authRequest)
|
||||||
|
=> PushAsync(new PushNotification<AuthRequestPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.AuthRequest,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = authRequest.UserId,
|
||||||
|
Payload = new AuthRequestPushNotification
|
||||||
|
{
|
||||||
|
Id = authRequest.Id,
|
||||||
|
UserId = authRequest.UserId,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
||||||
|
=> PushAsync(new PushNotification<AuthRequestPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.AuthRequestResponse,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = authRequest.UserId,
|
||||||
|
Payload = new AuthRequestPushNotification
|
||||||
|
{
|
||||||
|
Id = authRequest.Id,
|
||||||
|
UserId = authRequest.UserId,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncOrganizationStatusAsync(Organization organization)
|
||||||
|
=> PushAsync(new PushNotification<OrganizationStatusPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncOrganizationStatusChanged,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organization.Id,
|
||||||
|
Payload = new OrganizationStatusPushNotification
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Enabled = organization.Enabled,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
||||||
|
=> PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncOrganizationCollectionSettingChanged,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organization.Id,
|
||||||
|
Payload = new OrganizationCollectionManagementPushNotification
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
LimitCollectionCreation = organization.LimitCollectionCreation,
|
||||||
|
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
||||||
|
LimitItemDeletion = organization.LimitItemDeletion,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
Task PushPendingSecurityTasksAsync(Guid userId)
|
||||||
|
=> PushAsync(new PushNotification<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.PendingSecurityTasks,
|
||||||
|
Target = NotificationTarget.User,
|
||||||
|
TargetId = userId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
Date = TimeProvider.GetUtcNow().UtcDateTime,
|
||||||
|
},
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
});
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
|
||||||
|
|
||||||
|
Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||||
|
where T : class;
|
||||||
}
|
}
|
||||||
|
44
src/Core/Platform/Push/Services/IPushRelayer.cs
Normal file
44
src/Core/Platform/Push/Services/IPushRelayer.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Platform.Push.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An object encapsulating the information that is available in a notification
|
||||||
|
/// given to us from a self-hosted installation.
|
||||||
|
/// </summary>
|
||||||
|
public class RelayedNotification
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="PushNotification{T}.Type"/>
|
||||||
|
public required PushType Type { get; init; }
|
||||||
|
/// <inheritdoc cref="PushNotification{T}.Target"/>
|
||||||
|
public required NotificationTarget Target { get; init; }
|
||||||
|
/// <inheritdoc cref="PushNotification{T}.TargetId"/>
|
||||||
|
public required Guid TargetId { get; init; }
|
||||||
|
/// <inheritdoc cref="PushNotification{T}.Payload"/>
|
||||||
|
public required JsonElement Payload { get; init; }
|
||||||
|
/// <inheritdoc cref="PushNotification{T}.ClientType"/>
|
||||||
|
public required ClientType? ClientType { get; init; }
|
||||||
|
public required Guid? DeviceId { get; init; }
|
||||||
|
public required string? Identifier { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A service for taking a notification that was relayed to us from a self-hosted installation and
|
||||||
|
/// will be injested into our infrastructure so that we can get the notification to devices that require
|
||||||
|
/// cloud interaction.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This interface should be treated as internal and not consumed by other teams.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IPushRelayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Relays a notification that was received from an authenticated installation into our cloud push notification infrastructure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fromInstallation">The authenticated installation this notification came from.</param>
|
||||||
|
/// <param name="relayedNotification">The information received from the self-hosted installation.</param>
|
||||||
|
Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification);
|
||||||
|
}
|
@ -1,202 +1,77 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Platform.Push.Internal;
|
namespace Bit.Core.Platform.Push.Internal;
|
||||||
|
|
||||||
public class MultiServicePushNotificationService : IPushNotificationService
|
public class MultiServicePushNotificationService : IPushNotificationService
|
||||||
{
|
{
|
||||||
private readonly IEnumerable<IPushNotificationService> _services;
|
private readonly IEnumerable<IPushEngine> _services;
|
||||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
|
||||||
|
public Guid InstallationId { get; }
|
||||||
|
|
||||||
|
public TimeProvider TimeProvider { get; }
|
||||||
|
|
||||||
|
public ILogger Logger { get; }
|
||||||
|
|
||||||
public MultiServicePushNotificationService(
|
public MultiServicePushNotificationService(
|
||||||
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
IEnumerable<IPushEngine> services,
|
||||||
ILogger<MultiServicePushNotificationService> logger,
|
ILogger<MultiServicePushNotificationService> logger,
|
||||||
GlobalSettings globalSettings)
|
GlobalSettings globalSettings,
|
||||||
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
_services = services;
|
_services = services;
|
||||||
|
|
||||||
_logger = logger;
|
Logger = logger;
|
||||||
_logger.LogInformation("Hub services: {Services}", _services.Count());
|
Logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||||
globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
||||||
{
|
{
|
||||||
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
Logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||||
});
|
});
|
||||||
|
InstallationId = globalSettings.Installation.Id;
|
||||||
|
TimeProvider = timeProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
private Task PushToServices(Func<IPushEngine, Task> pushFunc)
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncCipherCreateAsync(cipher, collectionIds));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncCipherUpdateAsync(cipher, collectionIds));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCipherDeleteAsync(Cipher cipher)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncCipherDeleteAsync(cipher));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncFolderCreateAsync(folder));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncFolderUpdateAsync(folder));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncFolderDeleteAsync(folder));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncCiphersAsync(userId));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncVaultAsync(userId));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncOrganizationsAsync(userId));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncOrgKeysAsync(userId));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncSettingsAsync(userId));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushLogOutAsync(userId, excludeCurrentContext));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSendCreateAsync(Send send)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncSendCreateAsync(send));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncSendUpdateAsync(send));
|
|
||||||
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));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
|
|
||||||
{
|
|
||||||
PushToServices(s => s.PushSyncOrganizationCollectionManagementSettingsAsync(organization));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushNotificationAsync(Notification notification)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushNotificationAsync(notification));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
PushToServices((s) =>
|
|
||||||
s.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType));
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
PushToServices((s) => s.PushPendingSecurityTasksAsync(userId));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PushToServices(Func<IPushNotificationService, Task> pushFunc)
|
|
||||||
{
|
{
|
||||||
if (!_services.Any())
|
if (!_services.Any())
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No services found to push notification");
|
Logger.LogWarning("No services found to push notification");
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
#endif
|
||||||
|
|
||||||
foreach (var service in _services)
|
foreach (var service in _services)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
|
Logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
|
||||||
|
#if DEBUG
|
||||||
|
var task =
|
||||||
|
#endif
|
||||||
pushFunc(service);
|
pushFunc(service);
|
||||||
|
#if DEBUG
|
||||||
|
tasks.Add(task);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
return Task.WhenAll(tasks);
|
||||||
|
#else
|
||||||
|
return Task.CompletedTask;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)
|
||||||
|
{
|
||||||
|
return PushToServices((s) => s.PushCipherAsync(cipher, pushType, collectionIds));
|
||||||
|
}
|
||||||
|
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
|
||||||
|
{
|
||||||
|
return PushToServices((s) => s.PushAsync(pushNotification));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,129 +1,12 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
|
|
||||||
namespace Bit.Core.Platform.Push.Internal;
|
namespace Bit.Core.Platform.Push.Internal;
|
||||||
|
|
||||||
public class NoopPushNotificationService : IPushNotificationService
|
internal class NoopPushNotificationService : IPushEngine
|
||||||
{
|
{
|
||||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds) => Task.CompletedTask;
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCipherDeleteAsync(Cipher cipher)
|
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class => Task.CompletedTask;
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSendCreateAsync(Send send)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSendDeleteAsync(Send send)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task PushAuthRequestAsync(AuthRequest authRequest)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushNotificationAsync(Notification notification) => Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) =>
|
|
||||||
Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null) => Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -20,18 +16,15 @@ namespace Bit.Core.Platform.Push;
|
|||||||
/// Used by Cloud-Hosted environments.
|
/// Used by Cloud-Hosted environments.
|
||||||
/// Received by AzureQueueHostedService message receiver in Notifications project.
|
/// Received by AzureQueueHostedService message receiver in Notifications project.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
|
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushEngine
|
||||||
{
|
{
|
||||||
private readonly IGlobalSettings _globalSettings;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly TimeProvider _timeProvider;
|
|
||||||
|
|
||||||
public NotificationsApiPushNotificationService(
|
public NotificationsApiPushNotificationService(
|
||||||
IHttpClientFactory httpFactory,
|
IHttpClientFactory httpFactory,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<NotificationsApiPushNotificationService> logger,
|
ILogger<NotificationsApiPushNotificationService> logger)
|
||||||
TimeProvider timeProvider)
|
|
||||||
: base(
|
: base(
|
||||||
httpFactory,
|
httpFactory,
|
||||||
globalSettings.BaseServiceUri.InternalNotifications,
|
globalSettings.BaseServiceUri.InternalNotifications,
|
||||||
@ -41,27 +34,10 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
|||||||
globalSettings.InternalIdentityKey,
|
globalSettings.InternalIdentityKey,
|
||||||
logger)
|
logger)
|
||||||
{
|
{
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_timeProvider = timeProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
|
||||||
{
|
{
|
||||||
if (cipher.OrganizationId.HasValue)
|
if (cipher.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
@ -89,174 +65,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
|
||||||
{
|
|
||||||
var message = new SyncFolderPushNotification
|
|
||||||
{
|
|
||||||
Id = folder.Id,
|
|
||||||
UserId = folder.UserId,
|
|
||||||
RevisionDate = folder.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncVault);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
var message = new UserPushNotification
|
|
||||||
{
|
|
||||||
UserId = userId,
|
|
||||||
Date = _timeProvider.GetUtcNow().UtcDateTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendDeleteAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationAsync(Notification notification)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(PushType.Notification, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate,
|
|
||||||
ReadDate = notificationStatus.ReadDate,
|
|
||||||
DeletedDate = notificationStatus.DeletedDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(PushType.NotificationStatus, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushSendAsync(Send send, PushType type)
|
|
||||||
{
|
|
||||||
if (send.UserId.HasValue)
|
|
||||||
{
|
|
||||||
var message = new SyncSendPushNotification
|
|
||||||
{
|
|
||||||
Id = send.Id,
|
|
||||||
UserId = send.UserId.Value,
|
|
||||||
RevisionDate = send.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(type, message, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
|
||||||
{
|
{
|
||||||
var contextId = GetContextIdentifier(excludeCurrentContext);
|
var contextId = GetContextIdentifier(excludeCurrentContext);
|
||||||
@ -276,43 +84,8 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
|
|||||||
return currentContext?.DeviceIdentifier;
|
return currentContext?.DeviceIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
public async Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
|
||||||
string? deviceId = null, ClientType? clientType = null) =>
|
|
||||||
// Noop
|
|
||||||
Task.CompletedTask;
|
|
||||||
|
|
||||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
{
|
||||||
// Noop
|
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
// Noop
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
var message = new OrganizationStatusPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Enabled = organization.Enabled
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
|
||||||
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
|
|
||||||
new OrganizationCollectionManagementPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
|
||||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
|
||||||
LimitItemDeletion = organization.LimitItemDeletion
|
|
||||||
}, false);
|
|
||||||
}
|
}
|
||||||
|
78
src/Core/Platform/Push/Services/PushNotification.cs
Normal file
78
src/Core/Platform/Push/Services/PushNotification.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Platform.Push;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains constants for all the available targets for a given notification.
|
||||||
|
/// </summary>
|
||||||
|
public enum NotificationTarget
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The target for the notification is a single user.
|
||||||
|
/// </summary>
|
||||||
|
User,
|
||||||
|
/// <summary>
|
||||||
|
/// The target for the notification are all the users in an organization.
|
||||||
|
/// </summary>
|
||||||
|
Organization,
|
||||||
|
/// <summary>
|
||||||
|
/// The target for the notification are all the organizations,
|
||||||
|
/// and all the users in that organization for a installation.
|
||||||
|
/// </summary>
|
||||||
|
Installation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An object containing all the information required for getting a notification
|
||||||
|
/// to an end users device and the information you want available to that device.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the payload. This type is expected to be able to be roundtripped as JSON.</typeparam>
|
||||||
|
public record PushNotification<T>
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="PushType"/> to be associated with the notification. This is used to route
|
||||||
|
/// the notification to the correct handler on the client side. Be sure to use the correct payload
|
||||||
|
/// type for the associated <see cref="PushType"/>.
|
||||||
|
/// </summary>
|
||||||
|
public required PushType Type { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target entity type for the notification.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// When the target type is <see cref="NotificationTarget.User"/> the <see cref="TargetId"/>
|
||||||
|
/// property is expected to be a users ID. When it is <see cref="NotificationTarget.Organization"/>
|
||||||
|
/// it should be an organizations id. When it is a <see cref="NotificationTarget.Installation"/>
|
||||||
|
/// it should be an installation id.
|
||||||
|
/// </remarks>
|
||||||
|
public required NotificationTarget Target { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The indentifier for the given <see cref="Target"/>.
|
||||||
|
/// </summary>
|
||||||
|
public required Guid TargetId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The payload to be sent with the notification. This object will be JSON serialized.
|
||||||
|
/// </summary>
|
||||||
|
public required T Payload { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <see langword="true"/> the notification will not include the current context identifier on it, this
|
||||||
|
/// means that the notification may get handled on the device that this notification could have originated from.
|
||||||
|
/// </summary>
|
||||||
|
public required bool ExcludeCurrentContext { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of clients the notification should be sent to, if <see langword="null"/> then
|
||||||
|
/// <see cref="ClientType.All"/> is inferred.
|
||||||
|
/// </summary>
|
||||||
|
public ClientType? ClientType { get; init; }
|
||||||
|
|
||||||
|
internal Guid? GetTargetWhen(NotificationTarget notificationTarget)
|
||||||
|
{
|
||||||
|
return Target == notificationTarget ? TargetId : null;
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,15 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Auth.Entities;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Platform.Push.Internal;
|
namespace Bit.Core.Platform.Push.Internal;
|
||||||
@ -22,20 +19,18 @@ namespace Bit.Core.Platform.Push.Internal;
|
|||||||
/// Used by Self-Hosted environments.
|
/// Used by Self-Hosted environments.
|
||||||
/// Received by PushController endpoint in Api project.
|
/// Received by PushController endpoint in Api project.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService
|
public class RelayPushNotificationService : BaseIdentityClientService, IPushEngine
|
||||||
{
|
{
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
private readonly IDeviceRepository _deviceRepository;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly TimeProvider _timeProvider;
|
|
||||||
|
|
||||||
public RelayPushNotificationService(
|
public RelayPushNotificationService(
|
||||||
IHttpClientFactory httpFactory,
|
IHttpClientFactory httpFactory,
|
||||||
IDeviceRepository deviceRepository,
|
IDeviceRepository deviceRepository,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<RelayPushNotificationService> logger,
|
ILogger<RelayPushNotificationService> logger)
|
||||||
TimeProvider timeProvider)
|
|
||||||
: base(
|
: base(
|
||||||
httpFactory,
|
httpFactory,
|
||||||
globalSettings.PushRelayBaseUri,
|
globalSettings.PushRelayBaseUri,
|
||||||
@ -46,27 +41,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
|||||||
logger)
|
logger)
|
||||||
{
|
{
|
||||||
_deviceRepository = deviceRepository;
|
_deviceRepository = deviceRepository;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_timeProvider = timeProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
|
|
||||||
{
|
|
||||||
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
|
|
||||||
{
|
{
|
||||||
if (cipher.OrganizationId.HasValue)
|
if (cipher.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
@ -87,306 +65,45 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
|
|||||||
RevisionDate = cipher.RevisionDate,
|
RevisionDate = cipher.RevisionDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
|
await PushAsync(new PushNotification<SyncCipherPushNotification>
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderCreateAsync(Folder folder)
|
|
||||||
{
|
{
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderUpdateAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncFolderDeleteAsync(Folder folder)
|
|
||||||
{
|
|
||||||
await PushFolderAsync(folder, PushType.SyncFolderDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushFolderAsync(Folder folder, PushType type)
|
|
||||||
{
|
|
||||||
var message = new SyncFolderPushNotification
|
|
||||||
{
|
|
||||||
Id = folder.Id,
|
|
||||||
UserId = folder.UserId,
|
|
||||||
RevisionDate = folder.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(folder.UserId, type, message, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncCiphersAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncCiphers);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncVaultAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncVault);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrganizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrgKeysAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncOrgKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSettingsAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.SyncSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
|
|
||||||
{
|
|
||||||
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendCreateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendCreate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendUpdateAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncSendDeleteAsync(Send send)
|
|
||||||
{
|
|
||||||
await PushSendAsync(send, PushType.SyncSendDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushSendAsync(Send send, PushType type)
|
|
||||||
{
|
|
||||||
if (send.UserId.HasValue)
|
|
||||||
{
|
|
||||||
var message = new SyncSendPushNotification
|
|
||||||
{
|
|
||||||
Id = send.Id,
|
|
||||||
UserId = send.UserId.Value,
|
|
||||||
RevisionDate = send.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToUserAsync(message.UserId, type, message, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationAsync(Notification notification)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.Global)
|
|
||||||
{
|
|
||||||
await SendPayloadToInstallationAsync(PushType.Notification, message, true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.OrganizationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
|
|
||||||
true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
|
|
||||||
{
|
|
||||||
var message = new NotificationPushNotification
|
|
||||||
{
|
|
||||||
Id = notification.Id,
|
|
||||||
Priority = notification.Priority,
|
|
||||||
Global = notification.Global,
|
|
||||||
ClientType = notification.ClientType,
|
|
||||||
UserId = notification.UserId,
|
|
||||||
OrganizationId = notification.OrganizationId,
|
|
||||||
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
|
|
||||||
TaskId = notification.TaskId,
|
|
||||||
Title = notification.Title,
|
|
||||||
Body = notification.Body,
|
|
||||||
CreationDate = notification.CreationDate,
|
|
||||||
RevisionDate = notification.RevisionDate,
|
|
||||||
ReadDate = notificationStatus.ReadDate,
|
|
||||||
DeletedDate = notificationStatus.DeletedDate
|
|
||||||
};
|
|
||||||
|
|
||||||
if (notification.Global)
|
|
||||||
{
|
|
||||||
await SendPayloadToInstallationAsync(PushType.NotificationStatus, message, true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
|
|
||||||
notification.ClientType);
|
|
||||||
}
|
|
||||||
else if (notification.OrganizationId.HasValue)
|
|
||||||
{
|
|
||||||
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, message,
|
|
||||||
true, notification.ClientType);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationStatusAsync(Organization organization)
|
|
||||||
{
|
|
||||||
var message = new OrganizationStatusPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
Enabled = organization.Enabled
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
|
|
||||||
await SendPayloadToOrganizationAsync(
|
|
||||||
organization.Id,
|
|
||||||
PushType.SyncOrganizationCollectionSettingChanged,
|
|
||||||
new OrganizationCollectionManagementPushNotification
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
LimitCollectionCreation = organization.LimitCollectionCreation,
|
|
||||||
LimitCollectionDeletion = organization.LimitCollectionDeletion,
|
|
||||||
LimitItemDeletion = organization.LimitItemDeletion
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
public async Task PushPendingSecurityTasksAsync(Guid userId)
|
|
||||||
{
|
|
||||||
await PushUserAsync(userId, PushType.PendingSecurityTasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext,
|
|
||||||
ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var request = new PushSendRequestModel
|
|
||||||
{
|
|
||||||
InstallationId = _globalSettings.Installation.Id.ToString(),
|
|
||||||
Type = type,
|
Type = type,
|
||||||
Payload = payload,
|
Target = NotificationTarget.User,
|
||||||
ClientType = clientType
|
TargetId = cipher.UserId.Value,
|
||||||
|
Payload = message,
|
||||||
|
ExcludeCurrentContext = true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PushAsync<T>(PushNotification<T> pushNotification)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
var deviceIdentifier = _httpContextAccessor.HttpContext
|
||||||
|
?.RequestServices.GetService<ICurrentContext>()
|
||||||
|
?.DeviceIdentifier;
|
||||||
|
|
||||||
|
Guid? deviceId = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(deviceIdentifier))
|
||||||
|
{
|
||||||
|
var device = await _deviceRepository.GetByIdentifierAsync(deviceIdentifier);
|
||||||
|
deviceId = device?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new PushSendRequestModel<T>
|
||||||
|
{
|
||||||
|
Type = pushNotification.Type,
|
||||||
|
UserId = pushNotification.GetTargetWhen(NotificationTarget.User),
|
||||||
|
OrganizationId = pushNotification.GetTargetWhen(NotificationTarget.Organization),
|
||||||
|
InstallationId = pushNotification.GetTargetWhen(NotificationTarget.Installation),
|
||||||
|
Payload = pushNotification.Payload,
|
||||||
|
Identifier = pushNotification.ExcludeCurrentContext ? deviceIdentifier : null,
|
||||||
|
// We set the device id regardless of if they want to exclude the current context or not
|
||||||
|
DeviceId = deviceId,
|
||||||
|
ClientType = pushNotification.ClientType,
|
||||||
};
|
};
|
||||||
|
|
||||||
await AddCurrentContextAsync(request, excludeCurrentContext);
|
await SendAsync(HttpMethod.Post, "push/send", payload);
|
||||||
await SendAsync(HttpMethod.Post, "push/send", request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext,
|
|
||||||
ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var request = new PushSendRequestModel
|
|
||||||
{
|
|
||||||
UserId = userId.ToString(),
|
|
||||||
Type = type,
|
|
||||||
Payload = payload,
|
|
||||||
ClientType = clientType
|
|
||||||
};
|
|
||||||
|
|
||||||
await AddCurrentContextAsync(request, excludeCurrentContext);
|
|
||||||
await SendAsync(HttpMethod.Post, "push/send", request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
|
|
||||||
bool excludeCurrentContext, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
var request = new PushSendRequestModel
|
|
||||||
{
|
|
||||||
OrganizationId = orgId.ToString(),
|
|
||||||
Type = type,
|
|
||||||
Payload = payload,
|
|
||||||
ClientType = clientType
|
|
||||||
};
|
|
||||||
|
|
||||||
await AddCurrentContextAsync(request, excludeCurrentContext);
|
|
||||||
await SendAsync(HttpMethod.Post, "push/send", request);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
|
|
||||||
{
|
|
||||||
var currentContext =
|
|
||||||
_httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
|
|
||||||
if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
|
|
||||||
{
|
|
||||||
var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
|
|
||||||
if (device != null)
|
|
||||||
{
|
|
||||||
request.DeviceId = device.Id.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addIdentifier)
|
|
||||||
{
|
|
||||||
request.Identifier = currentContext.DeviceIdentifier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null) =>
|
|
||||||
throw new NotImplementedException();
|
|
||||||
|
|
||||||
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
|
|
||||||
string? deviceId = null, ClientType? clientType = null)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ public class SendGridMailDeliveryService : IMailDeliveryService, IDisposable
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IWebHostEnvironment hostingEnvironment,
|
IWebHostEnvironment hostingEnvironment,
|
||||||
ILogger<SendGridMailDeliveryService> logger)
|
ILogger<SendGridMailDeliveryService> logger)
|
||||||
: this(new SendGridClient(globalSettings.Mail.SendGridApiKey),
|
: this(new SendGridClient(globalSettings.Mail.SendGridApiKey, globalSettings.Mail.SendGridApiHost),
|
||||||
globalSettings, hostingEnvironment, logger)
|
globalSettings, hostingEnvironment, logger)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -431,6 +431,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public SmtpSettings Smtp { get; set; } = new SmtpSettings();
|
public SmtpSettings Smtp { get; set; } = new SmtpSettings();
|
||||||
public string SendGridApiKey { get; set; }
|
public string SendGridApiKey { get; set; }
|
||||||
public int? SendGridPercentage { get; set; }
|
public int? SendGridPercentage { get; set; }
|
||||||
|
public string SendGridApiHost { get; set; } = "https://api.sendgrid.com";
|
||||||
|
|
||||||
public class SmtpSettings
|
public class SmtpSettings
|
||||||
{
|
{
|
||||||
|
@ -56,7 +56,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
{
|
{
|
||||||
// Make sure the user can save new ciphers to their personal vault
|
// Make sure the user can save new ciphers to their personal vault
|
||||||
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)).DisablePersonalOwnership
|
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)).State == PersonalOwnershipState.Restricted
|
||||||
: await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
: await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
||||||
|
|
||||||
if (isPersonalVaultRestricted)
|
if (isPersonalVaultRestricted)
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Tools.Repositories;
|
|||||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Tools.SendFeatures.Commands;
|
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||||
|
|
||||||
@ -18,19 +19,22 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
|||||||
private readonly IPushNotificationService _pushNotificationService;
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private readonly ISendValidationService _sendValidationService;
|
private readonly ISendValidationService _sendValidationService;
|
||||||
private readonly ISendCoreHelperService _sendCoreHelperService;
|
private readonly ISendCoreHelperService _sendCoreHelperService;
|
||||||
|
private readonly ILogger<NonAnonymousSendCommand> _logger;
|
||||||
|
|
||||||
public NonAnonymousSendCommand(ISendRepository sendRepository,
|
public NonAnonymousSendCommand(ISendRepository sendRepository,
|
||||||
ISendFileStorageService sendFileStorageService,
|
ISendFileStorageService sendFileStorageService,
|
||||||
IPushNotificationService pushNotificationService,
|
IPushNotificationService pushNotificationService,
|
||||||
ISendAuthorizationService sendAuthorizationService,
|
ISendAuthorizationService sendAuthorizationService,
|
||||||
ISendValidationService sendValidationService,
|
ISendValidationService sendValidationService,
|
||||||
ISendCoreHelperService sendCoreHelperService)
|
ISendCoreHelperService sendCoreHelperService,
|
||||||
|
ILogger<NonAnonymousSendCommand> logger)
|
||||||
{
|
{
|
||||||
_sendRepository = sendRepository;
|
_sendRepository = sendRepository;
|
||||||
_sendFileStorageService = sendFileStorageService;
|
_sendFileStorageService = sendFileStorageService;
|
||||||
_pushNotificationService = pushNotificationService;
|
_pushNotificationService = pushNotificationService;
|
||||||
_sendValidationService = sendValidationService;
|
_sendValidationService = sendValidationService;
|
||||||
_sendCoreHelperService = sendCoreHelperService;
|
_sendCoreHelperService = sendCoreHelperService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveSendAsync(Send send)
|
public async Task SaveSendAsync(Send send)
|
||||||
@ -63,6 +67,11 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
|||||||
throw new BadRequestException("No file data.");
|
throw new BadRequestException("No file data.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileLength > SendFileSettingHelper.MAX_FILE_SIZE)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
|
||||||
|
}
|
||||||
|
|
||||||
var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);
|
var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
if (storageBytesRemaining < fileLength)
|
if (storageBytesRemaining < fileLength)
|
||||||
@ -77,13 +86,17 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
|||||||
data.Id = fileId;
|
data.Id = fileId;
|
||||||
data.Size = fileLength;
|
data.Size = fileLength;
|
||||||
data.Validated = false;
|
data.Validated = false;
|
||||||
send.Data = JsonSerializer.Serialize(data,
|
send.Data = JsonSerializer.Serialize(data, JsonHelpers.IgnoreWritingNull);
|
||||||
JsonHelpers.IgnoreWritingNull);
|
|
||||||
await SaveSendAsync(send);
|
await SaveSendAsync(send);
|
||||||
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Deleted file from {SendId} because an error occurred when creating the upload URL.",
|
||||||
|
send.Id
|
||||||
|
);
|
||||||
|
|
||||||
// Clean up since this is not transactional
|
// Clean up since this is not transactional
|
||||||
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
||||||
throw;
|
throw;
|
||||||
@ -135,23 +148,31 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
|||||||
{
|
{
|
||||||
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||||
|
|
||||||
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY);
|
var minimum = fileData.Size - SendFileSettingHelper.FILE_SIZE_LEEWAY;
|
||||||
|
var maximum = Math.Min(
|
||||||
|
fileData.Size + SendFileSettingHelper.FILE_SIZE_LEEWAY,
|
||||||
|
SendFileSettingHelper.MAX_FILE_SIZE
|
||||||
|
);
|
||||||
|
var (valid, size) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, minimum, maximum);
|
||||||
|
|
||||||
if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY)
|
// protect file service from upload hijacking by deleting invalid sends
|
||||||
|
if (!valid)
|
||||||
{
|
{
|
||||||
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
_logger.LogWarning(
|
||||||
|
"Deleted {SendId} because its reported size {Size} was outside the expected range ({Minimum} - {Maximum}).",
|
||||||
|
send.Id,
|
||||||
|
size,
|
||||||
|
minimum,
|
||||||
|
maximum
|
||||||
|
);
|
||||||
await DeleteSendAsync(send);
|
await DeleteSendAsync(send);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Send data if necessary
|
// replace expected size with validated size
|
||||||
if (realSize != fileData.Size)
|
fileData.Size = size;
|
||||||
{
|
|
||||||
fileData.Size = realSize.Value;
|
|
||||||
}
|
|
||||||
fileData.Validated = true;
|
fileData.Validated = true;
|
||||||
send.Data = JsonSerializer.Serialize(fileData,
|
send.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);
|
||||||
JsonHelpers.IgnoreWritingNull);
|
|
||||||
await SaveSendAsync(send);
|
await SaveSendAsync(send);
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
|
@ -88,7 +88,7 @@ public class AzureSendFileStorageService : ISendFileStorageService
|
|||||||
return sasUri.ToString();
|
return sasUri.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
public async Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||||
{
|
{
|
||||||
await InitAsync();
|
await InitAsync();
|
||||||
|
|
||||||
@ -116,17 +116,14 @@ public class AzureSendFileStorageService : ISendFileStorageService
|
|||||||
await blobClient.SetHttpHeadersAsync(headers);
|
await blobClient.SetHttpHeadersAsync(headers);
|
||||||
|
|
||||||
var length = blobProperties.Value.ContentLength;
|
var length = blobProperties.Value.ContentLength;
|
||||||
if (length < expectedFileSize - leeway || length > expectedFileSize + leeway)
|
var valid = minimum <= length || length <= maximum;
|
||||||
{
|
|
||||||
return (false, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, length);
|
return (valid, length);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Unhandled error in ValidateFileAsync");
|
_logger.LogError(ex, $"A storage operation failed in {nameof(ValidateFileAsync)}");
|
||||||
return (false, null);
|
return (false, -1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,16 +56,13 @@ public interface ISendFileStorageService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="send"><see cref="Send" /> used to help validate file</param>
|
/// <param name="send"><see cref="Send" /> used to help validate file</param>
|
||||||
/// <param name="fileId">File id to identify which file to validate</param>
|
/// <param name="fileId">File id to identify which file to validate</param>
|
||||||
/// <param name="expectedFileSize">Expected file size of the file</param>
|
/// <param name="minimum">The minimum allowed length of the stored file in bytes.</param>
|
||||||
/// <param name="leeway">
|
/// <param name="maximum">The maximuim allowed length of the stored file in bytes</param>
|
||||||
/// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize`
|
/// <returns>
|
||||||
/// is outside of the leeway, the storage operation fails.
|
/// A task that completes when validation is finished. The first element of the tuple is
|
||||||
/// </param>
|
/// <see langword="true" /> when validation succeeded, and false otherwise. The second element
|
||||||
/// <throws>
|
/// of the tuple contains the observed file length in bytes. If an error occurs during validation,
|
||||||
/// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect
|
/// this returns `-1`.
|
||||||
/// </throws>
|
|
||||||
/// <returns>Task object for async operations with Tuple of boolean that determines if file was valid and long that
|
|
||||||
/// the actual file size of the file.
|
|
||||||
/// </returns>
|
/// </returns>
|
||||||
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
Task<(bool valid, long length)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum);
|
||||||
}
|
}
|
||||||
|
@ -85,9 +85,9 @@ public class LocalSendStorageService : ISendFileStorageService
|
|||||||
public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
||||||
=> Task.FromResult($"/sends/{send.Id}/file/{fileId}");
|
=> Task.FromResult($"/sends/{send.Id}/file/{fileId}");
|
||||||
|
|
||||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||||
{
|
{
|
||||||
long? length = null;
|
long length = -1;
|
||||||
var path = FilePath(send, fileId);
|
var path = FilePath(send, fileId);
|
||||||
if (!File.Exists(path))
|
if (!File.Exists(path))
|
||||||
{
|
{
|
||||||
@ -95,11 +95,7 @@ public class LocalSendStorageService : ISendFileStorageService
|
|||||||
}
|
}
|
||||||
|
|
||||||
length = new FileInfo(path).Length;
|
length = new FileInfo(path).Length;
|
||||||
if (expectedFileSize < length - leeway || expectedFileSize > length + leeway)
|
var valid = minimum < length || length < maximum;
|
||||||
{
|
return Task.FromResult((valid, length));
|
||||||
return Task.FromResult((false, length));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult((true, length));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,8 @@ public class NoopSendFileStorageService : ISendFileStorageService
|
|||||||
return Task.FromResult((string)null);
|
return Task.FromResult((string)null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||||
{
|
{
|
||||||
return Task.FromResult((false, default(long?)));
|
return Task.FromResult((false, -1L));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -143,7 +143,7 @@ public class CipherService : ICipherService
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)).DisablePersonalOwnership
|
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)).State == PersonalOwnershipState.Restricted
|
||||||
: await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
: await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
||||||
|
|
||||||
if (isPersonalVaultRestricted)
|
if (isPersonalVaultRestricted)
|
||||||
|
@ -71,6 +71,7 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||||
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||||
|
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.Dapper.Dirt;
|
||||||
|
|
||||||
|
public class OrganizationMemberBaseDetailRepository : BaseRepository, IOrganizationMemberBaseDetailRepository
|
||||||
|
{
|
||||||
|
public OrganizationMemberBaseDetailRepository(GlobalSettings globalSettings)
|
||||||
|
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public OrganizationMemberBaseDetailRepository(string connectionString, string readOnlyConnectionString) : base(
|
||||||
|
connectionString, readOnlyConnectionString)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
|
||||||
|
var result = await connection.QueryAsync<OrganizationMemberBaseDetail>(
|
||||||
|
"[dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId]",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId
|
||||||
|
|
||||||
|
}, commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
|
using Bit.Core.Dirt.Reports.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Dirt;
|
||||||
|
|
||||||
|
public class OrganizationMemberBaseDetailRepository : BaseEntityFrameworkRepository, IOrganizationMemberBaseDetailRepository
|
||||||
|
{
|
||||||
|
public OrganizationMemberBaseDetailRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(
|
||||||
|
serviceScopeFactory,
|
||||||
|
mapper)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<OrganizationMemberBaseDetail>> GetOrganizationMemberBaseDetailsByOrganizationId(
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var result = await dbContext.Set<OrganizationMemberBaseDetail>()
|
||||||
|
.FromSqlRaw("EXEC [dbo].[MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId] @OrganizationId",
|
||||||
|
new SqlParameter("@OrganizationId", organizationId))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ using Bit.Core.Vault.Repositories;
|
|||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Dirt;
|
||||||
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
|
using Bit.Infrastructure.EntityFramework.Dirt.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
|
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
|
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
|
||||||
@ -108,6 +109,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
|||||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||||
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
services.AddSingleton<IUserSignatureKeyPairRepository, UserSignatureKeyPairRepository>();
|
||||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||||
|
services.AddSingleton<IOrganizationMemberBaseDetailRepository, OrganizationMemberBaseDetailRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Dirt.Reports.Models.Data;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||||
@ -81,6 +82,7 @@ public class DatabaseContext : DbContext
|
|||||||
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
|
||||||
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
|
||||||
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
|
||||||
|
public DbSet<OrganizationMemberBaseDetail> OrganizationMemberBaseDetails { get; set; }
|
||||||
public DbSet<SecurityTask> SecurityTasks { get; set; }
|
public DbSet<SecurityTask> SecurityTasks { get; set; }
|
||||||
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }
|
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }
|
||||||
|
|
||||||
@ -113,6 +115,7 @@ public class DatabaseContext : DbContext
|
|||||||
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
var eOrganizationConnection = builder.Entity<OrganizationConnection>();
|
||||||
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
||||||
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
||||||
|
var eOrganizationMemberBaseDetail = builder.Entity<OrganizationMemberBaseDetail>();
|
||||||
|
|
||||||
// Shadow property configurations go here
|
// Shadow property configurations go here
|
||||||
|
|
||||||
@ -135,6 +138,8 @@ public class DatabaseContext : DbContext
|
|||||||
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
||||||
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
||||||
|
|
||||||
|
eOrganizationMemberBaseDetail.HasNoKey();
|
||||||
|
|
||||||
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
||||||
Constants.DatabaseFieldProtectorPurpose);
|
Constants.DatabaseFieldProtectorPurpose);
|
||||||
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
||||||
|
@ -5,7 +5,7 @@ using System.Security.Cryptography.X509Certificates;
|
|||||||
using AspNetCoreRateLimit;
|
using AspNetCoreRateLimit;
|
||||||
using Azure.Storage.Queues;
|
using Azure.Storage.Queues;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.AdminConsole.Services.Implementations;
|
using Bit.Core.AdminConsole.Services.Implementations;
|
||||||
@ -288,7 +288,7 @@ public static class ServiceCollectionExtensions
|
|||||||
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Installation.Key))
|
CoreHelpers.SettingHasValue(globalSettings.Installation.Key))
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, RelayPushNotificationService>());
|
||||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -299,20 +299,20 @@ public static class ServiceCollectionExtensions
|
|||||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationsApiPushNotificationService>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
||||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||||
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationHubPushNotificationService>());
|
||||||
|
services.TryAddSingleton<IPushRelayer, NotificationHubPushNotificationService>();
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton("notifications",
|
services.AddKeyedSingleton("notifications",
|
||||||
(_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"));
|
(_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"));
|
||||||
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>(
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, AzureQueuePushNotificationService>());
|
||||||
"implementation");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +366,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddSingleton<IMailService, NoopMailService>();
|
services.AddSingleton<IMailService, NoopMailService>();
|
||||||
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
|
||||||
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
|
|
||||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||||
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
|
||||||
services.AddSingleton<ILicensingService, NoopLicensingService>();
|
services.AddSingleton<ILicensingService, NoopLicensingService>();
|
||||||
@ -718,7 +717,8 @@ public static class ServiceCollectionExtensions
|
|||||||
retryQueueName: integrationRetryQueueName,
|
retryQueueName: integrationRetryQueueName,
|
||||||
maxRetries: maxRetries,
|
maxRetries: maxRetries,
|
||||||
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
|
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
|
||||||
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>()));
|
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>(),
|
||||||
|
timeProvider: provider.GetRequiredService<TimeProvider>()));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
CREATE PROCEDURE dbo.MemberAccessReport_GetMemberAccessCipherDetailsByOrganizationId
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @OrganizationId IS NULL
|
||||||
|
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
OU.Id AS UserGuid,
|
||||||
|
U.Name AS UserName,
|
||||||
|
ISNULL(U.Email, OU.Email) as 'Email',
|
||||||
|
U.TwoFactorProviders,
|
||||||
|
U.UsesKeyConnector,
|
||||||
|
OU.ResetPasswordKey,
|
||||||
|
CC.CollectionId,
|
||||||
|
C.Name AS CollectionName,
|
||||||
|
NULL AS GroupId,
|
||||||
|
NULL AS GroupName,
|
||||||
|
CU.ReadOnly,
|
||||||
|
CU.HidePasswords,
|
||||||
|
CU.Manage,
|
||||||
|
Cipher.Id AS CipherId
|
||||||
|
FROM dbo.OrganizationUser OU
|
||||||
|
LEFT JOIN dbo.[User] U ON U.Id = OU.UserId
|
||||||
|
INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId
|
||||||
|
AND O.Id = @OrganizationId
|
||||||
|
AND O.Enabled = 1
|
||||||
|
INNER JOIN dbo.CollectionUser CU ON CU.OrganizationUserId = OU.Id
|
||||||
|
INNER JOIN dbo.Collection C ON C.Id = CU.CollectionId and C.OrganizationId = @OrganizationId
|
||||||
|
INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
|
||||||
|
INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId AND Cipher.OrganizationId = @OrganizationId
|
||||||
|
WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
|
||||||
|
AND Cipher.DeletedDate IS NULL
|
||||||
|
UNION ALL
|
||||||
|
-- Group-based collection permissions
|
||||||
|
SELECT
|
||||||
|
OU.Id AS UserGuid,
|
||||||
|
U.Name AS UserName,
|
||||||
|
ISNULL(U.Email, OU.Email) as 'Email',
|
||||||
|
U.TwoFactorProviders,
|
||||||
|
U.UsesKeyConnector,
|
||||||
|
OU.ResetPasswordKey,
|
||||||
|
CC.CollectionId,
|
||||||
|
C.Name AS CollectionName,
|
||||||
|
G.Id AS GroupId,
|
||||||
|
G.Name AS GroupName,
|
||||||
|
CG.ReadOnly,
|
||||||
|
CG.HidePasswords,
|
||||||
|
CG.Manage,
|
||||||
|
Cipher.Id AS CipherId
|
||||||
|
FROM dbo.OrganizationUser OU
|
||||||
|
LEFT JOIN dbo.[User] U ON U.Id = OU.UserId
|
||||||
|
INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId
|
||||||
|
AND O.Id = @OrganizationId
|
||||||
|
AND O.Enabled = 1
|
||||||
|
INNER JOIN dbo.GroupUser GU ON GU.OrganizationUserId = OU.Id
|
||||||
|
INNER JOIN dbo.[Group] G ON G.Id = GU.GroupId
|
||||||
|
INNER JOIN dbo.CollectionGroup CG ON CG.GroupId = G.Id
|
||||||
|
INNER JOIN dbo.Collection C ON C.Id = CG.CollectionId AND C.OrganizationId = @OrganizationId
|
||||||
|
INNER JOIN dbo.CollectionCipher CC ON CC.CollectionId = C.Id
|
||||||
|
INNER JOIN dbo.Cipher Cipher ON Cipher.Id = CC.CipherId and Cipher.OrganizationId = @OrganizationId
|
||||||
|
WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
|
||||||
|
AND Cipher.DeletedDate IS NULL
|
||||||
|
UNION ALL
|
||||||
|
-- Users without collection access (invited users)
|
||||||
|
-- typically invited users who have not yet accepted the invitation
|
||||||
|
-- and not yet assigned to any collection
|
||||||
|
SELECT
|
||||||
|
OU.Id AS UserGuid,
|
||||||
|
U.Name AS UserName,
|
||||||
|
ISNULL(U.Email, OU.Email) as 'Email',
|
||||||
|
U.TwoFactorProviders,
|
||||||
|
U.UsesKeyConnector,
|
||||||
|
OU.ResetPasswordKey,
|
||||||
|
null as CollectionId,
|
||||||
|
null AS CollectionName,
|
||||||
|
NULL AS GroupId,
|
||||||
|
NULL AS GroupName,
|
||||||
|
null as [ReadOnly],
|
||||||
|
null as HidePasswords,
|
||||||
|
null as Manage,
|
||||||
|
null AS CipherId
|
||||||
|
FROM dbo.OrganizationUser OU
|
||||||
|
LEFT JOIN dbo.[User] U ON U.Id = OU.UserId
|
||||||
|
INNER JOIN dbo.Organization O ON O.Id = OU.OrganizationId AND O.Id = @OrganizationId AND O.Enabled = 1
|
||||||
|
WHERE OU.Status IN (0,1,2) -- Invited, Accepted and Confirmed Users
|
||||||
|
AND OU.Id not in (
|
||||||
|
select OU1.Id from dbo.OrganizationUser OU1
|
||||||
|
inner join dbo.CollectionUser CU1 on CU1.OrganizationUserId = OU1.Id
|
||||||
|
WHERE OU1.OrganizationId = @organizationId
|
||||||
|
)
|
@ -28,6 +28,8 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
_identityApplicationFactory.ManagesDatabase = false;
|
_identityApplicationFactory.ManagesDatabase = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IdentityApplicationFactory Identity => _identityApplicationFactory;
|
||||||
|
|
||||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||||
{
|
{
|
||||||
base.ConfigureWebHost(builder);
|
base.ConfigureWebHost(builder);
|
||||||
|
@ -0,0 +1,449 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Azure.Storage.Queues;
|
||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.NotificationHub;
|
||||||
|
using Bit.Core.Platform.Installations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using static Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.Platform.Controllers;
|
||||||
|
|
||||||
|
public class PushControllerTests
|
||||||
|
{
|
||||||
|
private static readonly Guid _userId = Guid.NewGuid();
|
||||||
|
private static readonly Guid _organizationId = Guid.NewGuid();
|
||||||
|
private static readonly Guid _deviceId = Guid.NewGuid();
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> SendData()
|
||||||
|
{
|
||||||
|
static object[] Typed<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall = true)
|
||||||
|
{
|
||||||
|
return [pushSendRequestModel, expectedHubTagExpression, expectHubCall];
|
||||||
|
}
|
||||||
|
|
||||||
|
static object[] UserTyped(PushType pushType)
|
||||||
|
{
|
||||||
|
return Typed(new PushSendRequestModel<UserPushNotification>
|
||||||
|
{
|
||||||
|
Type = pushType,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new UserPushNotification
|
||||||
|
{
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cipher
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherUpdate,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
// Organization cipher, an org cipher would not naturally be synced from our
|
||||||
|
// code but it is technically possible to be submitted to the endpoint.
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherUpdate,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherCreate,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
// Organization cipher, an org cipher would not naturally be synced from our
|
||||||
|
// code but it is technically possible to be submitted to the endpoint.
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherCreate,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherDelete,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
// Organization cipher, an org cipher would not naturally be synced from our
|
||||||
|
// code but it is technically possible to be submitted to the endpoint.
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncCipherDelete,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncCipherPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderDelete,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderCreate,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.SyncFolderCreate,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new SyncFolderPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return UserTyped(PushType.SyncCiphers);
|
||||||
|
yield return UserTyped(PushType.SyncVault);
|
||||||
|
yield return UserTyped(PushType.SyncOrganizations);
|
||||||
|
yield return UserTyped(PushType.SyncOrgKeys);
|
||||||
|
yield return UserTyped(PushType.SyncSettings);
|
||||||
|
yield return UserTyped(PushType.LogOut);
|
||||||
|
yield return UserTyped(PushType.PendingSecurityTasks);
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.AuthRequest,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new AuthRequestPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.AuthRequestResponse,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new AuthRequestPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.Notification,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.Notification,
|
||||||
|
UserId = _userId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
ClientType = ClientType.All,
|
||||||
|
Payload = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Global = true,
|
||||||
|
},
|
||||||
|
}, $"(template:payload_userId:%installation%_{_userId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.NotificationStatus,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||||
|
|
||||||
|
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.NotificationStatus,
|
||||||
|
OrganizationId = _organizationId,
|
||||||
|
DeviceId = _deviceId,
|
||||||
|
Payload = new NotificationPushNotification
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
UserId = _userId,
|
||||||
|
},
|
||||||
|
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SendData))]
|
||||||
|
public async Task Send_Works<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall)
|
||||||
|
{
|
||||||
|
var (apiFactory, httpClient, installation, queueClient, notificationHubProxy) = await SetupTest();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var pushSendResponse = await httpClient.PostAsJsonAsync("push/send", pushSendRequestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
pushSendResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
// Relayed notifications, the ones coming to this endpoint should
|
||||||
|
// not make their way into our Azure Queue and instead should only be sent to Azure Notifications
|
||||||
|
// hub.
|
||||||
|
await queueClient
|
||||||
|
.Received(0)
|
||||||
|
.SendMessageAsync(Arg.Any<string>());
|
||||||
|
|
||||||
|
// Check that this notification was sent through hubs the expected number of times
|
||||||
|
await notificationHubProxy
|
||||||
|
.Received(expectHubCall ? 1 : 0)
|
||||||
|
.SendTemplateNotificationAsync(
|
||||||
|
Arg.Any<Dictionary<string, string>>(),
|
||||||
|
Arg.Is(expectedHubTagExpression.Replace("%installation%", installation.Id.ToString()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Expect on the dictionary more?
|
||||||
|
|
||||||
|
// Notifications being relayed from SH should have the device id
|
||||||
|
// tracked so that we can later send the notification to that device.
|
||||||
|
await apiFactory.GetService<IInstallationDeviceRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
|
||||||
|
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == pushSendRequestModel.DeviceId.ToString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Send_InstallationNotification_NotAuthenticatedInstallation_Fails()
|
||||||
|
{
|
||||||
|
var (_, httpClient, _, _, _) = await SetupTest();
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||||
|
{
|
||||||
|
Type = PushType.NotificationStatus,
|
||||||
|
InstallationId = Guid.NewGuid(),
|
||||||
|
Payload = new { }
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
|
||||||
|
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
|
||||||
|
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
|
||||||
|
Assert.Equal(JsonValueKind.String, message.GetValueKind());
|
||||||
|
Assert.Equal("InstallationId does not match current context.", message.GetValue<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Send_InstallationNotification_Works()
|
||||||
|
{
|
||||||
|
var (apiFactory, httpClient, installation, _, notificationHubProxy) = await SetupTest();
|
||||||
|
|
||||||
|
var deviceId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||||
|
{
|
||||||
|
Type = PushType.NotificationStatus,
|
||||||
|
InstallationId = installation.Id,
|
||||||
|
Payload = new { },
|
||||||
|
DeviceId = deviceId,
|
||||||
|
ClientType = ClientType.Web,
|
||||||
|
});
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await notificationHubProxy
|
||||||
|
.Received(1)
|
||||||
|
.SendTemplateNotificationAsync(
|
||||||
|
Arg.Any<Dictionary<string, string>>(),
|
||||||
|
Arg.Is($"(template:payload && installationId:{installation.Id} && clientType:Web)")
|
||||||
|
);
|
||||||
|
|
||||||
|
await apiFactory.GetService<IInstallationDeviceRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
|
||||||
|
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == deviceId.ToString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Send_NoOrganizationNoInstallationNoUser_FailsModelValidation()
|
||||||
|
{
|
||||||
|
var (_, client, _, _, _) = await SetupTest();
|
||||||
|
|
||||||
|
var response = await client.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
|
||||||
|
{
|
||||||
|
Type = PushType.AuthRequest,
|
||||||
|
Payload = new { },
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
|
||||||
|
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
|
||||||
|
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
|
||||||
|
Assert.Equal(JsonValueKind.String, message.GetValueKind());
|
||||||
|
Assert.Equal("The model state is invalid.", message.GetValue<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<(ApiApplicationFactory Factory, HttpClient AuthedClient, Installation Installation, QueueClient MockedQueue, INotificationHubProxy MockedHub)> SetupTest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var apiFactory = new ApiApplicationFactory();
|
||||||
|
|
||||||
|
var queueClient = Substitute.For<QueueClient>();
|
||||||
|
|
||||||
|
// Substitute the underlying queue messages will go to.
|
||||||
|
apiFactory.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
var queueClientService = services.FirstOrDefault(
|
||||||
|
sd => sd.ServiceKey == (object)"notifications"
|
||||||
|
&& sd.ServiceType == typeof(QueueClient)
|
||||||
|
) ?? throw new InvalidOperationException("Expected service was not found.");
|
||||||
|
|
||||||
|
services.Remove(queueClientService);
|
||||||
|
|
||||||
|
services.AddKeyedSingleton("notifications", queueClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
var notificationHubProxy = Substitute.For<INotificationHubProxy>();
|
||||||
|
|
||||||
|
apiFactory.SubstituteService<INotificationHubPool>(s =>
|
||||||
|
{
|
||||||
|
s.AllClients
|
||||||
|
.Returns(notificationHubProxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiFactory.SubstituteService<IInstallationDeviceRepository>(s => { });
|
||||||
|
|
||||||
|
// Setup as cloud with NotificationHub setup and Azure Queue
|
||||||
|
apiFactory.UpdateConfiguration("GlobalSettings:Notifications:ConnectionString", "any_value");
|
||||||
|
|
||||||
|
// Configure hubs
|
||||||
|
var index = 0;
|
||||||
|
void AddHub(NotificationHubSettings notificationHubSettings)
|
||||||
|
{
|
||||||
|
apiFactory.UpdateConfiguration(
|
||||||
|
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:ConnectionString",
|
||||||
|
notificationHubSettings.ConnectionString
|
||||||
|
);
|
||||||
|
apiFactory.UpdateConfiguration(
|
||||||
|
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:HubName",
|
||||||
|
notificationHubSettings.HubName
|
||||||
|
);
|
||||||
|
apiFactory.UpdateConfiguration(
|
||||||
|
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationStartDate",
|
||||||
|
notificationHubSettings.RegistrationStartDate?.ToString()
|
||||||
|
);
|
||||||
|
apiFactory.UpdateConfiguration(
|
||||||
|
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationEndDate",
|
||||||
|
notificationHubSettings.RegistrationEndDate?.ToString()
|
||||||
|
);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddHub(new NotificationHubSettings
|
||||||
|
{
|
||||||
|
ConnectionString = "some_value",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow.AddDays(-2),
|
||||||
|
});
|
||||||
|
|
||||||
|
var httpClient = apiFactory.CreateClient();
|
||||||
|
|
||||||
|
// Add installation into database
|
||||||
|
var installationRepository = apiFactory.GetService<IInstallationRepository>();
|
||||||
|
var installation = await installationRepository.CreateAsync(new Installation
|
||||||
|
{
|
||||||
|
Key = "my_test_key",
|
||||||
|
Email = "test@example.com",
|
||||||
|
Enabled = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var identityClient = apiFactory.Identity.CreateDefaultClient();
|
||||||
|
|
||||||
|
var connectTokenResponse = await identityClient.PostAsync("connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "grant_type", "client_credentials" },
|
||||||
|
{ "scope", "api.push" },
|
||||||
|
{ "client_id", $"installation.{installation.Id}" },
|
||||||
|
{ "client_secret", installation.Key },
|
||||||
|
}));
|
||||||
|
|
||||||
|
connectTokenResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var connectTokenResponseModel = await connectTokenResponse.Content.ReadFromJsonAsync<JsonNode>();
|
||||||
|
|
||||||
|
// Setup authentication
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
|
||||||
|
connectTokenResponseModel["token_type"].GetValue<string>(),
|
||||||
|
connectTokenResponseModel["access_token"].GetValue<string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (apiFactory, httpClient, installation, queueClient, notificationHubProxy);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ using Bit.Api.AdminConsole.Controllers;
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ public class UsersControllerTests
|
|||||||
SutProvider<UsersController> sutProvider)
|
SutProvider<UsersController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();
|
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid().ToString()));
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid()));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -37,7 +37,7 @@ public class UsersControllerTests
|
|||||||
var publicKey = "publicKey";
|
var publicKey = "publicKey";
|
||||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);
|
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetPublicKeyAsync(userId.ToString());
|
var result = await sutProvider.Sut.GetPublicKeyAsync(userId);
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Equal(userId, result.UserId);
|
Assert.Equal(userId, result.UserId);
|
||||||
Assert.Equal(publicKey, result.PublicKey);
|
Assert.Equal(publicKey, result.PublicKey);
|
||||||
|
@ -18,210 +18,6 @@ namespace Bit.Api.Test.Platform.Push.Controllers;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class PushControllerTests
|
public class PushControllerTests
|
||||||
{
|
{
|
||||||
[Theory]
|
|
||||||
[BitAutoData(false, true)]
|
|
||||||
[BitAutoData(false, false)]
|
|
||||||
[BitAutoData(true, true)]
|
|
||||||
public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted,
|
|
||||||
SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid organizationId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = selfHosted;
|
|
||||||
if (haveInstallationId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
||||||
sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = userId.ToString(),
|
|
||||||
OrganizationId = organizationId.ToString(),
|
|
||||||
InstallationId = installationId.ToString(),
|
|
||||||
Payload = "test-payload"
|
|
||||||
}));
|
|
||||||
|
|
||||||
Assert.Equal("Not correctly configured for push relays.", exception.Message);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
|
||||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent(
|
|
||||||
SutProvider<PushController> sutProvider, Guid installationId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
|
|
||||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = null,
|
|
||||||
OrganizationId = null,
|
|
||||||
InstallationId = null,
|
|
||||||
Payload = "test-payload"
|
|
||||||
});
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
|
||||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]
|
|
||||||
public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId,
|
|
||||||
bool haveOrganizationId, SutProvider<PushController> sutProvider, Guid installationId, Guid userId,
|
|
||||||
Guid identifier, Guid deviceId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
|
|
||||||
var expectedUserId = $"{installationId}_{userId}";
|
|
||||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
|
||||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
|
||||||
|
|
||||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = userId.ToString(),
|
|
||||||
OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null,
|
|
||||||
InstallationId = null,
|
|
||||||
Payload = "test-payload",
|
|
||||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
|
||||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
|
||||||
ClientType = ClientType.All,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
|
||||||
.SendPayloadToUserAsync(expectedUserId, PushType.Notification, "test-payload", expectedIdentifier,
|
|
||||||
expectedDeviceId, ClientType.All);
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[RepeatingPatternBitAutoData([false, true], [false, true])]
|
|
||||||
public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId,
|
|
||||||
SutProvider<PushController> sutProvider, Guid installationId, Guid organizationId, Guid identifier,
|
|
||||||
Guid deviceId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
|
|
||||||
var expectedOrganizationId = $"{installationId}_{organizationId}";
|
|
||||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
|
||||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
|
||||||
|
|
||||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = null,
|
|
||||||
OrganizationId = organizationId.ToString(),
|
|
||||||
InstallationId = null,
|
|
||||||
Payload = "test-payload",
|
|
||||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
|
||||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
|
||||||
ClientType = ClientType.All,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
|
||||||
.SendPayloadToOrganizationAsync(expectedOrganizationId, PushType.Notification, "test-payload",
|
|
||||||
expectedIdentifier, expectedDeviceId, ClientType.All);
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
|
||||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[RepeatingPatternBitAutoData([false, true], [false, true])]
|
|
||||||
public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId,
|
|
||||||
SutProvider<PushController> sutProvider, Guid installationId, Guid identifier, Guid deviceId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
|
|
||||||
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
|
|
||||||
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
|
|
||||||
|
|
||||||
await sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = null,
|
|
||||||
OrganizationId = null,
|
|
||||||
InstallationId = installationId.ToString(),
|
|
||||||
Payload = "test-payload",
|
|
||||||
DeviceId = haveDeviceId ? deviceId.ToString() : null,
|
|
||||||
Identifier = haveIdentifier ? identifier.ToString() : null,
|
|
||||||
ClientType = ClientType.All,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
|
||||||
.SendPayloadToInstallationAsync(installationId.ToString(), PushType.Notification, "test-payload",
|
|
||||||
expectedIdentifier, expectedDeviceId, ClientType.All);
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
|
||||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider<PushController> sutProvider,
|
|
||||||
Guid installationId)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
||||||
sutProvider.Sut.SendAsync(new PushSendRequestModel
|
|
||||||
{
|
|
||||||
Type = PushType.Notification,
|
|
||||||
UserId = null,
|
|
||||||
OrganizationId = null,
|
|
||||||
InstallationId = Guid.NewGuid().ToString(),
|
|
||||||
Payload = "test-payload",
|
|
||||||
DeviceId = null,
|
|
||||||
Identifier = null,
|
|
||||||
ClientType = ClientType.All,
|
|
||||||
}));
|
|
||||||
|
|
||||||
Assert.Equal("InstallationId does not match current context.", exception.Message);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
|
|
||||||
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
|
|
||||||
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
|
|
||||||
Arg.Any<string?>(), Arg.Any<ClientType?>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(false, true)]
|
[BitAutoData(false, true)]
|
||||||
[BitAutoData(false, false)]
|
[BitAutoData(false, false)]
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Models.Data.Integrations;
|
namespace Bit.Core.Test.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
public class IntegrationMessageTests
|
public class IntegrationMessageTests
|
||||||
{
|
{
|
||||||
@ -45,6 +45,7 @@ public class IntegrationMessageTests
|
|||||||
var json = message.ToJson();
|
var json = message.ToJson();
|
||||||
var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json);
|
var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
Assert.Equal(message.Configuration, result.Configuration);
|
Assert.Equal(message.Configuration, result.Configuration);
|
||||||
Assert.Equal(message.MessageId, result.MessageId);
|
Assert.Equal(message.MessageId, result.MessageId);
|
||||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
@ -10,6 +10,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -442,4 +443,98 @@ public class ConfirmOrganizationUserCommandTests
|
|||||||
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
||||||
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithValidCollectionName_CreatesDefaultCollection(
|
||||||
|
Organization organization, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = organization.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(user.Id)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement(
|
||||||
|
PersonalOwnershipState.Restricted,
|
||||||
|
[organization.Id]));
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateAsync(
|
||||||
|
Arg.Is<Collection>(c => c.Name == collectionName &&
|
||||||
|
c.OrganizationId == organization.Id &&
|
||||||
|
c.Type == CollectionType.DefaultUserCollection),
|
||||||
|
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
|
||||||
|
Arg.Is<IEnumerable<CollectionAccessSelection>>(u =>
|
||||||
|
u.Count() == 1 &&
|
||||||
|
u.First().Id == orgUser.Id &&
|
||||||
|
u.First().Manage == true));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyApplicable_WithInvalidCollectionName_DoesNotCreateDefaultCollection(
|
||||||
|
Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
string key, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(user.Id)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement(
|
||||||
|
PersonalOwnershipState.Restricted,
|
||||||
|
[org.Id]));
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, "");
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ConfirmUserAsync_WithCreateDefaultLocationEnabled_WithPersonalOwnershipPolicyNotApplicable_DoesNotCreateDefaultCollection(
|
||||||
|
Organization org, OrganizationUser confirmingUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
|
||||||
|
string key, string collectionName, SutProvider<ConfirmOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
org.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
|
||||||
|
orgUser.UserId = user.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.CreateDefaultLocation).Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(user.Id)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement(
|
||||||
|
PersonalOwnershipState.Restricted,
|
||||||
|
[Guid.NewGuid()]));
|
||||||
|
|
||||||
|
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, key, confirmingUser.Id, collectionName);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.CreateAsync(Arg.Any<Collection>(), Arg.Any<IEnumerable<CollectionAccessSelection>>(), Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,20 +12,42 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequire
|
|||||||
public class PersonalOwnershipPolicyRequirementFactoryTests
|
public class PersonalOwnershipPolicyRequirementFactoryTests
|
||||||
{
|
{
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
public void State_WithNoPolicies_ReturnsAllowed(SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
{
|
{
|
||||||
var actual = sutProvider.Sut.Create([]);
|
var actual = sutProvider.Sut.Create([]);
|
||||||
|
|
||||||
Assert.False(actual.DisablePersonalOwnership);
|
Assert.Equal(PersonalOwnershipState.Allowed, actual.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue(
|
public void State_WithPersonalOwnershipPolicies_ReturnsRestricted(
|
||||||
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
|
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
|
||||||
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
{
|
{
|
||||||
var actual = sutProvider.Sut.Create(policies);
|
var actual = sutProvider.Sut.Create(policies);
|
||||||
|
|
||||||
Assert.True(actual.DisablePersonalOwnership);
|
Assert.Equal(PersonalOwnershipState.Restricted, actual.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void RequiresDefaultCollection_WithNoPolicies_ReturnsFalse(
|
||||||
|
Guid organizationId,
|
||||||
|
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create([]);
|
||||||
|
|
||||||
|
Assert.False(actual.RequiresDefaultCollection(organizationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void RequiresDefaultCollection_WithPersonalOwnershipPolicies_ReturnsCorrectResult(
|
||||||
|
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
|
||||||
|
Guid nonPolicyOrganizationId,
|
||||||
|
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create(policies);
|
||||||
|
|
||||||
|
Assert.True(actual.RequiresDefaultCollection(policies[0].OrganizationId));
|
||||||
|
Assert.False(actual.RequiresDefaultCollection(nonPolicyOrganizationId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using Azure.Messaging.ServiceBus;
|
using Azure.Messaging.ServiceBus;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -52,7 +52,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
|||||||
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider();
|
var sutProvider = GetSutProvider();
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
|
||||||
message.RetryCount = 0;
|
message.RetryCount = 0;
|
||||||
|
|
||||||
var result = new IntegrationHandlerResult(false, message);
|
var result = new IntegrationHandlerResult(false, message);
|
||||||
@ -71,7 +70,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
|||||||
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider();
|
var sutProvider = GetSutProvider();
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
|
||||||
message.RetryCount = _maxRetries;
|
message.RetryCount = _maxRetries;
|
||||||
var result = new IntegrationHandlerResult(false, message);
|
var result = new IntegrationHandlerResult(false, message);
|
||||||
result.Retryable = true;
|
result.Retryable = true;
|
||||||
@ -90,12 +88,10 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
|||||||
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider();
|
var sutProvider = GetSutProvider();
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
|
||||||
message.RetryCount = 0;
|
message.RetryCount = 0;
|
||||||
|
|
||||||
var result = new IntegrationHandlerResult(false, message);
|
var result = new IntegrationHandlerResult(false, message);
|
||||||
result.Retryable = true;
|
result.Retryable = true;
|
||||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
|
||||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||||
|
|
||||||
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
var expected = (IntegrationMessage<WebhookIntegrationConfiguration>)IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson())!;
|
||||||
@ -110,7 +106,6 @@ public class AzureServiceBusIntegrationListenerServiceTests
|
|||||||
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage<WebhookIntegrationConfiguration> message)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider();
|
var sutProvider = GetSutProvider();
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
|
||||||
var result = new IntegrationHandlerResult(true, message);
|
var result = new IntegrationHandlerResult(true, message);
|
||||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
@ -18,19 +19,24 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
private const string _queueName = "test_queue";
|
private const string _queueName = "test_queue";
|
||||||
private const string _retryQueueName = "test_queue_retry";
|
private const string _retryQueueName = "test_queue_retry";
|
||||||
private const string _routingKey = "test_routing_key";
|
private const string _routingKey = "test_routing_key";
|
||||||
|
private readonly DateTime _now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||||
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
|
private readonly IIntegrationHandler _handler = Substitute.For<IIntegrationHandler>();
|
||||||
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
|
private readonly IRabbitMqService _rabbitMqService = Substitute.For<IRabbitMqService>();
|
||||||
|
|
||||||
private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider()
|
private SutProvider<RabbitMqIntegrationListenerService> GetSutProvider()
|
||||||
{
|
{
|
||||||
return new SutProvider<RabbitMqIntegrationListenerService>()
|
var sutProvider = new SutProvider<RabbitMqIntegrationListenerService>()
|
||||||
.SetDependency(_handler)
|
.SetDependency(_handler)
|
||||||
.SetDependency(_rabbitMqService)
|
.SetDependency(_rabbitMqService)
|
||||||
.SetDependency(_queueName, "queueName")
|
.SetDependency(_queueName, "queueName")
|
||||||
.SetDependency(_retryQueueName, "retryQueueName")
|
.SetDependency(_retryQueueName, "retryQueueName")
|
||||||
.SetDependency(_routingKey, "routingKey")
|
.SetDependency(_routingKey, "routingKey")
|
||||||
.SetDependency(_maxRetries, "maxRetries")
|
.SetDependency(_maxRetries, "maxRetries")
|
||||||
|
.WithFakeTimeProvider()
|
||||||
.Create();
|
.Create();
|
||||||
|
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(_now);
|
||||||
|
|
||||||
|
return sutProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -55,7 +61,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||||
|
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
message.DelayUntilDate = null;
|
||||||
message.RetryCount = 0;
|
message.RetryCount = 0;
|
||||||
var eventArgs = new BasicDeliverEventArgs(
|
var eventArgs = new BasicDeliverEventArgs(
|
||||||
consumerTag: string.Empty,
|
consumerTag: string.Empty,
|
||||||
@ -94,7 +100,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||||
|
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
message.DelayUntilDate = null;
|
||||||
message.RetryCount = _maxRetries;
|
message.RetryCount = _maxRetries;
|
||||||
var eventArgs = new BasicDeliverEventArgs(
|
var eventArgs = new BasicDeliverEventArgs(
|
||||||
consumerTag: string.Empty,
|
consumerTag: string.Empty,
|
||||||
@ -132,7 +138,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||||
|
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
message.DelayUntilDate = null;
|
||||||
message.RetryCount = 0;
|
message.RetryCount = 0;
|
||||||
var eventArgs = new BasicDeliverEventArgs(
|
var eventArgs = new BasicDeliverEventArgs(
|
||||||
consumerTag: string.Empty,
|
consumerTag: string.Empty,
|
||||||
@ -145,7 +151,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
);
|
);
|
||||||
var result = new IntegrationHandlerResult(false, message);
|
var result = new IntegrationHandlerResult(false, message);
|
||||||
result.Retryable = true;
|
result.Retryable = true;
|
||||||
result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
result.DelayUntilDate = _now.AddMinutes(1);
|
||||||
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
_handler.HandleAsync(Arg.Any<string>()).Returns(result);
|
||||||
|
|
||||||
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
var expected = IntegrationMessage<WebhookIntegrationConfiguration>.FromJson(message.ToJson());
|
||||||
@ -173,7 +179,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||||
|
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1);
|
message.DelayUntilDate = null;
|
||||||
var eventArgs = new BasicDeliverEventArgs(
|
var eventArgs = new BasicDeliverEventArgs(
|
||||||
consumerTag: string.Empty,
|
consumerTag: string.Empty,
|
||||||
deliveryTag: 0,
|
deliveryTag: 0,
|
||||||
@ -205,7 +211,7 @@ public class RabbitMqIntegrationListenerServiceTests
|
|||||||
var cancellationToken = CancellationToken.None;
|
var cancellationToken = CancellationToken.None;
|
||||||
await sutProvider.Sut.StartAsync(cancellationToken);
|
await sutProvider.Sut.StartAsync(cancellationToken);
|
||||||
|
|
||||||
message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1);
|
message.DelayUntilDate = _now.AddMinutes(1);
|
||||||
var eventArgs = new BasicDeliverEventArgs(
|
var eventArgs = new BasicDeliverEventArgs(
|
||||||
consumerTag: string.Empty,
|
consumerTag: string.Empty,
|
||||||
deliveryTag: 0,
|
deliveryTag: 0,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user