1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

[EC-388] Enforce organization policies when restoring user (#2152)

This commit is contained in:
Thomas Rittson 2022-08-03 07:09:22 +10:00 committed by GitHub
parent da3a3de7f2
commit ebdd30f5d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 12 deletions

View File

@ -193,7 +193,7 @@ namespace Bit.Scim.Controllers.v2
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked) if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
{ {
await _organizationService.RestoreUserAsync(orgUser, null); await _organizationService.RestoreUserAsync(orgUser, null, _userService);
} }
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked) else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
{ {
@ -229,7 +229,7 @@ namespace Bit.Scim.Controllers.v2
var active = activeProperty.GetBoolean(); var active = activeProperty.GetBoolean();
if (active && orgUser.Status == OrganizationUserStatusType.Revoked) if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
{ {
await _organizationService.RestoreUserAsync(orgUser, null); await _organizationService.RestoreUserAsync(orgUser, null, _userService);
operationHandled = true; operationHandled = true;
} }
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked) else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)

View File

@ -423,14 +423,14 @@ namespace Bit.Api.Controllers
[HttpPut("{id}/restore")] [HttpPut("{id}/restore")]
public async Task RestoreAsync(Guid orgId, Guid id) public async Task RestoreAsync(Guid orgId, Guid id)
{ {
await RestoreOrRevokeUserAsync(orgId, id, _organizationService.RestoreUserAsync); await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId, _userService));
} }
[HttpPatch("restore")] [HttpPatch("restore")]
[HttpPut("restore")] [HttpPut("restore")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{ {
return await RestoreOrRevokeUsersAsync(orgId, model, _organizationService.RestoreUsersAsync); return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _organizationService.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
} }
private async Task RestoreOrRevokeUserAsync( private async Task RestoreOrRevokeUserAsync(

View File

@ -61,8 +61,8 @@ namespace Bit.Core.Services
Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId); Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId);
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId, Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId); IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId); Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId); IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
} }
} }

View File

@ -2300,7 +2300,7 @@ namespace Bit.Core.Services
return result; return result;
} }
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId) public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService)
{ {
if (organizationUser.Status != OrganizationUserStatusType.Revoked) if (organizationUser.Status != OrganizationUserStatusType.Revoked)
{ {
@ -2318,6 +2318,8 @@ namespace Bit.Core.Services
throw new BadRequestException("Only owners can restore other owners."); throw new BadRequestException("Only owners can restore other owners.");
} }
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser); var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status); await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
@ -2326,7 +2328,7 @@ namespace Bit.Core.Services
} }
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId) IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
{ {
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds); var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId) var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
@ -2364,6 +2366,8 @@ namespace Bit.Core.Services
throw new BadRequestException("Only owners can restore other owners."); throw new BadRequestException("Only owners can restore other owners.");
} }
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser); var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status); await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
@ -2381,6 +2385,53 @@ namespace Bit.Core.Services
return result; return result;
} }
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, IUserService userService)
{
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
// The user will be subject to the same checks when they try to accept the invite
if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
{
return;
}
var userId = orgUser.UserId.Value;
// Enforce Single Organization Policy of organization user is being restored to
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
var singleOrgPoliciesApplyingToRevokedUsers = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(userId,
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
if (hasOtherOrgs && singleOrgPolicyApplies)
{
throw new BadRequestException("You cannot restore this user until " +
"they leave or remove all other organizations.");
}
// Enforce Single Organization Policy of other organizations user is a member of
var singleOrgPolicyCount = await _policyRepository.GetCountByTypeApplicableToUserIdAsync(userId,
PolicyType.SingleOrg);
if (singleOrgPolicyCount > 0)
{
throw new BadRequestException("You cannot restore this user because they are a member of " +
"another organization which forbids it");
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
var user = await _userRepository.GetByIdAsync(userId);
if (!await userService.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyRepository.GetManyByTypeApplicableToUserIdAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot restore this user until they enable " +
"two-step login on their user account.");
}
}
}
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser) static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
{ {
// Determine status to revert back to // Determine status to revert back to

View File

@ -25,7 +25,7 @@ LEFT JOIN
WHERE WHERE
( (
( (
OU.[Status] > 0 OU.[Status] != 0 -- OrgUsers who have accepted their invite and are linked to a UserId
AND OU.[UserId] = @UserId AND OU.[UserId] = @UserId
) )
OR ( OR (

View File

@ -1,7 +1,7 @@
CREATE PROCEDURE [dbo].[Policy_CountByTypeApplicableToUser] CREATE PROCEDURE [dbo].[Policy_CountByTypeApplicableToUser]
@UserId UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER,
@PolicyType TINYINT, @PolicyType TINYINT,
@MinimumStatus TINYINT @MinimumStatus SMALLINT
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON

View File

@ -1,7 +1,7 @@
CREATE PROCEDURE [dbo].[Policy_ReadByTypeApplicableToUser] CREATE PROCEDURE [dbo].[Policy_ReadByTypeApplicableToUser]
@UserId UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER,
@PolicyType TINYINT, @PolicyType TINYINT,
@MinimumStatus TINYINT @MinimumStatus SMALLINT
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON

View File

@ -0,0 +1,95 @@
-- 2022-06-08_00_DeactivatedUserStatus changed UserStatus from TINYINT to SMALLINT but these sprocs were missed
-- Policy_ReadByTypeApplicableToUser
IF OBJECT_ID('[dbo].[Policy_ReadByTypeApplicableToUser]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Policy_ReadByTypeApplicableToUser]
END
GO
CREATE PROCEDURE [dbo].[Policy_ReadByTypeApplicableToUser]
@UserId UNIQUEIDENTIFIER,
@PolicyType TINYINT,
@MinimumStatus SMALLINT
AS
BEGIN
SET NOCOUNT ON
SELECT *
FROM [dbo].[PolicyApplicableToUser](@UserId, @PolicyType, @MinimumStatus)
END
GO
-- Policy_CountByTypeApplicableToUser
IF OBJECT_ID('[dbo].[Policy_CountByTypeApplicableToUser]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[Policy_CountByTypeApplicableToUser]
END
GO
CREATE PROCEDURE [dbo].[Policy_CountByTypeApplicableToUser]
@UserId UNIQUEIDENTIFIER,
@PolicyType TINYINT,
@MinimumStatus SMALLINT
AS
BEGIN
SET NOCOUNT ON
SELECT COUNT(1)
FROM [dbo].[PolicyApplicableToUser](@UserId, @PolicyType, @MinimumStatus)
END
GO
-- We need to update this function because we now have OrganizationUserStatusTypes that are less than zero,
-- and Deactivated/Revoked users may or may not be associated with a UserId
IF OBJECT_ID('[dbo].[PolicyApplicableToUser]') IS NOT NULL
BEGIN
DROP FUNCTION [dbo].[PolicyApplicableToUser]
END
GO
CREATE FUNCTION [dbo].[PolicyApplicableToUser]
(
@UserId UNIQUEIDENTIFIER,
@PolicyType TINYINT,
@MinimumStatus SMALLINT
)
RETURNS TABLE
AS RETURN
SELECT
P.*
FROM
[dbo].[PolicyView] P
INNER JOIN
[dbo].[OrganizationUserView] OU ON P.[OrganizationId] = OU.[OrganizationId]
LEFT JOIN
(SELECT
PU.UserId,
PO.OrganizationId
FROM
[dbo].[ProviderUserView] PU
INNER JOIN
[ProviderOrganizationView] PO ON PO.[ProviderId] = PU.[ProviderId]) PUPO
ON PUPO.UserId = OU.UserId
AND PUPO.OrganizationId = P.OrganizationId
WHERE
(
(
OU.[Status] != 0 -- OrgUsers who have accepted their invite and are linked to a UserId
AND OU.[UserId] = @UserId
)
OR (
OU.[Status] = 0 -- 'Invited' OrgUsers are not linked to a UserId yet, so we have to look up their email
AND OU.[Email] IN (SELECT U.Email FROM [dbo].[UserView] U WHERE U.Id = @UserId)
)
)
AND P.[Type] = @PolicyType
AND P.[Enabled] = 1
AND OU.[Status] >= @MinimumStatus
AND OU.[Type] >= 2 -- Not an owner (0) or admin (1)
AND ( -- Can't manage policies
OU.[Permissions] IS NULL
OR COALESCE(JSON_VALUE(OU.[Permissions], '$.managePolicies'), 'false') = 'false'
)
AND PUPO.[UserId] IS NULL -- Not a provider
GO