* V15 QA Added the authorization integration tests (#18419) * Added authorization integration tests * Removed unnecessary tests and update tests for preview controller * Updated to use the newest changes from v15/dev and added an override for the AuthenticateClientAsync to use the userGroupKey * Updated CompatibilitySuppressions to include changes from integration tests * Updated pipelines * Skips managementApi tests * Only run necessary tests * Added new schema per fixture to reduce test setup time * Fixed failing tests * Updated test setup * Updated test * Added suppression * Fixed failing tests * Updated addOnTeardown methods to protected * Added method for clearing the host * Added teardown * Updated model usage * Added a lot of cleanup for memory leak issues when running tests * Added CompatibilitySuppressions.xml * Updated tests * Cleaned up * Adjusted base classes * Updated pipeline * Updated CompatibilitySuppressions.xml * Updated test logging * Fixed reponse * Updated condition to skip tests * Updated tests, not done * Reworked test to expect correct responses with correct setup * Updated tests * More updates to tests * Updated tests * Cleaned up tests * Updated setup * Cleaned up tests to match setup * Cleaned up setup * Removed suppression * Fixed tests * Move order of checks * Fix naming * Formatting * Dispose of host * Keep track of if we're disposed * Compat suppression * Dont dispose * Fix failing tests * removed unused virtual * Updated CompatibilitySuppressions.xml --------- Co-authored-by: Andreas Zerbst <andr317c@live.dk> Co-authored-by: Zeegaan <skrivdetud@gmail.com> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> # Conflicts: # tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml # tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs # tests/Umbraco.Tests.Integration/ManagementApi/Policies/AllCultureControllerTests.cs # tests/Umbraco.Tests.Integration/ManagementApi/Policies/CreateDocumentTests.cs # tests/Umbraco.Tests.Integration/ManagementApi/Policies/UpdateDocumentTests.cs # tests/Umbraco.Tests.Integration/ManagementApi/Preview/EndPreviewTests.cs # tests/Umbraco.Tests.Integration/ManagementApi/Preview/EnterPreviewTests.cs # tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs * Updated test * Updates * Removed unnessecary test --------- Co-authored-by: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Co-authored-by: Zeegaan <skrivdetud@gmail.com> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
210 lines
8.9 KiB
C#
210 lines
8.9 KiB
C#
using System.Linq.Expressions;
|
|
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using System.Net.Mime;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json.Serialization;
|
|
using System.Web;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using NUnit.Framework;
|
|
using OpenIddict.Abstractions;
|
|
using Umbraco.Cms.Api.Management.Controllers;
|
|
using Umbraco.Cms.Api.Management.Controllers.Security;
|
|
using Umbraco.Cms.Api.Management.Security;
|
|
using Umbraco.Cms.Api.Management.ViewModels.Security;
|
|
using Umbraco.Cms.Core;
|
|
using Umbraco.Cms.Core.Extensions;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Models.Membership;
|
|
using Umbraco.Cms.Core.Scoping;
|
|
using Umbraco.Cms.Core.Security;
|
|
using Umbraco.Cms.Core.Services;
|
|
using Umbraco.Cms.Infrastructure.Security;
|
|
using Umbraco.Cms.Tests.Common.Testing;
|
|
using Umbraco.Cms.Tests.Integration.TestServerTest;
|
|
|
|
namespace Umbraco.Cms.Tests.Integration.ManagementApi;
|
|
|
|
[TestFixture]
|
|
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture, Logger = UmbracoTestOptions.Logger.Console, Boot = true)]
|
|
public abstract class ManagementApiTest<T> : UmbracoTestServerTestBase
|
|
where T : ManagementApiControllerBase
|
|
{
|
|
|
|
private static readonly Dictionary<string, TokenModel> _tokenCache = new();
|
|
private static readonly SHA256 _sha256 = SHA256.Create();
|
|
|
|
protected abstract Expression<Func<T, object>> MethodSelector { get; set; }
|
|
|
|
protected string Url => GetManagementApiUrl(MethodSelector);
|
|
|
|
[SetUp]
|
|
public override void Setup()
|
|
{
|
|
InMemoryConfiguration["Umbraco:CMS:ModelsBuilder:ModelsMode"] = "Nothing";
|
|
|
|
base.Setup();
|
|
Client.DefaultRequestHeaders.Accept.Clear();
|
|
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
|
}
|
|
|
|
[SetUp]
|
|
public override void SetUp_Logging() =>
|
|
TestContext.Out.Write($"Start test {TestCount++}: {TestContext.CurrentContext.Test.FullName}");
|
|
|
|
[OneTimeTearDown]
|
|
public void ClearCache() => _tokenCache.Clear();
|
|
|
|
protected override void CustomTestAuthSetup(IServiceCollection services)
|
|
{
|
|
// We do not wanna fake anything, and thereby have protection
|
|
}
|
|
|
|
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) =>
|
|
await AuthenticateClientAsync(client,
|
|
async userService =>
|
|
{
|
|
IUser user;
|
|
if (isAdmin)
|
|
{
|
|
user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey);
|
|
user.Username = user.Email = username;
|
|
userService.Save(user);
|
|
}
|
|
else
|
|
{
|
|
user = (await userService.CreateAsync(
|
|
Constants.Security.SuperUserKey,
|
|
new UserCreateModel
|
|
{
|
|
Email = username,
|
|
Name = username,
|
|
UserName = username,
|
|
UserGroupKeys = new HashSet<Guid>(new[] { Constants.Security.EditorGroupKey })
|
|
},
|
|
true)).Result.CreatedUser;
|
|
}
|
|
|
|
return (user, password);
|
|
}, $"{username}:{isAdmin}");
|
|
|
|
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, Guid userGroupKey) =>
|
|
await AuthenticateClientAsync(client,
|
|
async userService =>
|
|
{
|
|
IUser user;
|
|
if (userGroupKey == Constants.Security.AdminGroupKey)
|
|
{
|
|
user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey);
|
|
user.Username = user.Email = username;
|
|
userService.Save(user);
|
|
}
|
|
else
|
|
{
|
|
user = (await userService.CreateAsync(
|
|
Constants.Security.SuperUserKey,
|
|
new UserCreateModel
|
|
{
|
|
Email = username,
|
|
Name = username,
|
|
UserName = username,
|
|
UserGroupKeys = new HashSet<Guid>([userGroupKey]),
|
|
},
|
|
true)).Result.CreatedUser;
|
|
}
|
|
|
|
return (user, password);
|
|
}, $"{username}:{userGroupKey}");
|
|
|
|
protected async Task AuthenticateClientAsync(HttpClient client, Func<IUserService, Task<(IUser User, string Password)>> createUser, string cacheKey = null)
|
|
{
|
|
// Check cache first
|
|
if (!string.IsNullOrEmpty(cacheKey) && _tokenCache.TryGetValue(cacheKey, out var cachedToken))
|
|
{
|
|
client.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", cachedToken.AccessToken);
|
|
return;
|
|
}
|
|
|
|
OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor;
|
|
var scopeProvider = GetRequiredService<ICoreScopeProvider>();
|
|
|
|
string username;
|
|
string password;
|
|
|
|
using (var scope = scopeProvider.CreateCoreScope())
|
|
{
|
|
var userService = GetRequiredService<IUserService>();
|
|
using var serviceScope = GetRequiredService<IServiceScopeFactory>().CreateScope();
|
|
var userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
|
|
|
var userCreationResult = await createUser(userService);
|
|
username = userCreationResult.User.Username;
|
|
password = userCreationResult.Password;
|
|
var userKey = userCreationResult.User.Key;
|
|
|
|
var token = await userManager.GeneratePasswordResetTokenAsync(userCreationResult.User);
|
|
|
|
var changePasswordAttempt = await userService.ChangePasswordAsync(userKey,
|
|
new ChangeUserPasswordModel
|
|
{
|
|
NewPassword = password, ResetPasswordToken = token.Result.ToUrlBase64(), UserKey = userKey,
|
|
});
|
|
|
|
Assert.IsTrue(changePasswordAttempt.Success);
|
|
|
|
var backOfficeApplicationManager =
|
|
serviceScope.ServiceProvider.GetRequiredService<IBackOfficeApplicationManager>() as
|
|
BackOfficeApplicationManager;
|
|
backofficeOpenIddictApplicationDescriptor =
|
|
backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress);
|
|
scope.Complete();
|
|
}
|
|
|
|
var loginModel = new LoginRequestModel { Username = username, Password = password };
|
|
|
|
// Login to ensure the cookie is set (used in next request)
|
|
var loginResponse = await client.PostAsync(
|
|
GetManagementApiUrl<BackOfficeController>(x => x.Login(CancellationToken.None, null)), JsonContent.Create(loginModel));
|
|
|
|
Assert.AreEqual(HttpStatusCode.OK, loginResponse.StatusCode, await loginResponse.Content.ReadAsStringAsync());
|
|
|
|
const string codeVerifier = "12345"; // Just a dummy value we use in tests
|
|
var codeChallenge = Convert.ToBase64String(_sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)))
|
|
.TrimEnd("=");
|
|
|
|
var authorizationUrl = GetManagementApiUrl<BackOfficeController>(x => x.Authorize(CancellationToken.None)) + $"?client_id={backofficeOpenIddictApplicationDescriptor.ClientId}&response_type=code&redirect_uri={WebUtility.UrlEncode(backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault()?.AbsoluteUri)}&code_challenge_method=S256&code_challenge={codeChallenge}";
|
|
var authorizeResponse = await client.GetAsync(authorizationUrl);
|
|
|
|
Assert.AreEqual(HttpStatusCode.Found, authorizeResponse.StatusCode, await authorizeResponse.Content.ReadAsStringAsync());
|
|
|
|
var tokenResponse = await client.PostAsync("/umbraco/management/api/v1/security/back-office/token",
|
|
new FormUrlEncodedContent(new Dictionary<string, string>
|
|
{
|
|
["grant_type"] = "authorization_code",
|
|
["code_verifier"] = codeVerifier,
|
|
["client_id"] = backofficeOpenIddictApplicationDescriptor.ClientId,
|
|
["code"] = HttpUtility.ParseQueryString(authorizeResponse.Headers.Location.Query).Get("code"),
|
|
["redirect_uri"] =
|
|
backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault().AbsoluteUri,
|
|
}));
|
|
|
|
var tokenModel = await tokenResponse.Content.ReadFromJsonAsync<TokenModel>();
|
|
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenModel.AccessToken);
|
|
|
|
// Cache the token if cache key provided
|
|
if (!string.IsNullOrEmpty(cacheKey))
|
|
{
|
|
_tokenCache[cacheKey] = tokenModel;
|
|
}
|
|
}
|
|
|
|
private class TokenModel
|
|
{
|
|
[JsonPropertyName("access_token")] public string AccessToken { get; set; }
|
|
}
|
|
}
|