Skip to content

Commit 3fa053a

Browse files
Delivery API: Adding allow list for content types (#21111)
* Adding allow content type alias settings and validator * Creating private helper method for tests * Revisiting logic for disallow types * Adding tests for validator * Obsolete unnecessary methods and overloads. * Fix warning and update naming in tests. --------- Co-authored-by: Andy Butland <[email protected]>
1 parent 7b13e23 commit 3fa053a

File tree

8 files changed

+337
-27
lines changed

8 files changed

+337
-27
lines changed

src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,25 @@ public class DeliveryApiSettings
4242
/// <value>
4343
/// The content type aliases that are not to be exposed.
4444
/// </value>
45+
/// <remarks>
46+
/// If <see cref="AllowedContentTypeAliases"/> is configured (non-empty), this setting is ignored.
47+
/// </remarks>
4548
public ISet<string> DisallowedContentTypeAliases { get; set; } = new HashSet<string>();
4649

50+
/// <summary>
51+
/// Gets or sets the aliases of the content types that are exclusively allowed to be exposed through the Delivery API.
52+
/// When configured, only content of these types will be returned from Delivery API endpoints and added to the query index.
53+
/// </summary>
54+
/// <value>
55+
/// The content type aliases that are allowed to be exposed.
56+
/// </value>
57+
/// <remarks>
58+
/// When this setting is configured (non-empty), it takes precedence over <see cref="DisallowedContentTypeAliases"/>.
59+
/// If a content type alias appears in both lists, the allow list wins and the content type will be exposed.
60+
/// If this setting is empty, all content types are allowed except those in <see cref="DisallowedContentTypeAliases"/>.
61+
/// </remarks>
62+
public ISet<string> AllowedContentTypeAliases { get; set; } = new HashSet<string>();
63+
4764
/// <summary>
4865
/// Gets or sets a value indicating whether the Delivery API should output rich text values as JSON instead of HTML.
4966
/// </summary>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Umbraco.
2+
// See LICENSE for more details.
3+
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Extensions.Options;
6+
using Umbraco.Extensions;
7+
8+
namespace Umbraco.Cms.Core.Configuration.Models.Validation;
9+
10+
/// <summary>
11+
/// Validator for configuration represented as <see cref="DeliveryApiSettings" />.
12+
/// </summary>
13+
public class DeliveryApiSettingsValidator : IValidateOptions<DeliveryApiSettings>
14+
{
15+
private readonly ILogger<DeliveryApiSettingsValidator> _logger;
16+
17+
public DeliveryApiSettingsValidator(ILogger<DeliveryApiSettingsValidator> logger)
18+
=> _logger = logger;
19+
20+
/// <inheritdoc />
21+
public ValidateOptionsResult Validate(string? name, DeliveryApiSettings options)
22+
{
23+
ValidateContentTypeAliasOverlap(options);
24+
25+
return ValidateOptionsResult.Success;
26+
}
27+
28+
private void ValidateContentTypeAliasOverlap(DeliveryApiSettings options)
29+
{
30+
if (options.AllowedContentTypeAliases.Count == 0 || options.DisallowedContentTypeAliases.Count == 0)
31+
{
32+
return;
33+
}
34+
35+
var overlappingAliases = options.AllowedContentTypeAliases
36+
.Where(alias => options.DisallowedContentTypeAliases.InvariantContains(alias))
37+
.ToArray();
38+
39+
if (overlappingAliases.Length > 0)
40+
{
41+
_logger.LogWarning(
42+
"Delivery API configuration contains content type aliases that appear in both AllowedContentTypeAliases and DisallowedContentTypeAliases: {Aliases}. " +
43+
"The allow list takes precedence, so these content types will be allowed. Consider removing them from the disallow list to avoid confusion.",
44+
string.Join(", ", overlappingAliases));
45+
}
46+
}
47+
}

src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,5 @@ public IEnumerable<IPublishedContent> GetByIds(IEnumerable<Guid> contentIds)
129129
: null;
130130

131131
private bool IsAllowedContentType(IPublishedContent content)
132-
=> _deliveryApiSettings.IsAllowedContentType(content);
132+
=> _deliveryApiSettings.IsAllowedContentType(content.ContentType.Alias);
133133
}

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
4141
{
4242
// Register configuration validators.
4343
builder.Services.AddSingleton<IValidateOptions<ContentSettings>, ContentSettingsValidator>();
44+
builder.Services.AddSingleton<IValidateOptions<DeliveryApiSettings>, DeliveryApiSettingsValidator>();
4445
builder.Services.AddSingleton<IValidateOptions<GlobalSettings>, GlobalSettingsValidator>();
4546
builder.Services.AddSingleton<IValidateOptions<HealthChecksSettings>, HealthChecksSettingsValidator>();
4647
builder.Services.AddSingleton<IValidateOptions<LoggingSettings>, LoggingSettingsValidator>();
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,48 @@
1-
using Umbraco.Cms.Core.Configuration.Models;
1+
using Umbraco.Cms.Core.Configuration.Models;
22
using Umbraco.Cms.Core.Models.PublishedContent;
33

44
namespace Umbraco.Extensions;
55

6+
/// <summary>
7+
/// Provides extension methods for determining whether content types or content items are allowed to be exposed through
8+
/// the Delivery API based on the configured allow and disallow lists.
9+
/// </summary>
610
public static class DeliveryApiSettingsExtensions
711
{
12+
[Obsolete("Please use the overload of IsAllowedContentType taking a content type alias. Scheduled for removal in Umbraco 19.")]
813
public static bool IsAllowedContentType(this DeliveryApiSettings settings, IPublishedContent content)
914
=> settings.IsAllowedContentType(content.ContentType.Alias);
1015

16+
[Obsolete("Please use IsAllowedContentType and negate the result instead. Scheduled for removal in Umbraco 19.")]
1117
public static bool IsDisallowedContentType(this DeliveryApiSettings settings, IPublishedContent content)
1218
=> settings.IsDisallowedContentType(content.ContentType.Alias);
1319

20+
/// <summary>
21+
/// Determines whether a content type alias is allowed to be exposed through the Delivery API.
22+
/// </summary>
23+
/// <param name="settings">The Delivery API settings.</param>
24+
/// <param name="contentTypeAlias">The content type alias to check.</param>
25+
/// <returns>
26+
/// <c>true</c> if the content type is allowed; otherwise, <c>false</c>.
27+
/// </returns>
28+
/// <remarks>
29+
/// If the allow list is configured (non-empty), only content types in the allow list are permitted.
30+
/// The allow list takes precedence - if a content type is in both allow and disallow lists, it is allowed.
31+
/// If the allow list is empty, all content types are allowed except those in the disallow list.
32+
/// </remarks>
1433
public static bool IsAllowedContentType(this DeliveryApiSettings settings, string contentTypeAlias)
15-
=> settings.IsDisallowedContentType(contentTypeAlias) is false;
34+
{
35+
// If allow list is configured, it takes precedence.
36+
if (settings.AllowedContentTypeAliases.Count > 0)
37+
{
38+
return settings.AllowedContentTypeAliases.InvariantContains(contentTypeAlias);
39+
}
1640

41+
// Otherwise the content type is allowed if it's not in the disallow list.
42+
return settings.DisallowedContentTypeAliases.InvariantContains(contentTypeAlias) is false;
43+
}
44+
45+
[Obsolete("Please use IsAllowedContentType and negate the result instead. Scheduled for removal in Umbraco 19.")]
1746
public static bool IsDisallowedContentType(this DeliveryApiSettings settings, string contentTypeAlias)
18-
=> settings.DisallowedContentTypeAliases.InvariantContains(contentTypeAlias);
47+
=> settings.IsAllowedContentType(contentTypeAlias) is false;
1948
}

src/Umbraco.Infrastructure/Examine/DeliveryApiContentIndexValueSetBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ private bool CanIndex(IContent content)
204204
}
205205

206206
// is the content type allowed in the index?
207-
if (_deliveryApiSettings.IsDisallowedContentType(content.ContentType.Alias))
207+
if (_deliveryApiSettings.IsAllowedContentType(content.ContentType.Alias) is false)
208208
{
209209
return false;
210210
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) Umbraco.
2+
// See LICENSE for more details.
3+
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Extensions.Options;
6+
using Moq;
7+
using NUnit.Framework;
8+
using Umbraco.Cms.Core.Configuration.Models;
9+
using Umbraco.Cms.Core.Configuration.Models.Validation;
10+
11+
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models.Validation;
12+
13+
[TestFixture]
14+
public class DeliveryApiSettingsValidatorTests
15+
{
16+
private Mock<ILogger<DeliveryApiSettingsValidator>> _loggerMock;
17+
18+
[SetUp]
19+
public void SetUp() => _loggerMock = new Mock<ILogger<DeliveryApiSettingsValidator>>();
20+
21+
[Test]
22+
public void Returns_Success_For_Configuration_With_Only_AllowList()
23+
{
24+
var validator = new DeliveryApiSettingsValidator(_loggerMock.Object);
25+
var options = new DeliveryApiSettings
26+
{
27+
AllowedContentTypeAliases = new HashSet<string> { "content1", "content2" },
28+
};
29+
30+
ValidateOptionsResult result = validator.Validate("settings", options);
31+
32+
Assert.IsTrue(result.Succeeded);
33+
VerifyNoWarningLogged();
34+
}
35+
36+
[Test]
37+
public void Returns_Success_For_Configuration_With_Only_DisallowList()
38+
{
39+
var validator = new DeliveryApiSettingsValidator(_loggerMock.Object);
40+
var options = new DeliveryApiSettings
41+
{
42+
DisallowedContentTypeAliases = new HashSet<string> { "content1", "content2" },
43+
};
44+
45+
ValidateOptionsResult result = validator.Validate("settings", options);
46+
47+
Assert.IsTrue(result.Succeeded);
48+
VerifyNoWarningLogged();
49+
}
50+
51+
[Test]
52+
public void Returns_Success_For_Configuration_With_No_Overlapping_Lists()
53+
{
54+
var validator = new DeliveryApiSettingsValidator(_loggerMock.Object);
55+
var options = new DeliveryApiSettings
56+
{
57+
AllowedContentTypeAliases = new HashSet<string> { "content1", "content2" },
58+
DisallowedContentTypeAliases = new HashSet<string> { "content3", "content4" },
59+
};
60+
61+
ValidateOptionsResult result = validator.Validate("settings", options);
62+
63+
Assert.IsTrue(result.Succeeded);
64+
VerifyNoWarningLogged();
65+
}
66+
67+
[Test]
68+
public void Returns_Success_But_Logs_Warning_For_Overlapping_Lists()
69+
{
70+
var validator = new DeliveryApiSettingsValidator(_loggerMock.Object);
71+
var options = new DeliveryApiSettings
72+
{
73+
AllowedContentTypeAliases = new HashSet<string> { "content1", "content2" },
74+
DisallowedContentTypeAliases = new HashSet<string> { "content1", "content3" },
75+
};
76+
77+
ValidateOptionsResult result = validator.Validate("settings", options);
78+
79+
Assert.IsTrue(result.Succeeded);
80+
81+
VerifyWarningLogged();
82+
}
83+
84+
private void VerifyWarningLogged()
85+
{
86+
var warningCount = GetWarningLogCount();
87+
Assert.AreEqual(1, warningCount);
88+
}
89+
90+
private void VerifyNoWarningLogged()
91+
{
92+
var warningCount = GetWarningLogCount();
93+
Assert.AreEqual(0, warningCount);
94+
}
95+
96+
private int GetWarningLogCount() =>
97+
_loggerMock.Invocations
98+
.Count(invocation =>
99+
invocation.Method.Name == nameof(ILogger.Log) &&
100+
invocation.Arguments.OfType<LogLevel>().Any(level => level == LogLevel.Warning));
101+
}

0 commit comments

Comments
 (0)