1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-24 17:17:40 +01:00

organization user apis, hardening, completeness

This commit is contained in:
Kyle Spearrin 2017-03-23 00:17:34 -04:00
parent eaeb18a46b
commit b7254519f0
9 changed files with 172 additions and 32 deletions

View File

@ -192,7 +192,7 @@ namespace Bit.Api.Controllers
if(userId.HasValue) if(userId.HasValue)
{ {
var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value); var date = await _userService.GetAccountRevisionDateByIdAsync(userId.Value);
revisionDate = Core.Utilities.CoreHelpers.EpocMilliseconds(date); revisionDate = Core.Utilities.CoreHelpers.ToEpocMilliseconds(date);
} }
return revisionDate; return revisionDate;

View File

@ -57,11 +57,19 @@ namespace Bit.Api.Controllers
[HttpPost("invite")] [HttpPost("invite")]
public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model) public async Task Invite(string orgId, [FromBody]OrganizationUserInviteRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var userId = _userService.GetProperUserId(User);
var result = await _organizationService.InviteUserAsync(new Guid(orgId), model.Email, model.Type, var result = await _organizationService.InviteUserAsync(new Guid(orgId), userId.Value, model.Email, model.Type,
model.Subvaults?.Select(s => s.ToSubvaultUser())); model.Subvaults?.Select(s => s.ToSubvaultUser()));
} }
[HttpPut("{id}/reinvite")]
[HttpPost("{id}/reinvite")]
public async Task Reinvite(string orgId, string id)
{
var userId = _userService.GetProperUserId(User);
await _organizationService.ResendInviteAsync(new Guid(orgId), userId.Value, new Guid(id));
}
[HttpPut("{id}/accept")] [HttpPut("{id}/accept")]
[HttpPost("{id}/accept")] [HttpPost("{id}/accept")]
public async Task Accept(string orgId, string id, [FromBody]OrganizationUserAcceptRequestModel model) public async Task Accept(string orgId, string id, [FromBody]OrganizationUserAcceptRequestModel model)
@ -74,12 +82,13 @@ namespace Bit.Api.Controllers
[HttpPost("{id}/confirm")] [HttpPost("{id}/confirm")]
public async Task Confirm(string orgId, string id, [FromBody]OrganizationUserConfirmRequestModel model) public async Task Confirm(string orgId, string id, [FromBody]OrganizationUserConfirmRequestModel model)
{ {
var result = await _organizationService.ConfirmUserAsync(new Guid(id), model.Key); var userId = _userService.GetProperUserId(User);
var result = await _organizationService.ConfirmUserAsync(new Guid(orgId), new Guid(id), model.Key, userId.Value);
} }
[HttpPut("{id}")] [HttpPut("{id}")]
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task Put(string id, [FromBody]OrganizationUserUpdateRequestModel model) public async Task Put(string orgId, string id, [FromBody]OrganizationUserUpdateRequestModel model)
{ {
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id)); var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
if(organizationUser == null) if(organizationUser == null)
@ -87,7 +96,8 @@ namespace Bit.Api.Controllers
throw new NotFoundException(); throw new NotFoundException();
} }
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), var userId = _userService.GetProperUserId(User);
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
model.Subvaults?.Select(s => s.ToSubvaultUser())); model.Subvaults?.Select(s => s.ToSubvaultUser()));
} }
@ -95,14 +105,8 @@ namespace Bit.Api.Controllers
[HttpPost("{id}/delete")] [HttpPost("{id}/delete")]
public async Task Delete(string orgId, string id) public async Task Delete(string orgId, string id)
{ {
var organization = await _organizationRepository.GetByIdAsync(new Guid(id), var userId = _userService.GetProperUserId(User);
_userService.GetProperUserId(User).Value); await _organizationService.DeleteUserAsync(new Guid(orgId), new Guid(id), userId.Value);
if(organization == null)
{
throw new NotFoundException();
}
await _organizationRepository.DeleteAsync(organization);
} }
} }
} }

View File

@ -10,5 +10,6 @@ namespace Bit.Core.Services
Task SendChangeEmailEmailAsync(string newEmailAddress, string token); Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendNoMasterPasswordHintEmailAsync(string email); Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint); Task SendMasterPasswordHintEmailAsync(string email, string hint);
Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token);
} }
} }

View File

@ -9,10 +9,12 @@ namespace Bit.Core.Services
public interface IOrganizationService public interface IOrganizationService
{ {
Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup); Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup organizationSignup);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, string email, Enums.OrganizationUserType type, Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
IEnumerable<SubvaultUser> subvaults); Enums.OrganizationUserType type, IEnumerable<SubvaultUser> subvaults);
Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token); Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationUserId, string key); Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
Task SaveUserAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults); Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SubvaultUser> subvaults);
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId);
} }
} }

View File

@ -23,6 +23,7 @@ namespace Bit.Core.Services
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash); Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider); Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider);
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
Task<string> GenerateUserTokenAsync(User user, string tokenProvider, string purpose);
Task<IdentityResult> DeleteAsync(User user); Task<IdentityResult> DeleteAsync(User user);
} }
} }

View File

@ -1,4 +1,5 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using Bit.Core.Models.Table; using Bit.Core.Models.Table;
namespace Bit.Core.Services namespace Bit.Core.Services
@ -25,6 +26,11 @@ namespace Bit.Core.Services
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token)
{
return Task.FromResult(0);
}
public Task SendWelcomeEmailAsync(User user) public Task SendWelcomeEmailAsync(User user)
{ {
return Task.FromResult(0); return Task.FromResult(0);

View File

@ -7,6 +7,7 @@ using Bit.Core.Models.Table;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.DataProtection;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -17,19 +18,25 @@ namespace Bit.Core.Services
private readonly ISubvaultRepository _subvaultRepository; private readonly ISubvaultRepository _subvaultRepository;
private readonly ISubvaultUserRepository _subvaultUserRepository; private readonly ISubvaultUserRepository _subvaultUserRepository;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IDataProtector _dataProtector;
private readonly IMailService _mailService;
public OrganizationService( public OrganizationService(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
ISubvaultRepository subvaultRepository, ISubvaultRepository subvaultRepository,
ISubvaultUserRepository subvaultUserRepository, ISubvaultUserRepository subvaultUserRepository,
IUserRepository userRepository) IUserRepository userRepository,
IDataProtectionProvider dataProtectionProvider,
IMailService mailService)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_subvaultRepository = subvaultRepository; _subvaultRepository = subvaultRepository;
_subvaultUserRepository = subvaultUserRepository; _subvaultUserRepository = subvaultUserRepository;
_userRepository = userRepository; _userRepository = userRepository;
_dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector");
_mailService = mailService;
} }
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup) public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup)
@ -90,9 +97,18 @@ namespace Bit.Core.Services
} }
} }
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, string email, Enums.OrganizationUserType type, public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid invitingUserId, string email,
IEnumerable<SubvaultUser> subvaults) Enums.OrganizationUserType type, IEnumerable<SubvaultUser> subvaults)
{ {
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, invitingUserId)))
{
throw new BadRequestException("Cannot invite users.");
}
// TODO: make sure user is not already invited
// TODO: validate subvaults?
var orgUser = new OrganizationUser var orgUser = new OrganizationUser
{ {
OrganizationId = organizationId, OrganizationId = organizationId,
@ -107,21 +123,70 @@ namespace Bit.Core.Services
await _organizationUserRepository.CreateAsync(orgUser); await _organizationUserRepository.CreateAsync(orgUser);
await SaveUserSubvaultsAsync(orgUser, subvaults, true); await SaveUserSubvaultsAsync(orgUser, subvaults, true);
await SendInviteAsync(organizationId, email);
// TODO: send email
return orgUser; return orgUser;
} }
public async Task ResendInviteAsync(Guid organizationId, Guid invitingUserId, Guid organizationUserId)
{
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, invitingUserId)))
{
throw new BadRequestException("Cannot invite users.");
}
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if(orgUser == null || orgUser.OrganizationId != organizationId ||
orgUser.Status == Enums.OrganizationUserStatusType.Invited)
{
throw new BadRequestException("User invalid.");
}
await SendInviteAsync(organizationId, orgUser.Email);
}
private async Task SendInviteAsync(Guid organizationId, string email)
{
var token = _dataProtector.Protect(
$"OrganizationInvite {organizationId} {email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
await _mailService.SendOrganizationInviteEmailAsync("Organization Name", email, token);
}
public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token) public async Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token)
{ {
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if(orgUser.Email != user.Email) if(orgUser == null || orgUser.Email != user.Email)
{ {
throw new BadRequestException("User invalid."); throw new BadRequestException("User invalid.");
} }
// TODO: validate token if(orgUser.Status != Enums.OrganizationUserStatusType.Invited)
{
throw new BadRequestException("Already accepted.");
}
var tokenValidationFailed = true;
try
{
var unprotectedData = _dataProtector.Unprotect(token);
var dataParts = unprotectedData.Split(' ');
if(dataParts.Length == 4 && dataParts[0] == "OrganizationInvite" &&
new Guid(dataParts[1]) == orgUser.OrganizationId && dataParts[2] == user.Email)
{
var creationTime = CoreHelpers.FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
tokenValidationFailed = creationTime.AddDays(5) < DateTime.UtcNow;
}
}
catch
{
tokenValidationFailed = true;
}
if(tokenValidationFailed)
{
throw new BadRequestException("Invalid token.");
}
orgUser.Status = Enums.OrganizationUserStatusType.Accepted; orgUser.Status = Enums.OrganizationUserStatusType.Accepted;
orgUser.UserId = orgUser.Id; orgUser.UserId = orgUser.Id;
@ -133,12 +198,19 @@ namespace Bit.Core.Services
return orgUser; return orgUser;
} }
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationUserId, string key) public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId)
{ {
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); if(!(await OrganizationUserHasAdminRightsAsync(organizationId, confirmingUserId)))
if(orgUser.Status != Enums.OrganizationUserStatusType.Accepted)
{ {
throw new BadRequestException("User not accepted."); throw new BadRequestException("Cannot confirm users.");
}
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if(orgUser == null || orgUser.Status != Enums.OrganizationUserStatusType.Accepted ||
orgUser.OrganizationId != organizationId)
{
throw new BadRequestException("User not valid.");
} }
orgUser.Status = Enums.OrganizationUserStatusType.Confirmed; orgUser.Status = Enums.OrganizationUserStatusType.Confirmed;
@ -151,17 +223,52 @@ namespace Bit.Core.Services
return orgUser; return orgUser;
} }
public async Task SaveUserAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults) public async Task SaveUserAsync(OrganizationUser user, Guid savingUserId, IEnumerable<SubvaultUser> subvaults)
{ {
if(user.Id.Equals(default(Guid))) if(user.Id.Equals(default(Guid)))
{ {
throw new BadRequestException("Invite the user first."); throw new BadRequestException("Invite the user first.");
} }
if(!(await OrganizationUserHasAdminRightsAsync(user.OrganizationId, savingUserId)))
{
throw new BadRequestException("Cannot update users.");
}
// TODO: validate subvaults?
await _organizationUserRepository.ReplaceAsync(user); await _organizationUserRepository.ReplaceAsync(user);
await SaveUserSubvaultsAsync(user, subvaults, false); await SaveUserSubvaultsAsync(user, subvaults, false);
} }
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid deletingUserId)
{
if(!(await OrganizationUserHasAdminRightsAsync(organizationId, deletingUserId)))
{
throw new BadRequestException("Cannot delete users.");
}
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if(orgUser == null || orgUser.OrganizationId != organizationId)
{
throw new BadRequestException("User not valid.");
}
await _organizationUserRepository.DeleteAsync(orgUser);
}
private async Task<bool> OrganizationUserHasAdminRightsAsync(Guid organizationId, Guid userId)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
if(orgUser == null)
{
return false;
}
return orgUser.Status == Enums.OrganizationUserStatusType.Confirmed &&
orgUser.Type != Enums.OrganizationUserType.User;
}
private async Task SaveUserSubvaultsAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults, bool newUser) private async Task SaveUserSubvaultsAsync(OrganizationUser user, IEnumerable<SubvaultUser> subvaults, bool newUser)
{ {
if(subvaults == null) if(subvaults == null)

View File

@ -14,6 +14,7 @@ namespace Bit.Core.Services
private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e"; private const string ChangeEmailTemplateId = "ec2c1471-8292-4f17-b6b6-8223d514f86e";
private const string NoMasterPasswordHintTemplateId = "136eb299-e102-495a-88bd-f96736eea159"; private const string NoMasterPasswordHintTemplateId = "136eb299-e102-495a-88bd-f96736eea159";
private const string MasterPasswordHintTemplateId = "be77cfde-95dd-4cb9-b5e0-8286b53885f1"; private const string MasterPasswordHintTemplateId = "be77cfde-95dd-4cb9-b5e0-8286b53885f1";
private const string OrganizationInviteTemplateId = "1eff5512-e36c-49a8-b9e2-2b215d6bbced";
private const string AdministrativeCategoryName = "Administrative"; private const string AdministrativeCategoryName = "Administrative";
private const string MarketingCategoryName = "Marketing"; private const string MarketingCategoryName = "Marketing";
@ -87,6 +88,19 @@ namespace Bit.Core.Services
await _client.SendEmailAsync(message); await _client.SendEmailAsync(message);
} }
public async Task SendOrganizationInviteEmailAsync(string organizationName, string email, string token)
{
var message = CreateDefaultMessage(OrganizationInviteTemplateId);
message.Subject = $"Join {organizationName}";
message.AddTo(new EmailAddress(email));
message.AddSubstitution("{{organizationName}}", organizationName);
message.AddSubstitution("{{token}}", token);
message.AddCategories(new List<string> { AdministrativeCategoryName, "Organization Invite" });
await _client.SendEmailAsync(message);
}
private SendGridMessage CreateDefaultMessage(string templateId) private SendGridMessage CreateDefaultMessage(string templateId)
{ {
var message = new SendGridMessage var message = new SendGridMessage

View File

@ -84,9 +84,14 @@ namespace Bit.Core.Utilities
return cert; return cert;
} }
public static long EpocMilliseconds(DateTime date) public static long ToEpocMilliseconds(DateTime date)
{ {
return (long)Math.Round((date - _epoc).TotalMilliseconds, 0); return (long)Math.Round((date - _epoc).TotalMilliseconds, 0);
} }
public static DateTime FromEpocMilliseconds(long milliseconds)
{
return _epoc.AddMilliseconds(milliseconds);
}
} }
} }