Skip to content

Commit

Permalink
Auth/PM-3275 - Changes to support TDE User without MP being able to S…
Browse files Browse the repository at this point in the history
…et a Password + misc refactoring (#3242)

* PM-3275 - Add new GetMasterPasswordPolicy endpoint which will allow authenticated clients to get an enabled MP org policy if it exists for the purposes of enforcing those policy requirements when setting a password.

* PM-3275 - AccountsController.cs - PostSetPasswordAsync - (1) Convert UserService.setPasswordAsync into new SetInitialMasterPasswordCommand (2) Refactor SetInitialMasterPasswordCommand to only accept post SSO users who are in the invited state
(3) Add TODOs for more cleanup work and more commands

* PM-3275 - Update AccountsControllerTests.cs to add new SetInitialMasterPasswordCommand

* PM-3275 - UserService.cs - Remove non implemented ChangePasswordAsync method

* PM-3275 - The new SetInitialMasterPasswordCommand leveraged the OrganizationService.cs AcceptUserAsync method so while I was in here I converted the AcceptUserAsync methods into a new AcceptOrgUserCommand.cs and turned the private method which accepted an existing org user public for use in the SetInitialMasterPasswordCommand

* PM-3275 - Dotnet format

* PM-3275 - Test SetInitialMasterPasswordCommand

* Dotnet format

* PM-3275 - In process AcceptOrgUserCommandTests.cs

* PM-3275 - Migrate changes from AC-244 / #3199 over into new AcceptOrgUserCommand

* PM-3275 - AcceptOrgUserCommand.cs - create data protector specifically for this command

* PM-3275 - Add TODO for renaming / removing overloading of methods to improve readability / clarity

* PM-3275 - AcceptOrgUserCommand.cs - refactor AcceptOrgUserAsync by OrgId to retrieve orgUser with _organizationUserRepository.GetByOrganizationAsync which gets a single user instead of a collection

* PM-3275 - AcceptOrgUserCommand.cs - update name in TODO for evaluation later

* PM-3275 / PM-1196 - (1) Slightly refactor SsoEmail2faSessionTokenable to provide public static GetTokenLifeTime() method for testing (2) Add missed tests to SsoEmail2faSessionTokenable in preparation for building tests for new OrgUserInviteTokenable.cs

* PM-3275 / PM-1196 - Removing SsoEmail2faSessionTokenable.cs changes + tests as I've handled that separately in a new PR (#3270) for newly created task PM-3925

* PM-3275 - ExpiringTokenable.cs - add clarifying comments to help distinguish between the Valid property and the TokenIsValid method.

* PM-3275 - Create OrgUserInviteTokenable.cs and add tests in OrgUserInviteTokenableTests.cs

* PM-3275 - OrganizationService.cs - Refactor Org User Invite methods to use new OrgUserInviteTokenable instead of manual creation of a token

* PM-3275 - OrgUserInviteTokenable.cs - clarify backwards compat note

* PM-3275 - AcceptOrgUserCommand.cs - Add TODOs + minor name refactor

* PM-3275 - AcceptOrgUserCommand.cs - replace method overloading with more easily readable names.

* PM-3275 - AcceptOrgUserCommand.cs - Update ValidateOrgUserInviteToken to add new token validation while maintaining backwards compatibility for 1 release.

* dotnet format

* PM-3275 - AcceptOrgUserCommand.cs - Move private method below where it is used

* PM-3275 - ServiceCollectionExtensions.cs - Must register IDataProtectorTokenFactory<OrgUserInviteTokenable> for new tokenable

* PM-3275 - OrgUserInviteTokenable needed access to global settings to set its token lifetime to the _globalSettings.OrganizationInviteExpirationHours value.  Creating a factory seemed the most straightforward way to encapsulate the desired creation logic. Unsure if in the correct location in ServiceCollectionExtensions.cs but will figure that out later.

* PM-3275 - In process work of creating AcceptOrgUserCommandTests.cs

* PM-3275 - Remove no longer relevant AcceptOrgUser tests from OrganizationServiceTests.cs

* PM-3275 - Register OrgUserInviteTokenableFactory alongside tokenizer

* PM-3275 - AcceptOrgUserCommandTests.cs - AcceptOrgUserAsync basic test suite completed.

* PM-3275 - AcceptOrgUserCommandTests.cs - tweak test names

* PM-3275 - AcceptOrgUserCommandTests.cs - (1) Remove old tests from OrganizationServiceTests as no longer needed to reference (2) Add summary for SetupCommonAcceptOrgUserMocks (3) Get AcceptOrgUserByToken_OldToken_AcceptsUserAndVerifiesEmail passing

* PM-3275 - Create interface for OrgUserInviteTokenableFactory b/c that's the right thing to do + enables test substitution

* PM-3275 - AcceptOrgUserCommandTests.cs - (1) Start work on AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail (2) Create and use SetupCommonAcceptOrgUserByTokenMocks() (3) Create generic FakeDataProtectorTokenFactory for tokenable testing

* PM-3275 - (1) Get AcceptOrgUserByToken_NewToken_AcceptsUserAndVerifiesEmail test passing (2) Move FakeDataProtectorTokenFactory to own file

* PM-3275 - AcceptOrgUserCommandTests.cs - Finish up tests for AcceptOrgUserByTokenAsync

* PM-3275 - Add pseudo section comments

* PM-3275 - Clean up unused params on AcceptOrgUserByToken_EmailMismatch_ThrowsBadRequest test

* PM-3275 - (1) Tests written for AcceptOrgUserByOrgSsoIdAsync (2) Refactor happy path assertions into helper function AssertValidAcceptedOrgUser to reduce code duplication

* PM-3275 - Finish up testing AcceptOrgUserCommandTests.cs by adding tests for AcceptOrgUserByOrgIdAsync

* PM-3275 - Tweaking test naming to ensure consistency.

* PM-3275 - Bugfix - OrgUserInviteTokenableFactory implementation required when declaring singleton service in ServiceCollectionExtensions.cs

* PM-3275 - Resolve failing OrganizationServiceTests.cs

* dotnet format

* PM-3275 - PoliciesController.cs - GetMasterPasswordPolicy bugfix - for orgs without a MP policy, policy comes back as null and we should return notFound in that case.

* PM-3275 - Add PoliciesControllerTests.cs specifically for new GetMasterPasswordPolicy(...) endpoint.

* PM-3275 - dotnet format PoliciesControllerTests.cs

* PM-3275 - PoliciesController.cs - (1) Add tech debt task number (2) Properly flag endpoint as deprecated

* PM-3275 - Add new hasManageResetPasswordPermission property to ProfileResponseModel.cs primarily for sync so that we can condition client side if TDE user obtains elevated permissions

* PM-3275 - Fix AccountsControllerTests.cs

* PM-3275 - OrgUserInviteTokenable.cs - clarify TODO

* PM-3275 - AcceptOrgUserCommand.cs - Refactor token validation to use short circuiting to only run old token validation if new token validation fails.

* PM-3275 - OrgUserInviteTokenable.cs - (1) Add new static methods to centralize validation logic to avoid repetition (2) Add new token validation method so we can avoid having to pass in a full org user (and hitting the db to do so)

* PM-3275 - Realized that the old token validation was used in the PoliciesController.cs (existing user clicks invite link in email and goes to log in) and UserService.cs (user clicks invite link in email and registers for a new acct). Added tech debt item for cleaning up backwards compatibility in future.

* dotnet format

* PM-3275 - (1) AccountsController.cs - Update PostSetPasswordAsync SetPasswordRequestModel to allow null keys for the case where we have a TDE user who obtains elevated permissions - they already have a user public and user encrypted private key saved in the db. (2) AccountsControllerTests.cs - test PostSetPasswordAsync scenarios to ensure changes will work as expected.

* PM-3275 - PR review feedback - (1) set CurrentContext to private (2) Refactor GetProfile to use variables to improve clarity and simplify debugging.

* PM-3275 - SyncController.cs - PR Review Feedback - Set current context as private instead of protected.

* PM-3275 - CurrentContextExtensions.cs - PR Feedback - move parenthesis up from own line.

* PM-3275 - SetInitialMasterPasswordCommandTests.cs - Replace unnecessary variable

* PM-3275 - SetInitialMasterPasswordCommandTests.cs - PR Feedback - Add expected outcome statement to test name

* PM-3275 - Set Initial Password command and tests - PR Feedback changes - (1) Rename orgIdentifier --> OrgSsoIdentifier for clarity (2) Update SetInitialMasterPasswordAsync to not allow null orgSsoId with explicit message saying this vs letting null org trigger invalid organization (3) Add test to cover this new scenario.

* PM-3275 - SetInitialMasterPasswordCommand.cs - Move summary from implementation to interface to better respect standards and the fact that the interface is the more seen piece of code.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, rename AcceptOrgUserByTokenAsync -> AcceptOrgUserByEmailTokenAsync + replace generic name token with emailToken

* PM-3275 - OrganizationService.cs - Per PR feedback, remove dupe line

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, remove new lines in error messages for consistency.

* PM-3275 - SetInitialMasterPasswordCommand.cs - Per PR feedback, adjust formatting of constructor for improved readability.

* PM-3275 - CurrentContextExtensions.cs - Refactor AnyOrgUserHasManageResetPasswordPermission per PR feedback to remove unnecessary var.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, remove completed TODO

* PM-3275 - PoliciesController.cs - Per PR feedback, update GetByInvitedUser param to be guid instead of string.

* PM-3275 - OrgUserInviteTokenable.cs - per PR feedback, add tech debt item info.

* PM-3275 - AcceptOrgUserCommand.cs - Per PR feedback, use const purpose from tokenable instead of magic string.

* PM-3275 - Restore non duplicate line to fix tests

* PM-3275 - Per PR feedback, revert all sync controller changes as the ProfileResponseModel.organizations array has org objects which have permissions which have the ManageResetPassword permission.  So, I have the information that I need clientside already to determine if the user has the ManageResetPassword in any org.

* PM-3275 - PoliciesControllerTests.cs - Update imports as the PoliciesController was moved under the admin console team's domain.

* PM-3275 - Resolve issues from merge conflict resolutions to get solution building.

* PM-3275 / PM-4633 - PoliciesController.cs - use orgUserId to look up user instead of orgId. Oops.

* Fix user service tests

* Resolve merge conflict
  • Loading branch information
JaredSnider-Bitwarden authored Nov 2, 2023
1 parent cfe9812 commit ee61832
Show file tree
Hide file tree
Showing 31 changed files with 2,171 additions and 300 deletions.
12 changes: 8 additions & 4 deletions src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Data.Organizations.Policies;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
Expand All @@ -33,6 +34,7 @@ public class OrganizationUsersController : Controller
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IUpdateOrganizationUserGroupsCommand _updateOrganizationUserGroupsCommand;
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;

public OrganizationUsersController(
IOrganizationRepository organizationRepository,
Expand All @@ -45,7 +47,8 @@ public OrganizationUsersController(
ICurrentContext currentContext,
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand)
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
IAcceptOrgUserCommand acceptOrgUserCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -58,6 +61,7 @@ public OrganizationUsersController(
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_updateOrganizationUserGroupsCommand = updateOrganizationUserGroupsCommand;
_acceptOrgUserCommand = acceptOrgUserCommand;
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -199,7 +203,7 @@ public async Task AcceptInit(Guid orgId, Guid organizationUserId, [FromBody] Org
}

await _organizationService.InitPendingOrganization(user.Id, orgId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService);
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService);
}

Expand All @@ -221,7 +225,7 @@ public async Task Accept(Guid orgId, Guid organizationUserId, [FromBody] Organiz
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
}

await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService);
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);

if (useMasterPasswordPolicy)
{
Expand Down Expand Up @@ -332,7 +336,7 @@ await _organizationService.UpdateUserResetPasswordEnrollmentAsync(
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, user.Id);
if (orgUser.Status == OrganizationUserStatusType.Invited)
{
await _organizationService.AcceptUserAsync(orgId, user, _userService);
await _acceptOrgUserCommand.AcceptOrgUserByOrgIdAsync(orgId, user, _userService);
}
}

Expand Down
63 changes: 48 additions & 15 deletions src/Api/AdminConsole/Controllers/PoliciesController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
Expand All @@ -26,6 +28,7 @@ public class PoliciesController : Controller
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IDataProtector _organizationServiceDataProtector;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;

public PoliciesController(
IPolicyRepository policyRepository,
Expand All @@ -35,7 +38,8 @@ public PoliciesController(
IUserService userService,
ICurrentContext currentContext,
GlobalSettings globalSettings,
IDataProtectionProvider dataProtectionProvider)
IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
{
_policyRepository = policyRepository;
_policyService = policyService;
Expand All @@ -46,6 +50,8 @@ public PoliciesController(
_globalSettings = globalSettings;
_organizationServiceDataProtector = dataProtectionProvider.CreateProtector(
"OrganizationServiceDataProtector");

_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
}

[HttpGet("{type}")]
Expand Down Expand Up @@ -81,41 +87,46 @@ public async Task<ListResponseModel<PolicyResponseModel>> Get(string orgId)

[AllowAnonymous]
[HttpGet("token")]
public async Task<ListResponseModel<PolicyResponseModel>> GetByToken(string orgId, [FromQuery] string email,
[FromQuery] string token, [FromQuery] string organizationUserId)
public async Task<ListResponseModel<PolicyResponseModel>> GetByToken(Guid orgId, [FromQuery] string email,
[FromQuery] string token, [FromQuery] Guid organizationUserId)
{
var orgUserId = new Guid(organizationUserId);
var tokenValid = CoreHelpers.UserInviteTokenIsValid(_organizationServiceDataProtector, token,
email, orgUserId, _globalSettings);
// TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
_orgUserInviteTokenDataFactory, token, organizationUserId, email);

var tokenValid = newTokenValid || CoreHelpers.UserInviteTokenIsValid(
_organizationServiceDataProtector, token, email, organizationUserId, _globalSettings
);

if (!tokenValid)
{
throw new NotFoundException();
}

var orgIdGuid = new Guid(orgId);
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId);
if (orgUser == null || orgUser.OrganizationId != orgIdGuid)
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != orgId)
{
throw new NotFoundException();
}

var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid);
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgId);
var responses = policies.Where(p => p.Enabled).Select(p => new PolicyResponseModel(p));
return new ListResponseModel<PolicyResponseModel>(responses);
}

// TODO: PM-4097 - remove GetByInvitedUser once all clients are updated to use the GetMasterPasswordPolicy endpoint below
[Obsolete("Deprecated API", false)]
[AllowAnonymous]
[HttpGet("invited-user")]
public async Task<ListResponseModel<PolicyResponseModel>> GetByInvitedUser(string orgId, [FromQuery] string userId)
public async Task<ListResponseModel<PolicyResponseModel>> GetByInvitedUser(Guid orgId, [FromQuery] Guid userId)
{
var user = await _userService.GetUserByIdAsync(new Guid(userId));
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var orgIdGuid = new Guid(orgId);
var orgUsersByUserId = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgIdGuid);
var orgUser = orgUsersByUserId.SingleOrDefault(u => u.OrganizationId == orgId);
if (orgUser == null)
{
throw new NotFoundException();
Expand All @@ -125,11 +136,33 @@ public async Task<ListResponseModel<PolicyResponseModel>> GetByInvitedUser(strin
throw new UnauthorizedAccessException();
}

var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgIdGuid);
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgId);
var responses = policies.Where(p => p.Enabled).Select(p => new PolicyResponseModel(p));
return new ListResponseModel<PolicyResponseModel>(responses);
}

[HttpGet("master-password")]
public async Task<PolicyResponseModel> GetMasterPasswordPolicy(Guid orgId)
{
var userId = _userService.GetProperUserId(User).Value;

var orgUser = await _organizationUserRepository.GetByOrganizationAsync(orgId, userId);

if (orgUser == null)
{
throw new NotFoundException();
}

var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.MasterPassword);

if (policy == null || !policy.Enabled)
{
throw new NotFoundException();
}

return new PolicyResponseModel(policy);
}

[HttpPut("{type}")]
public async Task<PolicyResponseModel> Put(string orgId, int type, [FromBody] PolicyRequestModel model)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations;
#nullable enable

using System.ComponentModel.DataAnnotations;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities;
using Bit.Core.Enums;
Expand All @@ -15,8 +17,7 @@ public class SetPasswordRequestModel : IValidatableObject
public string Key { get; set; }
[StringLength(50)]
public string MasterPasswordHint { get; set; }
[Required]
public KeysRequestModel Keys { get; set; }
public KeysRequestModel? Keys { get; set; }
[Required]
public KdfType Kdf { get; set; }
[Required]
Expand All @@ -33,7 +34,7 @@ public User ToUser(User existingUser)
existingUser.KdfMemory = KdfMemory;
existingUser.KdfParallelism = KdfParallelism;
existingUser.Key = Key;
Keys.ToUser(existingUser);
Keys?.ToUser(existingUser);
return existingUser;
}

Expand Down
21 changes: 18 additions & 3 deletions src/Api/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Auth.Utilities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
Expand Down Expand Up @@ -47,6 +48,8 @@ public class AccountsController : Controller
private readonly ISendService _sendService;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;


public AccountsController(
GlobalSettings globalSettings,
Expand All @@ -61,7 +64,9 @@ public AccountsController(
ISendRepository sendRepository,
ISendService sendService,
ICaptchaValidationService captchaValidationService,
IPolicyService policyService)
IPolicyService policyService,
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand
)
{
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
Expand All @@ -76,6 +81,7 @@ public AccountsController(
_sendService = sendService;
_captchaValidationService = captchaValidationService;
_policyService = policyService;
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
}

#region DEPRECATED (Moved to Identity Service)
Expand Down Expand Up @@ -253,8 +259,12 @@ public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model)
throw new UnauthorizedAccessException();
}

var result = await _userService.SetPasswordAsync(model.ToUser(user), model.MasterPasswordHash, model.Key,
var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
model.ToUser(user),
model.MasterPasswordHash,
model.Key,
model.OrgIdentifier);

if (result.Succeeded)
{
return;
Expand Down Expand Up @@ -456,8 +466,13 @@ public async Task<ProfileResponseModel> GetProfile()
var providerUserOrganizationDetails =
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed);

var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);

var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user));
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg);
return response;
}

Expand Down
1 change: 1 addition & 0 deletions src/Api/Vault/Controllers/SyncController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public async Task<SyncResponseModel> Get([FromQuery] bool excludeDomains = false
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed);
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);

var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, hasEnabledOrgs);
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Bit.Core.Entities;

namespace Bit.Core.Auth.Models.Business.Tokenables;

public interface IOrgUserInviteTokenableFactory
{
OrgUserInviteTokenable CreateToken(OrganizationUser orgUser);
}
84 changes: 84 additions & 0 deletions src/Core/Auth/Models/Business/Tokenables/OrgUserInviteTokenable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Tokens;

namespace Bit.Core.Auth.Models.Business.Tokenables;

public class OrgUserInviteTokenable : ExpiringTokenable
{
// TODO: PM-4317 - Ideally this would be internal and only visible to the test project.
// but configuring that is out of scope for these changes.
public static TimeSpan GetTokenLifetime() => TimeSpan.FromDays(5);

public const string ClearTextPrefix = "BwOrgUserInviteToken_";

// Backwards compatibility Note:
// Previously, tokens were manually created in the OrganizationService using a data protector
// initialized with purpose: "OrganizationServiceDataProtector"
// So, we must continue to use the existing purpose to be able to decrypt tokens
// in emailed invites that have not yet been accepted.
public const string DataProtectorPurpose = "OrganizationServiceDataProtector";

public const string TokenIdentifier = "OrgUserInviteToken";

public string Identifier { get; set; } = TokenIdentifier;
public Guid OrgUserId { get; set; }
public string OrgUserEmail { get; set; }

[JsonConstructor]
public OrgUserInviteTokenable()
{
ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime());
}

public OrgUserInviteTokenable(OrganizationUser orgUser) : this()
{
OrgUserId = orgUser?.Id ?? default;
OrgUserEmail = orgUser?.Email;
}

public bool TokenIsValid(OrganizationUser orgUser)
{
if (OrgUserId == default || OrgUserEmail == default || orgUser == null)
{
return false;
}

return OrgUserId == orgUser.Id &&
OrgUserEmail.Equals(orgUser.Email, StringComparison.InvariantCultureIgnoreCase);
}

public bool TokenIsValid(Guid orgUserId, string orgUserEmail)
{
if (OrgUserId == default || OrgUserEmail == default || orgUserId == default || orgUserEmail == default)
{
return false;
}

return OrgUserId == orgUserId &&
OrgUserEmail.Equals(orgUserEmail, StringComparison.InvariantCultureIgnoreCase);
}

// Validates deserialized
protected override bool TokenIsValid() =>
Identifier == TokenIdentifier && OrgUserId != default && !string.IsNullOrWhiteSpace(OrgUserEmail);


public static bool ValidateOrgUserInviteStringToken(
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
string orgUserInviteToken, OrganizationUser orgUser)
{
return orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken)
&& decryptedToken.Valid
&& decryptedToken.TokenIsValid(orgUser);
}

public static bool ValidateOrgUserInviteStringToken(
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
string orgUserInviteToken, Guid orgUserId, string orgUserEmail)
{
return orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken)
&& decryptedToken.Valid
&& decryptedToken.TokenIsValid(orgUserId, orgUserEmail);
}
}
Loading

0 comments on commit ee61832

Please sign in to comment.