Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only org policy #962

Merged
merged 23 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0720f5e
added OnlyOrg to PolicyType enum
addisonbeck Oct 6, 2020
118b997
blocked accepting new org invitations if OnlyOrg is relevant to the u…
addisonbeck Oct 6, 2020
33c8af8
blocked creating new orgs if already in an org with OnlyOrg enabled
addisonbeck Oct 6, 2020
e476ebd
created email alert for OnlyOrg policy
addisonbeck Oct 6, 2020
cb16690
removed users & sent alerts when appropriate for the OnlyOrg policy
addisonbeck Oct 6, 2020
b8faa4e
added method to noop mail service
addisonbeck Oct 7, 2020
28c2051
cleanup for OnlyOrg policy server logic
addisonbeck Oct 7, 2020
839b1bb
blocked confirming new org users if they have violated the OnlyOrg po…
addisonbeck Oct 8, 2020
e5e5e8a
added localization strings needed for the OnlyOrg policy
addisonbeck Oct 8, 2020
df7f2d2
allowed OnlyOrg policy configuration from the portal
addisonbeck Oct 8, 2020
3374c23
used correct localization key for onlyorg
addisonbeck Oct 8, 2020
3d709fd
formatting and messaging changes for OnlyOrg
addisonbeck Oct 10, 2020
eabd06e
formatting
addisonbeck Oct 10, 2020
de295de
messaging change
addisonbeck Oct 10, 2020
c5bcf17
code review changes for onlyorg
addisonbeck Oct 13, 2020
ac97c52
slimmed down a conditional
addisonbeck Oct 14, 2020
e6b2c9f
Merge branch 'master' into OnlyOrgPolicy
Oct 14, 2020
eafebd8
optimized getting many orgUser records from many userIds
addisonbeck Oct 14, 2020
f56a3fd
Merge branch 'OnlyOrgPolicy' of https://www.github.com/bitwarden/serv…
addisonbeck Oct 14, 2020
30b5b56
removed a test file
addisonbeck Oct 14, 2020
354d059
sql formatting
addisonbeck Oct 15, 2020
1965d49
weirdness
addisonbeck Oct 15, 2020
50bb35c
trying to resolve git diff formatting issues
addisonbeck Oct 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bitwarden_license/src/Portal/Models/PolicyEditModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ public Policy ToPolicy(Policy existingPolicy)
case PolicyType.PasswordGenerator:
existingPolicy.Data = JsonSerializer.Serialize(PasswordGeneratorDataModel, options);
break;
case PolicyType.OnlyOrg:
case PolicyType.TwoFactorAuthentication:
break;
default:
throw new ArgumentOutOfRangeException();
}
Expand Down
5 changes: 5 additions & 0 deletions bitwarden_license/src/Portal/Models/PolicyModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public PolicyModel(PolicyType policyType, bool enabled)
DescriptionKey = "PasswordGeneratorDescription";
break;

case PolicyType.OnlyOrg:
NameKey = "OnlyOrganization";
DescriptionKey = "OnlyOrganizationDescription";
break;

default:
throw new ArgumentOutOfRangeException();
}
Expand Down
11 changes: 11 additions & 0 deletions bitwarden_license/src/Portal/Views/Policies/Edit.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@
</div>
}

@if (Model.PolicyType == PolicyType.OnlyOrg)
{
<div class="callout callout-warning" role="alert">
<h3 class="callout-heading">
<i class="fa fa-warning" *ngIf="icon" aria-hidden="true"></i>
@i18nService.T("Warning")
</h3>
@i18nService.T("OnlyOrganizationPolicyWarning")
</div>
}

<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" asp-for="Enabled">
Expand Down
20 changes: 19 additions & 1 deletion src/Api/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.AspNetCore.Mvc;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Exceptions;
using Bit.Core.Services;
Expand All @@ -25,6 +26,7 @@ public class OrganizationsController : Controller
private readonly IPaymentService _paymentService;
private readonly CurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
private readonly IPolicyRepository _policyRepository;

public OrganizationsController(
IOrganizationRepository organizationRepository,
Expand All @@ -33,7 +35,8 @@ public OrganizationsController(
IUserService userService,
IPaymentService paymentService,
CurrentContext currentContext,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
IPolicyRepository policyRepository)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -42,6 +45,7 @@ public OrganizationsController(
_paymentService = paymentService;
_currentContext = currentContext;
_globalSettings = globalSettings;
_policyRepository = policyRepository;
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -156,6 +160,13 @@ public async Task<OrganizationResponseModel> Post([FromBody]OrganizationCreateRe
throw new Exception("Invalid plan selected.");
}

var policies = await _policyRepository.GetManyByUserIdAsync(user.Id);
if (policies.Any(policy => policy.Type == PolicyType.OnlyOrg))
{
throw new Exception("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}

var organizationSignup = model.ToOrganizationSignup(user);
var result = await _organizationService.SignUpAsync(organizationSignup);
return new OrganizationResponseModel(result.Item1);
Expand All @@ -177,6 +188,13 @@ public async Task<OrganizationResponseModel> PostLicense(OrganizationCreateLicen
throw new BadRequestException("Invalid license");
}

var policies = await _policyRepository.GetManyByUserIdAsync(user.Id);
if (policies.Any(policy => policy.Type == PolicyType.OnlyOrg))
{
throw new Exception("You may not create an organization. You belong to an organization " +
"which has a policy that prohibits you from being a member of any other organization.");
}

var result = await _organizationService.SignUpAsync(license, user, model.Key, model.CollectionName);
return new OrganizationResponseModel(result.Item1);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Core/Enums/PolicyType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public enum PolicyType : byte
{
TwoFactorAuthentication = 0,
MasterPassword = 1,
PasswordGenerator = 2
PasswordGenerator = 2,
OnlyOrg = 3,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ο»Ώ{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you are a part of another organization. The {{OrganizationName}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
</td>
</tr>
</table>
{{/FullHtmlLayout}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ο»Ώ{{#>BasicTextLayout}}
Your user account has been removed from the {{OrganizationName}} organization because you are a part of another
organization. The {{OrganizationName}} has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations, or join with a
new account.
{{/BasicTextLayout}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ο»Ώnamespace Bit.Core.Models.Mail
{
public class OrganizationUserRemovedForPolicyOnlyOrgViewModel : BaseMailModel
{
public string OrganizationName { get; set; }
}
}
1 change: 1 addition & 0 deletions src/Core/Repositories/IOrganizationUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync
Task UpdateGroupsAsync(Guid orgUserId, IEnumerable<Guid> groupIds);
Task CreateAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
Task ReplaceAsync(OrganizationUser obj, IEnumerable<SelectionReadOnly> collections);
Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds);
}
}
13 changes: 13 additions & 0 deletions src/Core/Repositories/SqlServer/OrganizationUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,5 +231,18 @@ public class OrganizationUserWithCollections : OrganizationUser
{
public DataTable Collections { get; set; }
}

public async Task<ICollection<OrganizationUser>> GetManyByManyUsersAsync(IEnumerable<Guid> userIds)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationUser>(
"[dbo].[OrganizationUser_ReadByUserIds]",
new { UserIds = userIds.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure);

return results.ToList();
}
}
}
}
9 changes: 9 additions & 0 deletions src/Core/Resources/SharedResources.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,13 @@
<data name="RedirectBehavior" xml:space="preserve">
<value>OIDC Redirect Behavior</value>
</data>
<data name="OnlyOrganization" xml:space="preserve">
<value>Only Organization</value>
</data>
<data name="OnlyOrganizationDescription" xml:space="preserve">
<value>Restrict users from being able to join any other organizations.</value>
</data>
<data name="OnlyOrganizationPolicyWarning" xml:space="preserve">
<value>Organization members who are already a part of another organization will be removed from this organization and will receive an email notifying them about the change.</value>
</data>
</root>
1 change: 1 addition & 0 deletions src/Core/Services/IMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ Task SendInvoiceUpcomingAsync(string email, decimal amount, DateTime dueDate, Li
Task SendLicenseExpiredAsync(IEnumerable<string> emails, string organizationName = null);
Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip);
Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip);
Task SendOrganizationUserRemovedForPolicyOnlyOrgEmailAsync(string organizationName, string email);
}
}
14 changes: 14 additions & 0 deletions src/Core/Services/Implementations/HandlebarsMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,20 @@ public async Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, st
await _mailDeliveryService.SendEmailAsync(message);
}

public async Task SendOrganizationUserRemovedForPolicyOnlyOrgEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
var model = new OrganizationUserRemovedForPolicyOnlyOrgViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyOnlyOrg", model);
message.Category = "OrganizationUserRemovedForPolicyOnlyOrg";
await _mailDeliveryService.SendEmailAsync(message);
}

private MailMessage CreateDefaultMessage(string subject, string toEmail)
{
return CreateDefaultMessage(subject, new List<string> { toEmail });
Expand Down
38 changes: 36 additions & 2 deletions src/Core/Services/Implementations/OrganizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,10 +1135,34 @@ private async Task<OrganizationUser> AcceptUserAsync(OrganizationUser orgUser, U
}
}

ICollection<Policy> orgPolicies = null;
ICollection<Policy> userPolicies = null;
async Task<bool> hasPolicyAsync(PolicyType policyType, bool useUserPolicies = false)
{
var policies = useUserPolicies ?
userPolicies = userPolicies ?? await _policyRepository.GetManyByUserIdAsync(user.Id) :
orgPolicies = orgPolicies ?? await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);

return policies.Any(p => p.Type == policyType && p.Enabled);
}
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (userOrgs.Any(ou => ou.OrganizationId != orgUser.OrganizationId && ou.Status != OrganizationUserStatusType.Invited))
{
if (await hasPolicyAsync(PolicyType.OnlyOrg))
{
throw new BadRequestException("You may not join this organization until you leave or remove " +
"all other organizations.");
}
if (await hasPolicyAsync(PolicyType.OnlyOrg, true))
{
throw new BadRequestException("You cannot join this organization because you are a member of " +
"an organization which forbids it");
}
}

if (!await userService.TwoFactorIsEnabledAsync(user))
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(orgUser.OrganizationId);
if (policies.Any(p => p.Type == PolicyType.TwoFactorAuthentication && p.Enabled))
if (await hasPolicyAsync(PolicyType.TwoFactorAuthentication))
{
throw new BadRequestException("You cannot join this organization until you enable " +
"two-step login on your user account.");
Expand Down Expand Up @@ -1185,6 +1209,16 @@ public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid o
throw new BadRequestException("User does not have two-step login enabled.");
}

var usingOnlyOrgPolicy = policies.Any(p => p.Type == PolicyType.OnlyOrg && p.Enabled);
if (usingOnlyOrgPolicy)
{
var userOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id);
if (userOrgs.Any(ou => ou.OrganizationId != organizationId && ou.Status != OrganizationUserStatusType.Invited))
{
throw new BadRequestException("User is a member of another organization.");
}
}

orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = key;
orgUser.Email = null;
Expand Down
51 changes: 35 additions & 16 deletions src/Core/Services/Implementations/PolicyService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
ο»Ώusing System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
Expand Down Expand Up @@ -53,27 +55,44 @@ public async Task SaveAsync(Policy policy, IUserService userService, IOrganizati
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
if (!currentPolicy?.Enabled ?? true)
{
if (currentPolicy.Type == Enums.PolicyType.TwoFactorAuthentication)
Organization organization = null;
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
policy.OrganizationId);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != Enums.OrganizationUserStatusType.Invited &&
ou.Type != Enums.OrganizationUserType.Owner && ou.UserId != savingUserId);
switch (currentPolicy.Type)
{
Organization organization = null;
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(
policy.OrganizationId);
foreach (var orgUser in orgUsers.Where(ou =>
ou.Status != Enums.OrganizationUserStatusType.Invited &&
ou.Type != Enums.OrganizationUserType.Owner))
{
if (orgUser.UserId != savingUserId && !await userService.TwoFactorIsEnabledAsync(orgUser))
case Enums.PolicyType.TwoFactorAuthentication:
foreach (var orgUser in removableOrgUsers)
{
if (organization == null)
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
{
organization = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
organization = organization ?? await _organizationRepository.GetByIdAsync(policy.OrganizationId);
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.Name, orgUser.Email);
}
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.Name, orgUser.Email);
}
}
break;
case Enums.PolicyType.OnlyOrg:
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId.Value));
foreach (var orgUser in removableOrgUsers)
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId && ou.Status != OrganizationUserStatusType.Invited))
{
organization = organization ?? await _organizationRepository.GetByIdAsync(policy.OrganizationId);
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyOnlyOrgEmailAsync(
organization.Name, orgUser.Email);
}
}
break;
default:
break;
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Services/NoopImplementations/NoopMailService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,10 @@ public Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string i
{
return Task.FromResult(0);
}

public Task SendOrganizationUserRemovedForPolicyOnlyOrgEmailAsync(string organizationName, string email)
{
return Task.FromResult(0);
}
}
}
18 changes: 18 additions & 0 deletions src/Sql/dbo/Stored Procedures/OrganizationUser_ReadByUserIds.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserIds]
@UserIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON

IF (SELECT COUNT(1) FROM @UserIds) < 1
BEGIN
RETURN(-1)
END

SELECT
*
FROM
[dbo].[OrganizationUserView]
WHERE
[UserId] IN (SELECT [Id] FROM @UserIds)
END
25 changes: 25 additions & 0 deletions util/Migrator/DbScripts/2020-10-14_00_OrgUserReadByUserIds.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
IF OBJECT_ID('[dbo].[OrganizationUser_ReadByUserIds]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_ReadByUserIds]
END
GO

CREATE PROCEDURE [dbo].[OrganizationUser_ReadByUserIds]
@UserIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON

IF (SELECT COUNT(1) FROM @UserIds) < 1
BEGIN
RETURN(-1)
END

SELECT
*
FROM
[dbo].[OrganizationUserView]
WHERE
[UserId] IN (SELECT [Id] FROM @UserIds)
END
cscharf marked this conversation as resolved.
Show resolved Hide resolved
GO