mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 09:32:48 -05:00
[AC-1751] AC Team code ownership moves: OrganizationUser (part 1) (#3487)
* Move OrganizationUser domain to AC Team ownership * Namespaces will be updated in a separate commit
This commit is contained in:
38
src/Core/AdminConsole/Entities/OrganizationUser.cs
Normal file
38
src/Core/AdminConsole/Entities/OrganizationUser.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class OrganizationUser : ITableObject<Guid>, IExternal
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[MaxLength(256)]
|
||||
public string Email { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool AccessAll { get; set; }
|
||||
[MaxLength(300)]
|
||||
public string ExternalId { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
public string Permissions { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public Permissions GetPermissions()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(Permissions) ? null
|
||||
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum OrganizationUserStatusType : short
|
||||
{
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
Revoked = -1,
|
||||
}
|
10
src/Core/AdminConsole/Enums/OrganizationUserType.cs
Normal file
10
src/Core/AdminConsole/Enums/OrganizationUserType.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum OrganizationUserType : byte
|
||||
{
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2,
|
||||
Manager = 3,
|
||||
Custom = 4,
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
public class ImportedOrganizationUser
|
||||
{
|
||||
public string Email { get; set; }
|
||||
public string ExternalId { get; set; }
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
public class OrganizationUserInvite
|
||||
{
|
||||
public IEnumerable<string> Emails { get; set; }
|
||||
public Enums.OrganizationUserType? Type { get; set; }
|
||||
public bool AccessAll { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public Permissions Permissions { get; set; }
|
||||
public IEnumerable<CollectionAccessSelection> Collections { get; set; }
|
||||
public IEnumerable<Guid> Groups { get; set; }
|
||||
|
||||
public OrganizationUserInvite() { }
|
||||
|
||||
public OrganizationUserInvite(OrganizationUserInviteData requestModel)
|
||||
{
|
||||
Emails = requestModel.Emails;
|
||||
Type = requestModel.Type;
|
||||
AccessAll = requestModel.AccessAll;
|
||||
AccessSecretsManager = requestModel.AccessSecretsManager;
|
||||
Collections = requestModel.Collections;
|
||||
Groups = requestModel.Groups;
|
||||
Permissions = requestModel.Permissions;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserInviteData
|
||||
{
|
||||
public IEnumerable<string> Emails { get; set; }
|
||||
public OrganizationUserType? Type { get; set; }
|
||||
public bool AccessAll { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public IEnumerable<CollectionAccessSelection> Collections { get; set; }
|
||||
public IEnumerable<Guid> Groups { get; set; }
|
||||
public Permissions Permissions { get; set; }
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
public bool UseKeyConnector { get; set; }
|
||||
public bool UseScim { get; set; }
|
||||
public bool UseGroups { get; set; }
|
||||
public bool UseDirectory { get; set; }
|
||||
public bool UseEvents { get; set; }
|
||||
public bool UseTotp { get; set; }
|
||||
public bool Use2fa { get; set; }
|
||||
public bool UseApi { get; set; }
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public bool SelfHost { get; set; }
|
||||
public bool UsersGetPremium { get; set; }
|
||||
public bool UseCustomPermissions { get; set; }
|
||||
public int? Seats { get; set; }
|
||||
public short? MaxCollections { get; set; }
|
||||
public short? MaxStorageGb { get; set; }
|
||||
public string Key { get; set; }
|
||||
public Enums.OrganizationUserStatusType Status { get; set; }
|
||||
public Enums.OrganizationUserType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public Enums.PlanType PlanType { get; set; }
|
||||
public string SsoExternalId { get; set; }
|
||||
public string Identifier { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { get; set; }
|
||||
public string FamilySponsorshipFriendlyName { get; set; }
|
||||
public string SsoConfig { get; set; }
|
||||
public DateTime? FamilySponsorshipLastSyncDate { get; set; }
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserPolicyDetails
|
||||
{
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
|
||||
public PolicyType PolicyType { get; set; }
|
||||
|
||||
public bool PolicyEnabled { get; set; }
|
||||
|
||||
public string PolicyData { get; set; }
|
||||
|
||||
public OrganizationUserType OrganizationUserType { get; set; }
|
||||
|
||||
public OrganizationUserStatusType OrganizationUserStatus { get; set; }
|
||||
|
||||
public string OrganizationUserPermissionsData { get; set; }
|
||||
|
||||
public bool IsProvider { get; set; }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserPublicKey
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserResetPasswordDetails
|
||||
{
|
||||
public OrganizationUserResetPasswordDetails(OrganizationUser orgUser, User user, Organization org)
|
||||
{
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(orgUser));
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (org == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(org));
|
||||
}
|
||||
|
||||
Kdf = user.Kdf;
|
||||
KdfIterations = user.KdfIterations;
|
||||
KdfMemory = user.KdfMemory;
|
||||
KdfParallelism = user.KdfParallelism;
|
||||
ResetPasswordKey = orgUser.ResetPasswordKey;
|
||||
EncryptedPrivateKey = org.PrivateKey;
|
||||
}
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string EncryptedPrivateKey { get; set; }
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser
|
||||
{
|
||||
private Dictionary<TwoFactorProviderType, TwoFactorProvider> _twoFactorProviders;
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string AvatarColor { get; set; }
|
||||
public string TwoFactorProviders { get; set; }
|
||||
public bool? Premium { get; set; }
|
||||
public OrganizationUserStatusType Status { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public bool AccessAll { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public string ExternalId { get; set; }
|
||||
public string SsoExternalId { get; set; }
|
||||
public string Permissions { get; set; }
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
public bool HasMasterPassword { get; set; }
|
||||
|
||||
public ICollection<Guid> Groups { get; set; } = new List<Guid>();
|
||||
public ICollection<CollectionAccessSelection> Collections { get; set; } = new List<CollectionAccessSelection>();
|
||||
|
||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TwoFactorProviders))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_twoFactorProviders == null)
|
||||
{
|
||||
_twoFactorProviders =
|
||||
JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
|
||||
TwoFactorProviders);
|
||||
}
|
||||
|
||||
return _twoFactorProviders;
|
||||
}
|
||||
catch (Newtonsoft.Json.JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid? GetUserId()
|
||||
{
|
||||
return UserId;
|
||||
}
|
||||
|
||||
public bool GetPremium()
|
||||
{
|
||||
return Premium.GetValueOrDefault(false);
|
||||
}
|
||||
|
||||
public Permissions GetPermissions()
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(Permissions) ? null
|
||||
: CoreHelpers.LoadClassFromJsonData<Permissions>(Permissions);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserWithCollections : OrganizationUser
|
||||
{
|
||||
public DataTable Collections { get; set; }
|
||||
}
|
40
src/Core/AdminConsole/Models/Data/Permissions.cs
Normal file
40
src/Core/AdminConsole/Models/Data/Permissions.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Core.Models.Data;
|
||||
|
||||
public class Permissions
|
||||
{
|
||||
public bool AccessEventLogs { get; set; }
|
||||
public bool AccessImportExport { get; set; }
|
||||
public bool AccessReports { get; set; }
|
||||
public bool CreateNewCollections { get; set; }
|
||||
public bool EditAnyCollection { get; set; }
|
||||
public bool DeleteAnyCollection { get; set; }
|
||||
public bool EditAssignedCollections { get; set; }
|
||||
public bool DeleteAssignedCollections { get; set; }
|
||||
public bool ManageGroups { get; set; }
|
||||
public bool ManagePolicies { get; set; }
|
||||
public bool ManageSso { get; set; }
|
||||
public bool ManageUsers { get; set; }
|
||||
public bool ManageResetPassword { get; set; }
|
||||
public bool ManageScim { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
|
||||
{
|
||||
(AccessEventLogs, "accesseventlogs"),
|
||||
(AccessImportExport, "accessimportexport"),
|
||||
(AccessReports, "accessreports"),
|
||||
(CreateNewCollections, "createnewcollections"),
|
||||
(EditAnyCollection, "editanycollection"),
|
||||
(DeleteAnyCollection, "deleteanycollection"),
|
||||
(EditAssignedCollections, "editassignedcollections"),
|
||||
(DeleteAssignedCollections, "deleteassignedcollections"),
|
||||
(ManageGroups, "managegroups"),
|
||||
(ManagePolicies, "managepolicies"),
|
||||
(ManageSso, "managesso"),
|
||||
(ManageUsers, "manageusers"),
|
||||
(ManageResetPassword, "manageresetpassword"),
|
||||
(ManageScim, "managescim"),
|
||||
};
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
{
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
|
||||
public AcceptOrgUserCommand(
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyService policyService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
{
|
||||
|
||||
// TODO: remove data protector when old token validation removed
|
||||
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
|
||||
_globalSettings = globalSettings;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_policyService = policyService;
|
||||
_mailService = mailService;
|
||||
_userRepository = userRepository;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
|
||||
IUserService userService)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User invalid.");
|
||||
}
|
||||
|
||||
// Tokens will have been created in two ways in the OrganizationService invite methods:
|
||||
// 1. New way - via OrgUserInviteTokenable
|
||||
// 2. Old way - via manual process using data protector initialized with purpose: "OrganizationServiceDataProtector"
|
||||
// For backwards compatibility, must check validity of both types of tokens and accept if either is valid
|
||||
|
||||
// TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete
|
||||
var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken(
|
||||
_orgUserInviteTokenDataFactory, emailToken, orgUser);
|
||||
|
||||
var tokenValid = newTokenValid ||
|
||||
CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id,
|
||||
_globalSettings);
|
||||
|
||||
if (!tokenValid)
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
|
||||
orgUser.OrganizationId, user.Email, true);
|
||||
if (existingOrgUserCount > 0)
|
||||
{
|
||||
if (orgUser.Status == OrganizationUserStatusType.Accepted)
|
||||
{
|
||||
throw new BadRequestException("Invitation already accepted. You will receive an email when your organization membership is confirmed.");
|
||||
}
|
||||
throw new BadRequestException("You are already part of this organization.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(orgUser.Email) ||
|
||||
!orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
|
||||
var organizationUser = await AcceptOrgUserAsync(orgUser, user, userService);
|
||||
|
||||
// Verify user email if they accept org invite via email link
|
||||
if (user.EmailVerified == false)
|
||||
{
|
||||
user.EmailVerified = true;
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
|
||||
return organizationUser;
|
||||
}
|
||||
|
||||
private bool ValidateOrgUserInviteToken(string orgUserInviteToken, OrganizationUser orgUser)
|
||||
{
|
||||
return _orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken)
|
||||
&& decryptedToken.Valid
|
||||
&& decryptedToken.TokenIsValid(orgUser);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptOrgUserByOrgSsoIdAsync(string orgSsoIdentifier, User user, IUserService userService)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization invalid.");
|
||||
}
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User not found within organization.");
|
||||
}
|
||||
|
||||
return await AcceptOrgUserAsync(orgUser, user, userService);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptOrgUserByOrgIdAsync(Guid organizationId, User user, IUserService userService)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
throw new BadRequestException("Organization invalid.");
|
||||
}
|
||||
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id);
|
||||
if (orgUser == null)
|
||||
{
|
||||
throw new BadRequestException("User not found within organization.");
|
||||
}
|
||||
|
||||
return await AcceptOrgUserAsync(orgUser, user, userService);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser, User user,
|
||||
IUserService userService)
|
||||
{
|
||||
if (orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Your organization access has been revoked.");
|
||||
}
|
||||
|
||||
if (orgUser.Status != OrganizationUserStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("Already accepted.");
|
||||
}
|
||||
|
||||
if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
|
||||
if (org.PlanType == PlanType.Free)
|
||||
{
|
||||
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(
|
||||
user.Id);
|
||||
if (adminCount > 0)
|
||||
{
|
||||
throw new BadRequestException("You can only be an admin of one free organization.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of organization user is trying to join
|
||||
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id);
|
||||
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
||||
var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.SingleOrg, OrganizationUserStatusType.Invited);
|
||||
|
||||
if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("You may not join this organization until you leave or remove all other organizations.");
|
||||
}
|
||||
|
||||
// Enforce Single Organization Policy of other organizations user is a member of
|
||||
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.SingleOrg);
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
throw new BadRequestException("You cannot join this organization because you are a member of another organization which forbids it");
|
||||
}
|
||||
|
||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||
if (!await userService.TwoFactorIsEnabledAsync(user))
|
||||
{
|
||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
||||
}
|
||||
}
|
||||
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted;
|
||||
orgUser.UserId = user.Id;
|
||||
orgUser.Email = null;
|
||||
|
||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||
|
||||
var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin);
|
||||
var adminEmails = admins.Select(a => a.Email).Distinct().ToList();
|
||||
|
||||
if (adminEmails.Count > 0)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId);
|
||||
await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IAcceptOrgUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves an OrganizationUser into the Accepted status and marks their email as verified.
|
||||
/// This method is used where the user has clicked the invitation link sent by email.
|
||||
/// </summary>
|
||||
/// <param name="emailToken">The token embedded in the email invitation link</param>
|
||||
/// <returns>The accepted OrganizationUser.</returns>
|
||||
Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, IUserService userService);
|
||||
Task<OrganizationUser> AcceptOrgUserByOrgSsoIdAsync(string orgIdentifier, User user, IUserService userService);
|
||||
Task<OrganizationUser> AcceptOrgUserByOrgIdAsync(Guid organizationId, User user, IUserService userService);
|
||||
Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser, User user, IUserService userService);
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IOrganizationUserRepository : IRepository<OrganizationUser, Guid>
|
||||
{
|
||||
Task<int> GetCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<int> GetCountByFreeOrganizationAdminUserAsync(Guid userId);
|
||||
Task<int> GetCountByOnlyOwnerAsync(Guid userId);
|
||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
|
||||
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||
Task<OrganizationUser> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
Task<Tuple<OrganizationUser, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||
Task<OrganizationUserUserDetails> GetDetailsByIdAsync(Guid id);
|
||||
Task<Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>>
|
||||
GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null);
|
||||
Task<OrganizationUserOrganizationDetails> GetDetailsByUserAsync(Guid userId, Guid organizationId,
|
||||
OrganizationUserStatusType? status = null);
|
||||
Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds);
|
||||
Task UpsertManyAsync(IEnumerable<OrganizationUser> organizationUsers);
|
||||
Task<Guid> CreateAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections);
|
||||
Task<ICollection<Guid>> CreateManyAsync(IEnumerable<OrganizationUser> organizationIdUsers);
|
||||
Task ReplaceAsync(OrganizationUser obj, IEnumerable<CollectionAccessSelection> collections);
|
||||
Task ReplaceManyAsync(IEnumerable<OrganizationUser> organizationUsers);
|
||||
Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);
|
||||
Task<ICollection<OrganizationUser>> GetManyAsync(IEnumerable<Guid> Ids);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> userIds);
|
||||
Task<OrganizationUser> GetByOrganizationEmailAsync(Guid organizationId, string email);
|
||||
Task<IEnumerable<OrganizationUserPublicKey>> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> Ids);
|
||||
Task<IEnumerable<OrganizationUserUserDetails>> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole);
|
||||
Task RevokeAsync(Guid id);
|
||||
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
|
||||
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
|
||||
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
}
|
Reference in New Issue
Block a user