Skip to content

Commit

Permalink
[SSO] New user provision flow (#945)
Browse files Browse the repository at this point in the history
* Initial commit of accept user during set password flow

* changed new org user from accepted to invited // moved another check to token accept function

* Revised some white space // Moved business logic to UserService

* Fixed UserServiceTest

* Removed some white-space

* Removed more white-space

* Final white-space issues
  • Loading branch information
vincentsalucci authored Oct 13, 2020
1 parent dfe5c57 commit 50cf16a
Show file tree
Hide file tree
Showing 7 changed files with 59 additions and 21 deletions.
2 changes: 1 addition & 1 deletion bitwarden_license/src/Sso/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ private async Task<User> AutoProvisionUserAsync(string provider, string provider
OrganizationId = orgId.Value,
UserId = user.Id,
Type = OrganizationUserType.User,
Status = OrganizationUserStatusType.Accepted
Status = OrganizationUserStatusType.Invited
};
await _organizationUserRepository.CreateAsync(orgUser);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class SetPasswordRequestModel
public KdfType Kdf { get; set; }
[Required]
public int KdfIterations { get; set; }
public string OrgIdentifier { get; set; }

public User ToUser(User existingUser)
{
Expand Down
1 change: 1 addition & 0 deletions src/Core/Services/IOrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Task<List<OrganizationUser>> InviteUserAsync(Guid organizationId, Guid? inviting
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService);
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public interface IUserService
Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword,
string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key, string orgIdentifier = null);
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
KdfType kdf, int kdfIterations);
Task<IdentityResult> UpdateKeyAsync(User user, string masterPassword, string key, string privateKey,
Expand Down
54 changes: 39 additions & 15 deletions src/Core/Services/Implementations/OrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,9 +1074,9 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
throw new BadRequestException("User invalid.");
}

if (orgUser.Status != OrganizationUserStatusType.Invited)
if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
{
throw new BadRequestException("Already accepted.");
throw new BadRequestException("Invalid token.");
}

if (string.IsNullOrWhiteSpace(orgUser.Email) ||
Expand All @@ -1085,6 +1085,42 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
throw new BadRequestException("User email does not match invite.");
}

var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
throw new BadRequestException("You are already part of this organization.");
}

return await AcceptUserAsync(orgUser, user, userService);
}

public async Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService)
{
var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier);
if (org == null)
{
throw new BadRequestException("Organization invalid.");
}

var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id);
if (orgUser == null)
{
throw new BadRequestException("User not found within organization.");
}

return await AcceptUserAsync(orgUser, user, userService);
}

private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, User user,
IUserService userService)
{
if (orgUser.Status != OrganizationUserStatusType.Invited)
{
throw new BadRequestException("Already accepted.");
}

if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin)
{
var org = await GetOrgById(orgUser.OrganizationId);
Expand All @@ -1099,18 +1135,6 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
}
}

var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
orgUser.OrganizationId, user.Email, true);
if (existingOrgUserCount > 0)
{
throw new BadRequestException("You are already part of this organization.");
}

if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings))
{
throw new BadRequestException("Invalid token.");
}

if (!await userService.TwoFactorIsEnabledAsync(user))
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);
Expand All @@ -1124,10 +1148,10 @@ public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, Use
orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
orgUser.Email = null;

await _organizationUserRepository.ReplaceAsync(orgUser);

// TODO: send notification emails to org admins and accepting user?

return orgUser;
}

Expand Down
15 changes: 12 additions & 3 deletions src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IReferenceEventService _referenceEventService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;

public UserService(
IUserRepository userRepository,
Expand All @@ -74,7 +75,8 @@ public UserService(
IPolicyRepository policyRepository,
IReferenceEventService referenceEventService,
CurrentContext currentContext,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IOrganizationService organizationService)
: base(
store,
optionsAccessor,
Expand Down Expand Up @@ -107,6 +109,7 @@ public UserService(
_referenceEventService = referenceEventService;
_currentContext = currentContext;
_globalSettings = globalSettings;
_organizationService = organizationService;
}

public Guid? GetProperUserId(ClaimsPrincipal principal)
Expand Down Expand Up @@ -579,7 +582,8 @@ public async Task<IdentityResult> ChangePasswordAsync(User user, string masterPa
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}

public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key)
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
string orgIdentifier = null)
{
if (user == null)
{
Expand All @@ -603,7 +607,12 @@ public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassw

await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);


if (!string.IsNullOrWhiteSpace(orgIdentifier))
{
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
}

return IdentityResult.Success;
}

Expand Down
5 changes: 4 additions & 1 deletion test/Core.Test/Services/UserServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class UserServiceTests
private readonly IReferenceEventService _referenceEventService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;

public UserServiceTests()
{
Expand Down Expand Up @@ -69,6 +70,7 @@ public UserServiceTests()
_referenceEventService = Substitute.For<IReferenceEventService>();
_currentContext = new CurrentContext();
_globalSettings = new GlobalSettings();
_organizationService = Substitute.For<IOrganizationService>();

_sut = new UserService(
_userRepository,
Expand All @@ -95,7 +97,8 @@ public UserServiceTests()
_policyRepository,
_referenceEventService,
_currentContext,
_globalSettings
_globalSettings,
_organizationService
);
}

Expand Down

0 comments on commit 50cf16a

Please sign in to comment.