V14: Add authorization policies to Management API controllers - p2 (#15211)
* Making ProblemDetails details more generic * Adding authorizer that can be replaces for external authz in handlers. Adding handler and requirement for UserBelongsToUserGroupInRequest policy * Adding method to get the GUID from claims * Adding service methods to check user group authz * Porting MustSatisfyRequirementAuthorizationHandler * Adding controllers authz * Fix return status code + produced response type * Moving to folder * Adding DenyLocalLogin policy scaffold * Implement a temp DenyLocalLoginHandler * Introducing a new Fobidden result * Fix comment * Introducing a helper class for authorizers * Changed nullability for GetCurrentUser * Changes from Attempt to Status + FIXME comments * Create a UserGroupAuthorizationStatus to be used in the future * Introduces a new authz status for checking media acess * Introducing a new permission service for media * Adding fixme * Adding more policy configurations * Adding Media policy requirement and handler * Adding media authorizer * Fix order of params * Adding duplicate code comment * Adding authz to media controllers * Migrating more logic from MediaPermissions.cs * Adding more MediaAuthorizationStatus-es * Handling of new authorization status * Fix comment * Adding NotFound case * Adding NewDenyLocalLoginIfConfigured policy && commenting [AllowAnonymous] where the policy is applied since it is already handled * Changed Forbid() to Forbidden() to get the correct status code * Remove policy that is applied on the base controller already * Implement and apply NewUmbracoFeatureEnabled policy * Renaming classes to add Permission in the name * Register permission services * Add FIXME * Introduce new IUserGroupPermissionService and refactor accordingly * Add single overload with default implementation * Adding user permission policy and related * Applying admin policy * Register all new policies * Better wording * Add default implementation for a single overload * Adding remarks to IContentPermissionService.cs * Supporting null as key in ContentPermissionService * Fix namespace * Reverting back to not supporting null as content key, but having dedicated implementation * Adding content authorizer with null values to represent root item * Removing null key support and adding dedicated implementation * Removing remarks * Adding content resource with null support * Removing null support * Adding requirement and status * Adding content authorizer + handlers * Applying policies to content controllers * Update comment * Handling of Authorization Statuses * More authz in controllers * Fix comments * New branch handler * Obsolete old implementation * Adding dedicated policies to root and bin * Adding a branch specific namespace * Bin specific requirement and namespace * Root specific requirement and namespace * Changing to new root policy * Refactoring * Save policies * Fix null check/reference * Add TODO comment * Create media root- and bin-specific policies, handlers, etc. * Apply correct policy in create and update media controllers * Apply root policy to move and sort controllers * Fix wording * Adding UserGroupAuthorizationStatusResult * Remove all AuthorizationStatusResult as we cannot get the specific AuthorizationStatus * Fixing Umbraco feature policy * Fix allow anonymous endpoints - the value returned from DenyLocalLoginHandler wasn't enough, we need to succeed DenyAnonymousAuthorizationRequirement as it is required for some of the endpoints that had the attribute * Apply DenyLocalLoginIfConfigured policy to corresponding re-implementation of PostSetInvitedUserPassword * Fix comment * Renaming performingUser to user and fixing comments * Rename helper method * Fix references * Re-add merge conflict deletion * Adding Backoffice requirement and relevant * Registering * Added a simple policy test * Fixed small test things and clean up * Temp solution * Added one more test and fix another static issue * Fix another merge conflict * Remove BackOfficePermissionRequirement and handler as they might not be necessary * Comment out again [AllowAnonymous] * Remove AuthorizationPolicies.BackOfficeAccessWithoutApproval policy as it might not be necessary * Fix temp implementation * Fix reference to correct handler * Apply authz policy to new publish/unpublish controllers * Fix comments * Removing duplicate ProducesResponseTypes * Added swagger documentation about the 401 and 403 * Added Resources to Media, User and UserGroup * Handle root, recycle bin and branch in the same handler * Handle both parent and target when moving * Check Ids for all sort requests * Xml docs * Clean up * Clean up * Fix build * Cleanup * Remove TODO * Added missing overload * Use yield * Adding some keys to check --------- Co-authored-by: Bjarke Berg <mail@bergmania.dk> Co-authored-by: Andreas Zerbst <andr317c@live.dk>
This commit is contained in:
committed by
GitHub
parent
f8b95e5c69
commit
fda866fc9e
@@ -0,0 +1,143 @@
|
||||
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.AspNetCore.Http;
|
||||
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.Core;
|
||||
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.Integration.TestServerTest;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi;
|
||||
|
||||
[TestFixture]
|
||||
public abstract class ManagementApiTest<T> : UmbracoTestServerTestBase
|
||||
where T : ManagementApiControllerBase
|
||||
{
|
||||
[SetUp]
|
||||
public async Task Setup()
|
||||
{
|
||||
Client.DefaultRequestHeaders
|
||||
.Accept
|
||||
.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json));
|
||||
}
|
||||
|
||||
protected override void CustomTestAuthSetup(IServiceCollection services)
|
||||
{
|
||||
// We do not wanna fake anything, and thereby have protection
|
||||
}
|
||||
|
||||
protected abstract Expression<Func<T, object>> MethodSelector { get; }
|
||||
|
||||
protected virtual string Url => GetManagementApiUrl(MethodSelector);
|
||||
|
||||
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin)
|
||||
{
|
||||
Guid userKey = Constants.Security.SuperUserKey;
|
||||
OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor;
|
||||
var scopeProvider = GetRequiredService<ICoreScopeProvider>();
|
||||
using (var scope = scopeProvider.CreateCoreScope())
|
||||
{
|
||||
var userService = GetRequiredService<IUserService>();
|
||||
using var serviceScope = GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||
var userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
|
||||
|
||||
IUser user;
|
||||
if (isAdmin)
|
||||
{
|
||||
user = await userService.GetAsync(userKey) ??
|
||||
throw new Exception("Super user not found.");
|
||||
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;
|
||||
userKey = user.Key;
|
||||
}
|
||||
|
||||
|
||||
var token = await userManager.GeneratePasswordResetTokenAsync(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 BackOfficeController.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(null)), JsonContent.Create(loginModel));
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, loginResponse.StatusCode, await loginResponse.Content.ReadAsStringAsync());
|
||||
|
||||
var codeVerifier = "12345"; // Just a dummy value we use in tests
|
||||
var codeChallange = Convert.ToBase64String(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)))
|
||||
.TrimEnd("=");
|
||||
|
||||
var authorizeResponse = await client.GetAsync(
|
||||
GetManagementApiUrl<BackOfficeController>(x => x.Authorize()) +
|
||||
$"?client_id={backofficeOpenIddictApplicationDescriptor.ClientId}&response_type=code&redirect_uri={WebUtility.UrlEncode(backofficeOpenIddictApplicationDescriptor.RedirectUris.FirstOrDefault()?.AbsoluteUri)}&code_challenge_method=S256&code_challenge={codeChallange}");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private class TokenModel
|
||||
{
|
||||
[JsonPropertyName("access_token")] public string AccessToken { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Api.Management.Controllers.AuditLog;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class ByKeyAuditLogControllerTests : ManagementApiTest<ByKeyAuditLogController>
|
||||
{
|
||||
protected override Expression<Func<ByKeyAuditLogController, object>> MethodSelector =>
|
||||
x => x.ByKey(Constants.Security.SuperUserKey, Direction.Ascending, null, 0, 100);
|
||||
|
||||
[Test]
|
||||
public virtual async Task As_Admin_I_Have_Access()
|
||||
{
|
||||
await AuthenticateClientAsync(Client, "admin@umbraco.com", "1234567890", true);
|
||||
|
||||
var response = await Client.GetAsync(Url);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public virtual async Task As_Editor_I_Have_Access()
|
||||
{
|
||||
await AuthenticateClientAsync(Client, "editor@umbraco.com", "1234567890", false);
|
||||
|
||||
var response = await Client.GetAsync(Url);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public virtual async Task Unauthourized_when_no_token_is_provided()
|
||||
{
|
||||
var response = await Client.GetAsync(Url);
|
||||
|
||||
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode, await response.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user