mirror of
https://github.com/bitwarden/server
synced 2025-12-31 23:53:17 +00:00
Merge branch 'main' into billing/PM-24964/msp-unable-verfy-bank-account
This commit is contained in:
@@ -2,10 +2,12 @@
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
@@ -29,6 +31,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@@ -293,4 +296,40 @@ public class OrganizationsControllerTests : IDisposable
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task PutCollectionManagement_ValidRequest_Success(
|
||||
Organization organization,
|
||||
OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
// Arrange
|
||||
_currentContext.OrganizationOwner(organization.Id).Returns(true);
|
||||
|
||||
var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
|
||||
_pricingClient.GetPlan(Arg.Any<PlanType>()).Returns(plan);
|
||||
|
||||
_organizationService
|
||||
.UpdateCollectionManagementSettingsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<OrganizationCollectionManagementSettings>(s =>
|
||||
s.LimitCollectionCreation == model.LimitCollectionCreation &&
|
||||
s.LimitCollectionDeletion == model.LimitCollectionDeletion &&
|
||||
s.LimitItemDeletion == model.LimitItemDeletion &&
|
||||
s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems))
|
||||
.Returns(organization);
|
||||
|
||||
// Act
|
||||
await _sut.PutCollectionManagement(organization.Id, model);
|
||||
|
||||
// Assert
|
||||
await _organizationService
|
||||
.Received(1)
|
||||
.UpdateCollectionManagementSettingsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<OrganizationCollectionManagementSettings>(s =>
|
||||
s.LimitCollectionCreation == model.LimitCollectionCreation &&
|
||||
s.LimitCollectionDeletion == model.LimitCollectionDeletion &&
|
||||
s.LimitItemDeletion == model.LimitItemDeletion &&
|
||||
s.AllowAdminAccessToAllCollectionItems == model.AllowAdminAccessToAllCollectionItems));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
@@ -126,7 +127,7 @@ public class FreshdeskControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_in_BadRequest(
|
||||
public async Task PostWebhookOnyxAi_invalid_onyx_response_results_is_logged(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
@@ -150,8 +151,18 @@ public class FreshdeskControllerTests
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<BadRequestObjectResult>(response);
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
|
||||
var statusCodeResult = Assert.IsAssignableFrom<StatusCodeResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode);
|
||||
|
||||
var _logger = sutProvider.GetDependency<ILogger<FreshdeskController>>();
|
||||
|
||||
// workaround because _logger.Received(1).LogWarning(...) does not work
|
||||
_logger.ReceivedCalls().Any(c => c.GetMethodInfo().Name == "Log" && c.GetArguments()[1].ToString().Contains("Error getting answer from Onyx AI"));
|
||||
|
||||
// sent call to Onyx API - but we got an error response
|
||||
_ = mockOnyxHttpMessageHandler.Received(1).Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
// did not call freshdesk to add a note since onyx failed
|
||||
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -174,10 +185,9 @@ public class FreshdeskControllerTests
|
||||
.Returns(mockFreshdeskAddNoteResponse);
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
onyxResponse.ErrorMsg = string.Empty;
|
||||
onyxResponse.ErrorMsg = "string.Empty";
|
||||
var mockOnyxResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent(JsonSerializer.Serialize(onyxResponse))
|
||||
@@ -195,6 +205,37 @@ public class FreshdeskControllerTests
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(WebhookKey)]
|
||||
public async Task PostWebhookOnyxAi_ticket_description_is_empty_return_success(
|
||||
string freshdeskWebhookKey, FreshdeskOnyxAiWebhookModel model,
|
||||
SutProvider<FreshdeskController> sutProvider)
|
||||
{
|
||||
var billingSettings = sutProvider.GetDependency<IOptions<BillingSettings>>().Value;
|
||||
billingSettings.FreshDesk.WebhookKey.Returns(freshdeskWebhookKey);
|
||||
billingSettings.Onyx.BaseUrl.Returns("http://simulate-onyx-api.com/api");
|
||||
|
||||
model.TicketDescriptionText = " "; // empty description
|
||||
|
||||
// mocking freshdesk api add note request (POST)
|
||||
var mockFreshdeskHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var freshdeskHttpClient = new HttpClient(mockFreshdeskHttpMessageHandler);
|
||||
|
||||
// mocking Onyx api response given a ticket description
|
||||
var mockOnyxHttpMessageHandler = Substitute.ForPartsOf<MockHttpMessageHandler>();
|
||||
var onyxHttpClient = new HttpClient(mockOnyxHttpMessageHandler);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("FreshdeskApi").Returns(freshdeskHttpClient);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().CreateClient("OnyxApi").Returns(onyxHttpClient);
|
||||
|
||||
var response = await sutProvider.Sut.PostWebhookOnyxAi(freshdeskWebhookKey, model);
|
||||
|
||||
var result = Assert.IsAssignableFrom<OkResult>(response);
|
||||
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
|
||||
_ = mockFreshdeskHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
_ = mockOnyxHttpMessageHandler.DidNotReceive().Send(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
public class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
@@ -27,7 +28,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -42,8 +42,6 @@ public class OrganizationServiceTests
|
||||
{
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
|
||||
InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData]
|
||||
@@ -1229,6 +1227,109 @@ public class OrganizationServiceTests
|
||||
.GetByIdentifierAsync(Arg.Is<string>(id => id == organization.Identifier));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, true, false, true)]
|
||||
[BitAutoData(true, false, true, false)]
|
||||
public async Task UpdateCollectionManagementSettingsAsync_WhenSettingsChanged_LogsSpecificEvents(
|
||||
bool newLimitCollectionCreation,
|
||||
bool newLimitCollectionDeletion,
|
||||
bool newLimitItemDeletion,
|
||||
bool newAllowAdminAccessToAllCollectionItems,
|
||||
Organization existingOrganization, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
existingOrganization.LimitCollectionCreation = false;
|
||||
existingOrganization.LimitCollectionDeletion = false;
|
||||
existingOrganization.LimitItemDeletion = false;
|
||||
existingOrganization.AllowAdminAccessToAllCollectionItems = false;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(existingOrganization.Id)
|
||||
.Returns(existingOrganization);
|
||||
|
||||
var settings = new OrganizationCollectionManagementSettings
|
||||
{
|
||||
LimitCollectionCreation = newLimitCollectionCreation,
|
||||
LimitCollectionDeletion = newLimitCollectionDeletion,
|
||||
LimitItemDeletion = newLimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = newAllowAdminAccessToAllCollectionItems
|
||||
};
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateCollectionManagementSettingsAsync(existingOrganization.Id, settings);
|
||||
|
||||
// Assert
|
||||
var eventService = sutProvider.GetDependency<IEventService>();
|
||||
if (newLimitCollectionCreation)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled));
|
||||
}
|
||||
|
||||
if (newLimitCollectionDeletion)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled));
|
||||
}
|
||||
|
||||
if (newLimitItemDeletion)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_LimitItemDeletionEnabled));
|
||||
}
|
||||
|
||||
if (newAllowAdminAccessToAllCollectionItems)
|
||||
{
|
||||
await eventService.Received(1).LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
await eventService.DidNotReceive().LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(org => org.Id == existingOrganization.Id),
|
||||
Arg.Is<EventType>(e => e == EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateCollectionManagementSettingsAsync_WhenOrganizationNotFound_ThrowsNotFoundException(
|
||||
Guid organizationId, OrganizationCollectionManagementSettings settings, SutProvider<OrganizationService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns((Organization)null);
|
||||
|
||||
// Act/Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateCollectionManagementSettingsAsync(organizationId, settings));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.Received(1)
|
||||
.GetByIdAsync(organizationId);
|
||||
}
|
||||
|
||||
// Must set real guids in order for dictionary of guids to not throw aggregate exceptions
|
||||
private void SetupOrgUserRepositoryCreateManyAsyncMock(IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Identity;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.SendAccess;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer.ClientProviders;
|
||||
using Duende.IdentityModel;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.UserFeatures.SendAccess;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.KeyManagement.Sends;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@@ -442,7 +446,8 @@ public class OrganizationUserRepositoryTests
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ISsoConfigRepository ssoConfigRepository)
|
||||
{
|
||||
var user1 = await userRepository.CreateAsync(new User
|
||||
{
|
||||
@@ -475,6 +480,18 @@ public class OrganizationUserRepositoryTests
|
||||
AccessSecretsManager = false
|
||||
});
|
||||
|
||||
var ssoConfigData = new SsoConfigurationData
|
||||
{
|
||||
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption
|
||||
};
|
||||
|
||||
var ssoConfig = await ssoConfigRepository.CreateAsync(new SsoConfig
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Enabled = true,
|
||||
Data = ssoConfigData.Serialize()
|
||||
});
|
||||
|
||||
var responseModel = await organizationUserRepository.GetManyDetailsByUserAsync(user1.Id);
|
||||
|
||||
Assert.NotNull(responseModel);
|
||||
@@ -487,6 +504,8 @@ public class OrganizationUserRepositoryTests
|
||||
Assert.Equal(organization.UsePolicies, result.UsePolicies);
|
||||
Assert.Equal(organization.UseSso, result.UseSso);
|
||||
Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);
|
||||
Assert.Equal(ssoConfig.Enabled, result.SsoEnabled);
|
||||
Assert.Equal(ssoConfig.Data, result.SsoConfig);
|
||||
Assert.Equal(organization.UseScim, result.UseScim);
|
||||
Assert.Equal(organization.UseGroups, result.UseGroups);
|
||||
Assert.Equal(organization.UseDirectory, result.UseDirectory);
|
||||
|
||||
Reference in New Issue
Block a user