mirror of
https://github.com/bitwarden/server
synced 2026-02-22 04:13:43 +00:00
feat(emergency-access): [PM-29585] Prevent New EA Invitations or Acceptance - Initial implementation
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
public class AutomaticUserConfirmationPolicyRequirementTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(OrganizationUserStatusType.Accepted)]
|
||||
[InlineData(OrganizationUserStatusType.Confirmed)]
|
||||
[InlineData(OrganizationUserStatusType.Revoked)]
|
||||
public void CannotGrantEmergencyAccess_WithActiveStatus_ReturnsTrue(OrganizationUserStatusType status)
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = status
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.True(sut.CannotGrantEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotGrantEmergencyAccess_WithInvitedStatus_ReturnsFalse()
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.False(sut.CannotGrantEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotGrantEmergencyAccess_WithNoPolicies_ReturnsFalse()
|
||||
{
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
|
||||
Assert.False(sut.CannotGrantEmergencyAccess());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(OrganizationUserStatusType.Accepted)]
|
||||
[InlineData(OrganizationUserStatusType.Confirmed)]
|
||||
[InlineData(OrganizationUserStatusType.Revoked)]
|
||||
public void CannotBeGrantedEmergencyAccess_WithActiveStatus_ReturnsTrue(OrganizationUserStatusType status)
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = status
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.True(sut.CannotBeGrantedEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotBeGrantedEmergencyAccess_WithInvitedStatus_ReturnsFalse()
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.False(sut.CannotBeGrantedEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotBeGrantedEmergencyAccess_WithNoPolicies_ReturnsFalse()
|
||||
{
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
|
||||
Assert.False(sut.CannotBeGrantedEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotGrantEmergencyAccess_WithMultiplePolicies_OneActive_ReturnsTrue()
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.True(sut.CannotGrantEmergencyAccess());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotBeGrantedEmergencyAccess_WithMultiplePolicies_OneActive_ReturnsTrue()
|
||||
{
|
||||
var policyDetails = new[]
|
||||
{
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
},
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new AutomaticUserConfirmationPolicyRequirement(policyDetails);
|
||||
|
||||
Assert.True(sut.CannotBeGrantedEmergencyAccess());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@@ -1512,4 +1516,317 @@ public class EmergencyAccessServiceTests
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.GetAttachmentDownloadAsync(emergencyAccess.Id, default, default, granteeUser));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
public async Task InviteAsync_FeatureFlagEnabled_UserInAutoConfirmOrg_ThrowsBadRequest(
|
||||
OrganizationUserStatusType status,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
string email,
|
||||
int waitTime)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with an active status that blocks emergency access
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = status
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(invitingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.View, waitTime));
|
||||
|
||||
Assert.Contains("You cannot invite emergency contacts because you are a member of an organization that uses Automatic User Confirmation.", exception.Message);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.DidNotReceiveWithAnyArgs().CreateAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InviteAsync_FeatureFlagEnabled_UserNotInAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
string email,
|
||||
int waitTime)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with no policies (user is not in auto-confirm org)
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(invitingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
var result = await sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.View, waitTime);
|
||||
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1).CreateAsync(Arg.Any<EmergencyAccess>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InviteAsync_FeatureFlagDisabled_UserInAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
string email,
|
||||
int waitTime)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.View, waitTime);
|
||||
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1).CreateAsync(Arg.Any<EmergencyAccess>());
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InviteAsync_FeatureFlagEnabled_UserInvitedToAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
string email,
|
||||
int waitTime)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(invitingUser).Returns(true);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with Invited status (should not block emergency access)
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(invitingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
var result = await sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.View, waitTime);
|
||||
|
||||
Assert.NotNull(result);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1).CreateAsync(Arg.Any<EmergencyAccess>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
public async Task AcceptUserAsync_FeatureFlagEnabled_UserInAutoConfirmOrg_ThrowsBadRequest(
|
||||
OrganizationUserStatusType status,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
emergencyAccess.Email = acceptingUser.Email;
|
||||
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(emergencyAccess);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with an active status that blocks emergency access
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = status
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(acceptingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>()));
|
||||
|
||||
Assert.Contains("You cannot accept emergency access invitations because you are a member of an organization that uses Automatic User Confirmation.", exception.Message);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_FeatureFlagEnabled_UserNotInAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
emergencyAccess.Email = acceptingUser.Email;
|
||||
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(emergencyAccess);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(invitingUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with no policies (user is not in auto-confirm org)
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement([]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(acceptingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_FeatureFlagDisabled_UserInAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
emergencyAccess.Email = acceptingUser.Email;
|
||||
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(emergencyAccess);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(invitingUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(false);
|
||||
|
||||
await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AcceptUserAsync_FeatureFlagEnabled_UserInvitedToAutoConfirmOrg_Succeeds(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
emergencyAccess.Email = acceptingUser.Email;
|
||||
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(emergencyAccess);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(invitingUser);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.TryUnprotect(token, out Arg.Any<EmergencyAccessInviteTokenable>())
|
||||
.Returns(callInfo =>
|
||||
{
|
||||
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1);
|
||||
return true;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
// Create a real requirement with Invited status (should not block emergency access)
|
||||
var requirement = new AutomaticUserConfirmationPolicyRequirement(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
PolicyType = PolicyType.AutomaticUserConfirmation,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Invited
|
||||
}
|
||||
]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(acceptingUser.Id)
|
||||
.Returns(requirement);
|
||||
|
||||
await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency<IUserService>());
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user