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:
Elitsa Marinovska
2023-12-11 08:25:29 +01:00
committed by GitHub
parent f8b95e5c69
commit fda866fc9e
109 changed files with 3750 additions and 212 deletions

View File

@@ -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; }
}
}

View File

@@ -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());
}
}