Skip to content

Commit c9758ed

Browse files
V15: Add authorization to saves (#18111)
* Re-add authorization * Add test plumbing * Add test helper * Add happy path test * Remove usage of negation * Minor DRYup of test code. --------- Co-authored-by: Andy Butland <[email protected]>
1 parent 1d9704d commit c9758ed

File tree

4 files changed

+236
-39
lines changed

4 files changed

+236
-39
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/UpdateDocumentControllerBase.cs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,18 @@ protected UpdateDocumentControllerBase(IAuthorizationService authorizationServic
1717

1818
protected async Task<IActionResult> HandleRequest(Guid id, UpdateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
1919
{
20-
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
21-
// The values are ignored in the ContentEditingService
20+
// We intentionally don't pass in cultures here.
21+
// This is to support the client sending values for all cultures even if the user doesn't have access to the language.
22+
// Values for unauthorized languages are later ignored in the ContentEditingService.
23+
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
24+
User,
25+
ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id),
26+
AuthorizationPolicies.ContentPermissionByResource);
2227

23-
// IEnumerable<string> cultures = requestModel.Variants
24-
// .Where(v => v.Culture is not null)
25-
// .Select(v => v.Culture!);
26-
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
27-
// User,
28-
// ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id, cultures),
29-
// AuthorizationPolicies.ContentPermissionByResource);
30-
//
31-
// if (!authorizationResult.Succeeded)
32-
// {
33-
// return Forbidden();
34-
// }
28+
if (authorizationResult.Succeeded is false)
29+
{
30+
return Forbidden();
31+
}
3532

3633
return await authorizedHandler();
3734
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using Umbraco.Cms.Api.Management.ViewModels;
2+
using Umbraco.Cms.Api.Management.ViewModels.Document;
3+
using Umbraco.Cms.Core.Models.ContentEditing;
4+
5+
namespace Umbraco.Cms.Tests.Common.TestHelpers;
6+
7+
public static class DocumentUpdateHelper
8+
{
9+
public static UpdateDocumentRequestModel CreateInvariantDocumentUpdateRequestModel(ContentCreateModel createModel)
10+
{
11+
var updateRequestModel = new UpdateDocumentRequestModel();
12+
13+
updateRequestModel.Template = ReferenceByIdModel.ReferenceOrNull(createModel.TemplateKey);
14+
updateRequestModel.Variants =
15+
[
16+
new DocumentVariantRequestModel
17+
{
18+
Segment = null,
19+
Culture = null,
20+
Name = createModel.InvariantName!,
21+
}
22+
];
23+
updateRequestModel.Values = createModel.InvariantProperties.Select(x => new DocumentValueModel
24+
{
25+
Alias = x.Alias,
26+
Value = x.Value,
27+
});
28+
29+
return updateRequestModel;
30+
}
31+
}

tests/Umbraco.Tests.Integration/ManagementApi/ManagementApiTest.cs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,41 +47,56 @@ protected override void CustomTestAuthSetup(IServiceCollection services)
4747

4848
protected virtual string Url => GetManagementApiUrl(MethodSelector);
4949

50-
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin)
50+
protected async Task AuthenticateClientAsync(HttpClient client, string username, string password, bool isAdmin) =>
51+
await AuthenticateClientAsync(client,
52+
async userService =>
53+
{
54+
IUser user;
55+
if (isAdmin)
56+
{
57+
user = await userService.GetRequiredUserAsync(Constants.Security.SuperUserKey);
58+
user.Username = user.Email = username;
59+
userService.Save(user);
60+
}
61+
else
62+
{
63+
user = (await userService.CreateAsync(
64+
Constants.Security.SuperUserKey,
65+
new UserCreateModel
66+
{
67+
Email = username,
68+
Name = username,
69+
UserName = username,
70+
UserGroupKeys = new HashSet<Guid>(new[] { Constants.Security.EditorGroupKey })
71+
},
72+
true)).Result.CreatedUser;
73+
}
74+
75+
return (user, password);
76+
});
77+
78+
79+
protected async Task AuthenticateClientAsync(HttpClient client, Func<IUserService, Task<(IUser user, string Password)>> createUser)
5180
{
52-
Guid userKey = Constants.Security.SuperUserKey;
81+
5382
OpenIddictApplicationDescriptor backofficeOpenIddictApplicationDescriptor;
5483
var scopeProvider = GetRequiredService<ICoreScopeProvider>();
84+
85+
string? username;
86+
string? password;
87+
5588
using (var scope = scopeProvider.CreateCoreScope())
5689
{
5790
var userService = GetRequiredService<IUserService>();
5891
using var serviceScope = GetRequiredService<IServiceScopeFactory>().CreateScope();
5992
var userManager = serviceScope.ServiceProvider.GetRequiredService<ICoreBackOfficeUserManager>();
6093

61-
IUser user;
62-
if (isAdmin)
63-
{
64-
user = await userService.GetRequiredUserAsync(userKey);
65-
user.Username = user.Email = username;
66-
userService.Save(user);
67-
}
68-
else
69-
{
70-
user = (await userService.CreateAsync(
71-
Constants.Security.SuperUserKey,
72-
new UserCreateModel()
73-
{
74-
Email = username,
75-
Name = username,
76-
UserName = username,
77-
UserGroupKeys = new HashSet<Guid>(new[] { Constants.Security.EditorGroupKey })
78-
},
79-
true)).Result.CreatedUser;
80-
userKey = user.Key;
81-
}
94+
var userCreationResult = await createUser(userService);
95+
username = userCreationResult.user.Username;
96+
password = userCreationResult.Password;
97+
var userKey = userCreationResult.user.Key;
8298

83-
84-
var token = await userManager.GeneratePasswordResetTokenAsync(user);
99+
var token = await userManager.GeneratePasswordResetTokenAsync(userCreationResult.user);
85100

86101

87102
var changePasswordAttempt = await userService.ChangePasswordAsync(userKey,
@@ -99,6 +114,7 @@ protected async Task AuthenticateClientAsync(HttpClient client, string username,
99114
BackOfficeApplicationManager;
100115
backofficeOpenIddictApplicationDescriptor =
101116
backOfficeApplicationManager.BackofficeOpenIddictApplicationDescriptor(client.BaseAddress);
117+
102118
scope.Complete();
103119
}
104120

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System.Linq.Expressions;
2+
using System.Net;
3+
using System.Text;
4+
using NUnit.Framework;
5+
using Umbraco.Cms.Api.Management.Controllers.Document;
6+
using Umbraco.Cms.Api.Management.ViewModels.Document;
7+
using Umbraco.Cms.Core;
8+
using Umbraco.Cms.Core.Actions;
9+
using Umbraco.Cms.Core.Models;
10+
using Umbraco.Cms.Core.Models.ContentEditing;
11+
using Umbraco.Cms.Core.Models.ContentPublishing;
12+
using Umbraco.Cms.Core.Models.Membership;
13+
using Umbraco.Cms.Core.Serialization;
14+
using Umbraco.Cms.Core.Services;
15+
using Umbraco.Cms.Core.Services.ContentTypeEditing;
16+
using Umbraco.Cms.Core.Strings;
17+
using Umbraco.Cms.Tests.Common.Builders;
18+
using Umbraco.Cms.Tests.Common.TestHelpers;
19+
20+
namespace Umbraco.Cms.Tests.Integration.ManagementApi.Policies;
21+
22+
public class UpdateDocumentTests : ManagementApiTest<UpdateDocumentController>
23+
{
24+
private IUserGroupService UserGroupService => GetRequiredService<IUserGroupService>();
25+
26+
private IShortStringHelper ShortStringHelper => GetRequiredService<IShortStringHelper>();
27+
28+
private IJsonSerializer JsonSerializer => GetRequiredService<IJsonSerializer>();
29+
30+
private ITemplateService TemplateService => GetRequiredService<ITemplateService>();
31+
32+
private IContentTypeEditingService ContentTypeEditingService => GetRequiredService<IContentTypeEditingService>();
33+
34+
private IContentEditingService ContentEditingService => GetRequiredService<IContentEditingService>();
35+
36+
private IContentPublishingService ContentPublishingService => GetRequiredService<IContentPublishingService>();
37+
38+
private IContentService ContentService => GetRequiredService<IContentService>();
39+
40+
protected override Expression<Func<UpdateDocumentController, object>> MethodSelector =>
41+
x => x.Update(CancellationToken.None, Guid.Empty, null!);
42+
43+
[Test]
44+
public async Task UserWithoutPermissionCannotUpdate()
45+
{
46+
var userGroup = new UserGroup(ShortStringHelper)
47+
{
48+
Name = "Test",
49+
Alias = "test",
50+
Permissions = new HashSet<string> { ActionBrowse.ActionLetter },
51+
HasAccessToAllLanguages = true,
52+
StartContentId = -1,
53+
StartMediaId = -1
54+
};
55+
userGroup.AddAllowedSection("content");
56+
userGroup.AddAllowedSection("media");
57+
58+
var groupCreationResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey);
59+
Assert.IsTrue(groupCreationResult.Success);
60+
61+
await AuthenticateClientAsync(Client, async userService =>
62+
{
63+
var email = "[email protected]";
64+
var testUserCreateModel = new UserCreateModel
65+
{
66+
Email = email,
67+
Name = "Test Mc.Gee",
68+
UserName = email,
69+
UserGroupKeys = new HashSet<Guid> { groupCreationResult.Result.Key },
70+
};
71+
72+
var userCreationResult =
73+
await userService.CreateAsync(Constants.Security.SuperUserKey, testUserCreateModel, true);
74+
75+
Assert.IsTrue(userCreationResult.Success);
76+
77+
return (userCreationResult.Result.CreatedUser, "1234567890");
78+
});
79+
80+
const string UpdatedName = "NewName";
81+
82+
var model = await CreateContent();
83+
var updateRequestModel = CreateRequestModel(model, UpdatedName);
84+
85+
var response = await GetManagementApiResponse(model, updateRequestModel);
86+
87+
AssertResponse(response, model, HttpStatusCode.Forbidden, model.InvariantName);
88+
}
89+
90+
[Test]
91+
public async Task UserWithPermissionCanUpdate()
92+
{
93+
// "Default" version creates an editor that has permission to update content.
94+
await AuthenticateClientAsync(Client, "[email protected]", "1234567890", false);
95+
96+
const string UpdatedName = "NewName";
97+
98+
var model = await CreateContent();
99+
var updateRequestModel = CreateRequestModel(model, UpdatedName);
100+
101+
var response = await GetManagementApiResponse(model, updateRequestModel);
102+
103+
AssertResponse(response, model, HttpStatusCode.OK, UpdatedName);
104+
}
105+
106+
private async Task<ContentCreateModel> CreateContent()
107+
{
108+
var userKey = Constants.Security.SuperUserKey;
109+
var template = TemplateBuilder.CreateTextPageTemplate();
110+
var templateAttempt = await TemplateService.CreateAsync(template, userKey);
111+
Assert.IsTrue(templateAttempt.Success);
112+
113+
var contentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key);
114+
var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentTypeCreateModel, userKey);
115+
Assert.IsTrue(contentTypeAttempt.Success);
116+
117+
var textPage = ContentEditingBuilder.CreateSimpleContent(contentTypeAttempt.Result.Key);
118+
textPage.TemplateKey = templateAttempt.Result.Key;
119+
textPage.Key = Guid.NewGuid();
120+
var createContentResult = await ContentEditingService.CreateAsync(textPage, userKey);
121+
Assert.IsTrue(createContentResult.Success);
122+
123+
var publishResult = await ContentPublishingService.PublishAsync(
124+
createContentResult.Result.Content!.Key,
125+
[new() { Culture = "*" }],
126+
userKey);
127+
128+
Assert.IsTrue(publishResult.Success);
129+
return textPage;
130+
}
131+
132+
private static UpdateDocumentRequestModel CreateRequestModel(ContentCreateModel model, string name)
133+
{
134+
var updateRequestModel = DocumentUpdateHelper.CreateInvariantDocumentUpdateRequestModel(model);
135+
updateRequestModel.Variants.First().Name = name;
136+
return updateRequestModel;
137+
}
138+
139+
private async Task<HttpResponseMessage> GetManagementApiResponse(ContentCreateModel model, UpdateDocumentRequestModel updateRequestModel)
140+
{
141+
var url = GetManagementApiUrl<UpdateDocumentController>(x => x.Update(CancellationToken.None, model.Key!.Value, null));
142+
var requestBody = new StringContent(JsonSerializer.Serialize(updateRequestModel), Encoding.UTF8, "application/json");
143+
return await Client.PutAsync(url, requestBody);
144+
}
145+
146+
private void AssertResponse(HttpResponseMessage response, ContentCreateModel model, HttpStatusCode expectedStatusCode, string expectedContentName)
147+
{
148+
Assert.That(response.StatusCode, Is.EqualTo(expectedStatusCode));
149+
var content = ContentService.GetById(model.Key!.Value);
150+
Assert.IsNotNull(content);
151+
Assert.That(content.Name, Is.EqualTo(expectedContentName));
152+
}
153+
}

0 commit comments

Comments
 (0)