Compare commits

...
37 Commits
Author SHA1 Message Date
Andy Butland c6ab394947 Merge branch 'release/10.8.11' into v10/dev
# Conflicts:
#	version.json
2025-07-02 06:32:18 +02:00
Kenn JacobsenandGitHub d8f68d2c40 Merge commit from fork 2025-06-24 08:39:17 +02:00
Andy Butland ff88617db0 Bumped version to 10.8.11. 2025-06-05 07:49:09 +02:00
Andy Butland 9f912aea0e Merge branch 'release/10.8.10' into v10/dev
# Conflicts:
#	version.json
2025-05-06 07:15:10 +02:00
Andy Butland ba95c12f09 Resolved failures in unit tests. 2025-05-06 05:37:30 +02:00
Andy ButlandandGitHub 14fbd20665 Merge commit from fork
* Backport user enumeration fix.

* Supress warning on use of .NET 6 with TimeProvider dependency.

* Replace TimeProvider with Stopwatch, as the former isn't tested against .NET 6.0 and generates warnings.

* Remove full path details from exception when requesting a path outside of the physical file system's root.

* Added randomness to login duration.

* Ensured against negative duration.
2025-05-06 05:11:03 +02:00
mole 2d8b5e8786 Use windows agent for nuget push 2025-04-28 10:48:26 +02:00
Andy Butland 747e095178 Bump version to 10.8.10. 2025-04-22 10:06:27 +02:00
Andy Butland 1cfa5a225e Merge branch 'release/10.8.9' into v10/dev
# Conflicts:
#	version.json
2025-03-11 09:20:51 +01:00
7888b9a4ce Merge commit from fork
* Bumped version to 10.8.9.

* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring.

# Conflicts:
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs

* Add HttpPost attributes to backoffice endpoints that should only accept post requests.

* Narrow PermissionQueryString parsing to the releveant UmbracoObjectType

* Add missed update from v10

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
2025-03-11 05:11:08 +01:00
Andy Butland e31582b297 Backport bumped imagesharp to prevent CVE-2025-27598 #18602 2025-03-09 08:58:25 +01:00
Nikolaj Geisle 75cc017a18 Merge branch 'release/10.8.8' into v10/dev
# Conflicts:
#	version.json
2025-01-23 11:08:09 +01:00
d60137e6da Merge commit from fork
* Ensure preview can only be requested with a valid culture code.

* Update src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>

* Restricted to predefined culture codes.

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2025-01-20 14:14:28 +01:00
Andy Butland 9f9c88781a Bumped version to 10.8.8. 2025-01-07 10:08:01 +01:00
c7014e159b Sort manifest file paths alphabetically (#14466)
* Sort manifest file paths alphabetically

* Update src/Umbraco.Infrastructure/Manifest/ManifestParser.cs

Co-authored-by: Ronald Barendse <ronald@barend.se>

---------

Co-authored-by: Ronald Barendse <ronald@barend.se>
2024-11-21 16:19:48 +01:00
Zeegaan 31e1acce67 Merge remote-tracking branch 'origin/release/10.8.7' into v10/dev
# Conflicts:
#	version.json
2024-10-23 08:48:08 +02:00
Sven GeusensandNikolaj Geisle 35c51a029a Prevents XSS when viewing an uploaded SVG
from the media-info and image-preview components.
2024-10-21 12:42:06 +02:00
ElitsaandNikolaj Geisle 8c1128c85b Update the HttpContext's user with the authenticated user's principal 2024-10-21 12:41:42 +02:00
ElitsaandNikolaj Geisle c9021ab2d2 Reduce the time when getRemainingTimeoutSeconds request is made from 30s to 20s, so fewer calls are made 2024-10-21 12:41:18 +02:00
ElitsaandNikolaj Geisle 67a71f8f82 Make sure that the client shows the login screen as close to the server's timout time as possible 2024-10-21 12:41:17 +02:00
Andreas ZerbstandAndreas Zerbst edd0a4a4a9 Updated message pack (#17320)
(cherry picked from commit 11270eaaf5)
2024-10-21 11:46:43 +02:00
Andreas ZerbstandGitHub 11270eaaf5 Updated message pack (#17320) 2024-10-21 11:00:47 +02:00
Bjarke BergandGitHub 2d71b5a63b Updated image sharp to a non vulnerable version (#17290) 2024-10-16 12:16:38 +02:00
Nikolaj Geisle 9bab74d30e Bump version.json 2024-10-07 11:55:34 +02:00
Bjarke Berg 0ee0db8071 Merge remote-tracking branch 'refs/remotes/origin/release/10.8' into v10/dev
# Conflicts:
#	version.json
2024-05-21 08:44:27 +02:00
Bjarke BergandGitHub c17d4e1a60 Merge pull request from GHSA-j74q-mv2c-rxmp 2024-05-17 08:37:51 +02:00
Bjarke Berg fee222daff Bump version 2024-05-03 09:29:37 +02:00
119fde2033 V10: Fix for fallback file upload (#14892) (#15868)
* Fix for fallback file upload (#14892)

* Added check for file type

* Removed unneeded null checks and fixed tabs

* Cleaning

* Cleanups, cleanups, and removal of unneeded null checks

* Reverted removal of relationshipservice

* Revert null check removals (too risky)

---------

Co-authored-by: Ambert van Unen <AvanUnen@ilionx.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>

(cherry picked from commit 0b5d1f8aa6)

* Fix up formatting

---------

Co-authored-by: Ambert van Unen <ambertvu@gmail.com>
2024-04-24 13:44:20 +02:00
Andy ButlandandGitHub 52c21b0fca Updates JSON schema for Umbraco 10 with latest references for Forms and Deploy (#15918) 2024-03-20 13:20:40 +01:00
Bjarke Berg 99d5a7e609 Merge remote-tracking branch 'origin/release/10.8' into v10/dev 2024-03-19 08:43:59 +01:00
JeyandBjarke Berg 7e1d1a1968 Merge pull request from GHSA-552f-97wf-pmpq
Co-authored-by: jey <jey@umbraco.dk>
(cherry picked from commit b743f6a2df)
2024-03-18 08:28:25 +01:00
b743f6a2df Merge pull request from GHSA-552f-97wf-pmpq
Co-authored-by: jey <jey@umbraco.dk>
2024-03-18 08:27:41 +01:00
Bjarke Berg a2511ff09b Fixing locking issues for document type saves. (#15854)
* Added  ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands

* Added Cache Instructions lock, to avoid deadlocks

* Optimized read locks for nucache when only one content type is rebuilt

* Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two

* Avoid breaking changes

* Cosmetic changes

* Take locks if everything is rebuild

* Use same lock in scopes, to avoid potential deadlocks between the two

* Use eager locks in PublishedSnapshotService.cs

* Added timeouts to some of the application locks

* Revert "Use eager locks in PublishedSnapshotService.cs"

This reverts commit 01873aae97.

* Revert "Added Cache Instructions lock, to avoid deadlocks"

This reverts commit e3fca7c12a.

* Use single readlock call to lock many

* Use eager locks for reads

* Eager write locks

* Ignore test of lazy locks

* Unique timeout exception messages

---------

Co-authored-by: kjac <kja@umbraco.dk>
(cherry picked from commit 2c23e67c65)
2024-03-15 14:55:20 +01:00
2c23e67c65 Fixing locking issues for document type saves. (#15854)
* Added  ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands

* Added Cache Instructions lock, to avoid deadlocks

* Optimized read locks for nucache when only one content type is rebuilt

* Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two

* Avoid breaking changes

* Cosmetic changes

* Take locks if everything is rebuild

* Use same lock in scopes, to avoid potential deadlocks between the two

* Use eager locks in PublishedSnapshotService.cs

* Added timeouts to some of the application locks

* Revert "Use eager locks in PublishedSnapshotService.cs"

This reverts commit 01873aae97.

* Revert "Added Cache Instructions lock, to avoid deadlocks"

This reverts commit e3fca7c12a.

* Use single readlock call to lock many

* Use eager locks for reads

* Eager write locks

* Ignore test of lazy locks

* Unique timeout exception messages

---------

Co-authored-by: kjac <kja@umbraco.dk>
2024-03-15 10:56:02 +01:00
Bjarke Berg 8e837d387d Merge remote-tracking branch 'origin/v10/dev' into release/10.8
# Conflicts:
#	version.json
2024-03-14 14:53:42 +01:00
Bjarke Berg 801fb5f885 Bump version 2024-03-14 14:48:09 +01:00
Bjarke BergandGitHub ed517ecd86 Update Imagesharp (#15885) 2024-03-14 14:31:30 +01:00
42 changed files with 840 additions and 272 deletions
+1
View File
@@ -49,4 +49,5 @@
<PropertyGroup>
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
</PropertyGroup>
</Project>
+4
View File
@@ -533,6 +533,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to pre-release feed
steps:
- checkout: none
@@ -559,6 +561,8 @@ stages:
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
jobs:
- job:
pool:
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
displayName: Push to NuGet
steps:
- checkout: none
+49 -52
View File
@@ -4,67 +4,64 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<NoWarn>NU1507</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
<PackageVersion Include="Examine" Version="3.0.1"/>
<PackageVersion Include="Examine.Core" Version="3.0.1"/>
<PackageVersion Include="HtmlAgilityPack" Version="1.11.54"/>
<PackageVersion Include="IPNetwork2" Version="2.6.618"/>
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6"/>
<PackageVersion Include="MailKit" Version="3.2.0"/>
<PackageVersion Include="Markdown" Version="2.2.1"/>
<PackageVersion Include="MessagePack" Version="2.5.129"/>
<PackageVersion Include="Examine" Version="3.0.1" />
<PackageVersion Include="Examine.Core" Version="3.0.1" />
<PackageVersion Include="HtmlAgilityPack" Version="1.11.54" />
<PackageVersion Include="IPNetwork2" Version="2.6.618" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.6" />
<PackageVersion Include="MailKit" Version="3.2.0" />
<PackageVersion Include="Markdown" Version="2.2.1" />
<PackageVersion Include="MessagePack" Version="2.5.187" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.24" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.24" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.24"/>
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0"/>
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.24"/>
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="6.0.24"/>
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1"/>
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0"/>
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="6.0.24"/>
<PackageVersion Include="Microsoft.Data.Sqlite" Version="6.0.24" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="6.0.24" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="6.0.24" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageVersion Include="Microsoft.Extensions.Identity.Stores" Version="6.0.24" />
<PackageVersion Include="MiniProfiler.AspNetCore.Mvc" Version="4.2.22" />
<PackageVersion Include="MiniProfiler.Shared" Version="4.2.22"/>
<PackageVersion Include="ncrontab" Version="3.3.3"/>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageVersion Include="NPoco.SqlServer" Version="5.3.2"/>
<PackageVersion Include="Serilog" Version="2.12.0"/>
<PackageVersion Include="MiniProfiler.Shared" Version="4.2.22" />
<PackageVersion Include="ncrontab" Version="3.3.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NPoco.SqlServer" Version="5.3.2" />
<PackageVersion Include="Serilog" Version="2.12.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="5.0.0" />
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2"/>
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0"/>
<PackageVersion Include="Serilog.Expressions" Version="3.4.1"/>
<PackageVersion Include="Serilog.Extensions.Hosting" Version="4.2.0"/>
<PackageVersion Include="Serilog.Formatting.Compact" Version="1.1.0"/>
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="1.0.5"/>
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0"/>
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0"/>
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2"/>
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.6"/>
<PackageVersion Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Expressions" Version="3.4.1" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="4.2.0" />
<PackageVersion Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageVersion Include="Serilog.Formatting.Compact.Reader" Version="1.0.5" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Map" Version="1.0.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.10" />
<PackageVersion Include="SixLabors.ImageSharp.Web" Version="2.0.2" />
<PackageVersion Include="Smidge.InMemory" Version="4.3.0" />
<PackageVersion Include="Smidge.Nuglify" Version="4.2.1" />
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0"/>
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="6.0.4"/>
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="6.0.0"/>
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0"/>
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0"/>
<PackageVersion Include="System.Runtime.Caching" Version="6.0.0"/>
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085"/>
<PackageVersion Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageVersion Include="System.Security.Cryptography.Pkcs" Version="6.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="6.0.0" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<PackageVersion Include="System.Runtime.Caching" Version="6.0.0" />
<PackageVersion Include="Umbraco.CSharpTest.Net.Collections" Version="14.906.1403.1085" />
<!-- Add dependencies that we force an update to, even that we do not use them explicitly and they seems to be taken from the framework instead of from Nuget -->
<PackageVersion Include="System.Net.Http" Version="4.3.4"/>
<PackageVersion Include="System.Security.Cryptography.Xml" Version="6.0.1"/>
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1"/>
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="6.0.1" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -13,7 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.3.3" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.3" />
<PackageReference Include="Umbraco.Deploy.Core" Version="10.4.0" />
<PackageReference Include="Umbraco.Forms.Core" Version="10.5.4" />
</ItemGroup>
</Project>
@@ -134,9 +134,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id";
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
var i = db.ExecuteScalar<int?>(query, new { id = LockId });
// execute the lock timeout query and the actual query in a single server roundtrip
var i = db.ExecuteScalar<int?>($"{lockTimeoutQuery};{query}", new { id = LockId });
if (i == null)
{
@@ -169,9 +170,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism
const string query =
@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id";
db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";");
var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}";
var i = db.Execute(query, new { id = LockId });
// execute the lock timeout query and the actual query in a single server roundtrip
var i = db.Execute($"{lockTimeoutQuery};{query}", new { id = LockId });
if (i == 0)
{
@@ -154,7 +154,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism
try
{
var i = command.ExecuteNonQuery();
var i = db.ExecuteNonQuery(command);
if (i == 0)
{
+31 -8
View File
@@ -9,6 +9,9 @@ namespace Umbraco.Cms.Core.Cache;
/// </summary>
public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
private static readonly TimeSpan _readLockTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan _writeLockTimeout = TimeSpan.FromSeconds(5);
private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion);
private bool _disposedValue;
@@ -33,7 +36,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
Lazy<object?>? result;
try
{
_locker.EnterReadLock();
if (_locker.TryEnterReadLock(_readLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when getting item");
}
result = MemoryCache.Get(key) as Lazy<object?>; // null if key not found
}
finally
@@ -195,7 +201,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing item");
}
if (MemoryCache[key] == null)
{
return;
@@ -223,8 +232,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
var isInterface = type.IsInterface;
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by type");
}
// ToArray required to remove
foreach (var key in MemoryCache
.Where(x =>
@@ -259,7 +270,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing by generic type");
}
Type typeOfT = typeof(T);
var isInterface = typeOfT.IsInterface;
@@ -296,7 +310,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing generic type with predicate");
}
Type typeOfT = typeof(T);
var isInterface = typeOfT.IsInterface;
@@ -338,7 +355,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
{
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cache when clearing with prefix");
}
// ToArray required to remove
foreach (var key in MemoryCache
@@ -365,7 +385,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable
try
{
_locker.EnterWriteLock();
if (_locker.TryEnterWriteLock(_writeLockTimeout) is false)
{
throw new TimeoutException("Timeout exceeded to the memory cach when clearing by regex");
}
// ToArray required to remove
foreach (var key in MemoryCache
@@ -2,6 +2,7 @@
// See LICENSE for more details.
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Core.Configuration.Models;
@@ -24,6 +25,8 @@ public class SecuritySettings
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
/// <summary>
/// Gets or sets a value indicating whether to keep the user logged in.
@@ -109,4 +112,26 @@ public class SecuritySettings
[Obsolete("Use ContentSettings.AllowEditFromInvariant instead")]
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
/// <summary>
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The default duration (in milliseconds) of failed login attempts.
/// </value>
/// <remarks>
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
/// However, if no successful logins have occurred, this value is used as the default duration.
/// </remarks>
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
/// <summary>
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
/// </summary>
/// <value>
/// The minimum duration (in milliseconds) of failed login attempts.
/// </value>
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
}
+1 -1
View File
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
}
/// <summary>
+24
View File
@@ -50,4 +50,28 @@ public class WebPath
return sb.ToString();
}
/// <summary>
/// Determines whether the provided web path is well-formed according to the specified UriKind.
/// </summary>
/// <param name="webPath">The web path to check. This can be null.</param>
/// <param name="uriKind">The kind of Uri (Absolute, Relative, or RelativeOrAbsolute).</param>
/// <returns>
/// true if <paramref name="webPath"/> is well-formed; otherwise, false.
/// </returns>
public static bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
{
if (string.IsNullOrWhiteSpace(webPath))
{
return false;
}
if (webPath.StartsWith("//"))
{
return uriKind is not UriKind.Relative;
}
return Uri.IsWellFormedUriString(webPath, uriKind);
}
}
+118
View File
@@ -0,0 +1,118 @@
using System.Diagnostics;
namespace Umbraco.Cms.Core;
/// <summary>
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
/// </summary>
public sealed class TimedScope : IDisposable, IAsyncDisposable
{
private readonly TimeSpan _duration;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly Stopwatch _stopwatch;
/// <summary>
/// Gets the elapsed time.
/// </summary>
/// <value>
/// The elapsed time.
/// </value>
public TimeSpan Elapsed => _stopwatch.Elapsed;
/// <summary>
/// Gets the remaining time.
/// </summary>
/// <value>
/// The remaining time.
/// </value>
public TimeSpan Remaining
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
public TimedScope(long millisecondsDuration)
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope"/> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
public TimedScope(TimeSpan duration)
: this(duration, new CancellationTokenSource())
{ }
/// <summary>
/// Initializes a new instance of the <see cref="TimedScope" /> class.
/// </summary>
/// <param name="duration">The duration the scope should at least take.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
: this(duration, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{ }
private TimedScope(TimeSpan duration, CancellationTokenSource cancellationTokenSource)
{
_duration = duration;
_cancellationTokenSource = cancellationTokenSource;
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
/// <summary>
/// Cancels the timed scope.
/// </summary>
public void Cancel()
=> _cancellationTokenSource.Cancel();
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <remarks>
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public void Dispose()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
Thread.Sleep(remaining);
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
/// </summary>
/// <returns>
/// A task that represents the asynchronous dispose operation.
/// </returns>
/// <remarks>
/// This will delay using <see cref="Task.Delay(TimeSpan, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
/// </remarks>
public async ValueTask DisposeAsync()
{
if (_cancellationTokenSource.IsCancellationRequested is false &&
TryGetRemaining(out TimeSpan remaining))
{
await Task.Delay(remaining, _cancellationTokenSource.Token).ConfigureAwait(false);
}
}
private bool TryGetRemaining(out TimeSpan remaining)
{
remaining = _duration.Subtract(Elapsed);
return remaining > TimeSpan.Zero;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Umbraco.Cms.Core</PackageId>
<Title>Umbraco CMS - Core</Title>
@@ -250,6 +250,11 @@ public class ManifestParser : IManifestParser
return Array.Empty<string>();
}
return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
var files = Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
// Ensure a consistent, alphabetical sorting of paths, because this is not guaranteed to be the same between file systems or OSes
Array.Sort(files);
return files;
}
}
@@ -1,3 +1,4 @@
using System.Data.Common;
using NPoco;
using Umbraco.Cms.Infrastructure.Migrations.Install;
@@ -33,4 +34,7 @@ public interface IUmbracoDatabase : IDatabase
bool IsUmbracoInstalled();
DatabaseSchemaResult ValidateSchema();
/// <returns>The number of rows affected.</returns>
int ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery();
}
@@ -224,6 +224,14 @@ public class UmbracoDatabase : Database, IUmbracoDatabase
return databaseSchemaValidationResult ?? new DatabaseSchemaResult();
}
public int ExecuteNonQuery(DbCommand command)
{
OnExecutingCommand(command);
var i = command.ExecuteNonQuery();
OnExecutedCommand(command);
return i;
}
/// <summary>
/// Returns true if Umbraco database tables are detected to be installed
/// </summary>
+12 -14
View File
@@ -23,10 +23,9 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private readonly bool _autoComplete;
private readonly CoreDebugSettings _coreDebugSettings;
private readonly object _dictionaryLocker;
private readonly IEventAggregator _eventAggregator;
private readonly IsolationLevel _isolationLevel;
private readonly object _lockQueueLocker = new();
private readonly object _locker = new();
private readonly ILogger<Scope> _logger;
private readonly MediaFileManager _mediaFileManager;
private readonly RepositoryCacheMode _repositoryCacheMode;
@@ -87,7 +86,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping
_scopeFileSystem = scopeFileSystems;
_autoComplete = autoComplete;
Detachable = detachable;
_dictionaryLocker = new object();
#if DEBUG_SCOPES
_scopeProvider.RegisterScope(this);
@@ -562,7 +560,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
DisposeLastScope();
}
lock (_lockQueueLocker)
lock (_locker)
{
_queuedLocks?.Clear();
}
@@ -573,24 +571,24 @@ namespace Umbraco.Cms.Infrastructure.Scoping
public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds);
public void ReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds);
public void EagerReadLock(TimeSpan timeout, int lockId) =>
EagerReadLockInner(InstanceId, timeout, lockId);
/// <inheritdoc />
public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId);
public void ReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(InstanceId, timeout, lockId);
public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
/// <inheritdoc />
public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds);
public void WriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds);
public void EagerWriteLock(TimeSpan timeout, int lockId) =>
EagerWriteLockInner(InstanceId, timeout, lockId);
/// <inheritdoc />
public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId);
public void WriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(InstanceId, timeout, lockId);
/// <summary>
/// Used for testing. Ensures and gets any queued read locks.
@@ -659,7 +657,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks?.Count > 0)
{
@@ -970,7 +968,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
_readLocksDictionary?.Remove(instanceId);
_writeLocksDictionary?.Remove(instanceId);
@@ -1045,7 +1043,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds)
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks == null)
{
@@ -1061,7 +1059,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId)
{
lock (_lockQueueLocker)
lock (_locker)
{
if (_queuedLocks == null)
{
@@ -1088,7 +1086,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
foreach (var lockId in lockIds)
{
@@ -1122,7 +1120,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping
}
else
{
lock (_dictionaryLocker)
lock (_locker)
{
foreach (var lockId in lockIds)
{
@@ -134,8 +134,8 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
/// <inheritdoc />
public override async Task<bool> CheckPasswordAsync(TUser user, string? password)
{
// we cannot proceed if the user passed in does not have an identity
if (user.HasIdentity == false)
// we cannot proceed if the user passed in does not have an identity, or if no password is provided.
if (user.HasIdentity == false || password is null)
{
return false;
}
@@ -252,7 +252,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
public async Task<bool> ValidateCredentialsAsync(string username, string password)
{
TUser user = await FindByNameAsync(username);
if (user == null)
{
return false;
@@ -263,7 +263,7 @@ public abstract class UmbracoUserManager<TUser, TPasswordConfig> : UserManager<T
throw new NotSupportedException("The current user store does not implement " +
typeof(IUserPasswordStore<>));
}
var result = await VerifyPasswordAsync(userPasswordStore, user, password);
return result == PasswordVerificationResult.Success || result == PasswordVerificationResult.SuccessRehashNeeded;
@@ -27,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache;
/// </remarks>
public class ContentStore
{
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
// TODO: collection trigger (ok for now)
// see SnapDictionary notes
private const long CollectMinGenDelta = 8;
@@ -330,7 +332,12 @@ public class ContentStore
throw new InvalidOperationException("Recursive locks not allowed");
}
Monitor.Enter(_wlocko, ref lockInfo.Taken);
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
if (Monitor.IsEntered(_wlocko) is false)
{
throw new TimeoutException("Could not enter monitor before timeout in content store");
}
lock (_rlocko)
{
@@ -127,9 +127,25 @@ public class NuCacheContentService : RepositoryService, INuCacheContentService
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped))
{
scope.ReadLock(Constants.Locks.ContentTree);
scope.ReadLock(Constants.Locks.MediaTree);
scope.ReadLock(Constants.Locks.MemberTree);
if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null)
{
scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree);
}
if (contentTypeIds is not null && contentTypeIds.Any())
{
scope.ReadLock(Constants.Locks.ContentTree);
}
if (mediaTypeIds is not null && mediaTypeIds.Any())
{
scope.ReadLock(Constants.Locks.MediaTree);
}
if (memberTypeIds is not null && memberTypeIds.Any())
{
scope.ReadLock(Constants.Locks.MemberTree);
}
_repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds);
@@ -9,6 +9,8 @@ public class SnapDictionary<TKey, TValue>
where TValue : class
where TKey : notnull
{
private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30);
// minGenDelta to be adjusted
// we may want to throttle collects even if delta is reached
// we may want to force collect if delta is not reached but very old
@@ -198,7 +200,12 @@ public class SnapDictionary<TKey, TValue>
throw new InvalidOperationException("Recursive locks not allowed");
}
Monitor.Enter(_wlocko, ref lockInfo.Taken);
Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken);
if (Monitor.IsEntered(_wlocko) is false)
{
throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary");
}
lock (_rlocko)
{
@@ -19,6 +19,8 @@ public class
{
private readonly ContentPermissions _contentPermissions;
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document;
/// <summary>
/// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class.
/// </summary>
@@ -47,7 +49,11 @@ public class
return Task.FromResult(true);
}
var argument = routeVal.ToString();
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
// It's the first one that'll be processed by the protected method so we should verify that.
var argument = routeVal.Count == 1
? routeVal.ToString()
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
if (!TryParseNodeId(argument, out nodeId))
{
@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
{
private readonly MediaPermissions _mediaPermissions;
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media;
/// <summary>
/// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class.
/// </summary>
@@ -44,7 +46,11 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
return Task.FromResult(true);
}
var argument = routeVal.ToString();
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
// It's the first one that'll be processed by the protected method so we should verify that.
var argument = routeVal.Count == 1
? routeVal.ToString()
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
if (!TryParseNodeId(argument, out var nodeId))
{
@@ -49,12 +49,18 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
/// </summary>
protected IEntityService EntityService { get; set; }
/// <summary>
/// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works,
/// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance.
/// </summary>
protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown;
/// <summary>
/// Attempts to parse a node ID from a string representation found in a querystring value.
/// </summary>
/// <param name="argument">Querystring value.</param>
/// <param name="nodeId">Output parsed Id.</param>
/// <returns>True of node ID could be parased, false it not.</returns>
/// <returns>True of node ID could be parsed, false it not.</returns>
protected bool TryParseNodeId(string argument, out int nodeId)
{
// If the argument is an int, it will parse and can be assigned to nodeId.
@@ -75,7 +81,7 @@ public abstract class PermissionsQueryStringHandler<T> : MustSatisfyRequirementA
if (Guid.TryParse(argument, out Guid key))
{
nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result;
nodeId = EntityService.GetId(key, KeyParsingFilterType).Result;
return true;
}
@@ -74,6 +74,9 @@ public class AuthenticationController : UmbracoApiControllerBase
private readonly IUserService _userService;
private readonly WebRoutingSettings _webRoutingSettings;
private const int FailedLoginDurationRandomOffsetInMilliseconds = 100;
private static long? _loginDurationAverage;
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
[ActivatorUtilitiesConstructor]
public AuthenticationController(
@@ -129,12 +132,17 @@ public class AuthenticationController : UmbracoApiControllerBase
AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
public IDictionary<string, object> GetPasswordConfig(int userId)
{
if (HttpContext.HasActivePasswordResetFlowSession(userId))
{
return _passwordConfiguration.GetConfiguration();
}
Attempt<int> currentUserId =
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
return _passwordConfiguration.GetConfiguration(
currentUserId.Success
? currentUserId.Result != userId
: true);
return currentUserId.Success
? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId)
: new Dictionary<string, object>();
}
/// <summary>
@@ -342,47 +350,85 @@ public class AuthenticationController : UmbracoApiControllerBase
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
{
HttpContext.EndPasswordResetFlowSession();
// Start a timed scope to ensure failed responses return is a consistent time
await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None);
// Sign the user in with username/password, this also gives a chance for developers to
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
SignInResult result = await _signInManager.PasswordSignInAsync(
loginModel.Username, loginModel.Password, true, true);
if (result.Succeeded)
if (result.Succeeded is false)
{
// return the user detail
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
}
BackOfficeIdentityUser? user = await _userManager.FindByNameAsync(loginModel.Username.Trim());
if (result.RequiresTwoFactor)
{
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
if (twofactorView.IsNullOrWhiteSpace())
if (user is not null &&
await _userManager.CheckPasswordAsync(user, loginModel.Password))
{
return new ValidationErrorResult(
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
timedScope.Cancel();
if (result.RequiresTwoFactor)
{
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
if (twofactorView.IsNullOrWhiteSpace())
{
return new ValidationErrorResult(
$"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
}
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
{
StatusCode = StatusCodes.Status402PaymentRequired
};
return verifyResponse;
}
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
}
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
// create a with information to display a custom two factor send code view
var verifyResponse =
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
{
StatusCode = StatusCodes.Status402PaymentRequired
};
return verifyResponse;
// Return BadRequest (400), we don't want to return a 401 because that get's intercepted
// by our angular helper because it thinks that we need to re-perform the request once we are
// authorized and we don't want to return a 403 because angular will show a warning message indicating
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
return BadRequest();
}
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
// Set initial or update average (successful) login duration
_loginDurationAverage = _loginDurationAverage is long average
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
: (long)timedScope.Elapsed.TotalMilliseconds;
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
// by our angular helper because it thinks that we need to re-perform the request once we are
// authorized and we don't want to return a 403 because angular will show a warning message indicating
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
return BadRequest();
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
timedScope.Cancel();
// Return the user detail
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
}
private long GetLoginDuration()
{
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds);
var random = new Random();
var randomDelay = random.Next(-FailedLoginDurationRandomOffsetInMilliseconds, FailedLoginDurationRandomOffsetInMilliseconds);
loginDuration += randomDelay;
// Just be sure we don't get a negative number - possible if someone has configured a very low UserMinimumFailedLoginDurationInMilliseconds value.
if (loginDuration < 0)
{
loginDuration = 0;
}
return loginDuration;
}
/// <summary>
@@ -401,6 +447,8 @@ public class AuthenticationController : UmbracoApiControllerBase
return BadRequest();
}
HttpContext.EndPasswordResetFlowSession();
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration
@@ -554,6 +602,8 @@ public class AuthenticationController : UmbracoApiControllerBase
[AllowAnonymous]
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
{
HttpContext.EndPasswordResetFlowSession();
BackOfficeIdentityUser? identityUser =
await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
@@ -370,6 +370,8 @@ public class BackOfficeController : UmbracoController
var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode);
if (result)
{
HttpContext.StartPasswordResetFlowSession(userId);
//Add a flag and redirect for it to be displayed
TempData[ViewDataExtensions.TokenPasswordResetCode] =
_jsonSerializer.Serialize(
@@ -196,6 +196,7 @@ public class ContentController : ContentControllerBase
/// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to
/// update
/// </remarks>
[HttpPost]
public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions(
UserGroupPermissionsSave saveModel)
{
@@ -842,6 +843,7 @@ public class ContentController : ContentControllerBase
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
[FileUploadCleanupFilter]
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
[HttpPost]
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
{
@@ -879,6 +881,7 @@ public class ContentController : ContentControllerBase
[FileUploadCleanupFilter]
[ContentSaveValidation]
[OutgoingEditorModelEvent]
[HttpPost]
public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave(
[ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
{
@@ -1960,6 +1963,7 @@ public class ContentController : ContentControllerBase
/// does not have Publish access to this node.
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
[HttpPost]
public IActionResult PostPublishById(int id)
{
IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id));
@@ -1991,6 +1995,7 @@ public class ContentController : ContentControllerBase
/// does not have Publish access to this node.
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
[HttpPost]
public IActionResult PostPublishByIdAndCulture(PublishContent model)
{
var languageCount = _allLangs.Value.Count();
@@ -2114,6 +2119,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="sorted"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
{
if (sorted == null)
@@ -2165,6 +2171,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="move"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult?> PostMove(MoveOrCopy move)
{
// Authorize...
@@ -2199,6 +2206,7 @@ public class ContentController : ContentControllerBase
/// </summary>
/// <param name="copy"></param>
/// <returns></returns>
[HttpPost]
public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy)
{
// Authorize...
@@ -2238,6 +2246,7 @@ public class ContentController : ContentControllerBase
/// <param name="model">The content and variants to unpublish</param>
/// <returns></returns>
[OutgoingEditorModelEvent]
[HttpPost]
public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model)
{
IContent? foundContent = _contentService.GetById(model.Id);
@@ -2960,6 +2969,7 @@ public class ContentController : ContentControllerBase
return notifications;
}
[HttpPost]
public IActionResult PostNotificationOptions(
int contentId,
[FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
@@ -123,7 +124,7 @@ public class ImagesController : UmbracoAuthorizedApiController
private bool IsAllowed(string encodedImagePath)
{
if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
{
return true;
}
@@ -189,7 +189,7 @@ public class MediaController : ContentControllerBase
if (mapped is not null)
{
//remove the listview app if it exists
// remove the listview app if it exists
mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList();
}
@@ -205,7 +205,7 @@ public class MediaController : ContentControllerBase
var apps = new List<ContentApp>
{
ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "media",
Constants.DataTypes.DefaultMediaListView)
Constants.DataTypes.DefaultMediaListView)
};
apps[0].Active = true;
var display = new MediaItemDisplay
@@ -238,7 +238,8 @@ public class MediaController : ContentControllerBase
if (foundMedia == null)
{
HandleContentNotFound(id);
//HandleContentNotFound will throw an exception
// HandleContentNotFound will throw an exception
return null;
}
@@ -306,8 +307,8 @@ public class MediaController : ContentControllerBase
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildFolders(int id, int pageNumber = 1,
int pageSize = 1000)
{
//Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
//if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
// Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it...
// if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
var folderTypes = _mediaTypeService
.GetAll()
.Where(x => x.Alias.EndsWith("Folder"))
@@ -320,7 +321,8 @@ public class MediaController : ContentControllerBase
}
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out long total,
//lookup these content types
// lookup these content types
_sqlContext.Query<IMedia>().Where(x => folderTypes.Contains(x.ContentTypeId)),
Ordering.By("Name"));
@@ -336,6 +338,7 @@ public class MediaController : ContentControllerBase
/// </summary>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>))]
public IEnumerable<ContentItemBasic<ContentPropertyBasic>> GetRootMedia() =>
// TODO: Add permissions check!
_mediaService.GetRootMedia()?
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull() ??
@@ -357,7 +360,7 @@ public class MediaController : ContentControllerBase
return HandleContentNotFound(id);
}
//if the current item is in the recycle bin
// if the current item is in the recycle bin
if (foundMedia.Trashed == false)
{
Attempt<OperationResult?> moveResult = _mediaService.MoveToRecycleBin(foundMedia,
@@ -385,12 +388,15 @@ public class MediaController : ContentControllerBase
/// </summary>
/// <param name="move"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostMove(MoveOrCopy move)
{
// Authorize...
var requirement = new MediaPermissionsResourceRequirement();
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(User,
new MediaPermissionsResource(_mediaService.GetById(move.Id)), requirement);
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeAsync(
User,
new MediaPermissionsResource(_mediaService.GetById(move.Id)),
requirement);
if (!authorizationResult.Succeeded)
{
return Forbid();
@@ -403,18 +409,20 @@ public class MediaController : ContentControllerBase
return convertToActionResult.Convert();
}
var destinationParentID = move.ParentId;
var sourceParentID = toMove?.ParentId;
var destinationParentId = move.ParentId;
var sourceParentId = toMove?.ParentId;
var moveResult = toMove is null
? false
: _mediaService.Move(toMove, move.ParentId,
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
if (sourceParentID == destinationParentID)
if (sourceParentId == destinationParentId)
{
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification("",
_localizedTextService.Localize("media", "moveToSameFolderFailed"), NotificationStyle.Error)));
return ValidationProblem(new SimpleNotificationModel(new BackOfficeNotification(
string.Empty,
_localizedTextService.Localize("media", "moveToSameFolderFailed"),
NotificationStyle.Error)));
}
if (moveResult == false)
@@ -432,12 +440,13 @@ public class MediaController : ContentControllerBase
[FileUploadCleanupFilter]
[MediaItemSaveValidation]
[OutgoingEditorModelEvent]
[HttpPost]
public ActionResult<MediaItemDisplay?>? PostSave(
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
{
//Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
//To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
//uploaded files to being *only* the actual file name (as it should be).
// Recent versions of IE/Edge may send in the full client side file path instead of just the file name.
// To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all
// uploaded files to being *only* the actual file name (as it should be).
if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any())
{
foreach (ContentPropertyFile file in contentItem.UploadedFiles)
@@ -446,14 +455,14 @@ public class MediaController : ContentControllerBase
}
}
//If we've reached here it means:
// If we've reached here it means:
// * Our model has been bound
// * and validated
// * any file attachments have been saved to their temporary location for us to use
// * we have a reference to the DTO object and the persisted object
// * Permissions are valid
//Don't update the name if it is empty
// Don't update the name if it is empty
if (contentItem.Name.IsNullOrWhiteSpace() == false && contentItem.PersistedContent is not null)
{
contentItem.PersistedContent.Name = contentItem.Name;
@@ -466,14 +475,14 @@ public class MediaController : ContentControllerBase
(save, property, v) => property?.SetValue(v), //set prop val
null); // media are all invariant
//we will continue to save if model state is invalid, however we cannot save if critical data is missing.
//TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
// we will continue to save if model state is invalid, however we cannot save if critical data is missing.
// TODO: Allowing media to be saved when it is invalid is odd - media doesn't have a publish phase so suddenly invalid data is allowed to be 'live'
if (!ModelState.IsValid)
{
//check for critical data validation issues, we can't continue saving if this data is invalid
// check for critical data validation issues, we can't continue saving if this data is invalid
if (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(contentItem))
{
//ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue!
// add the model state to the outgoing object and throw validation response
MediaItemDisplay? forDisplay = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
return ValidationProblem(forDisplay, ModelState);
@@ -485,20 +494,20 @@ public class MediaController : ContentControllerBase
return null;
}
//save the item
// save the item
Attempt<OperationResult?> saveStatus = _mediaService.Save(contentItem.PersistedContent,
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1);
//return the updated model
// return the updated model
MediaItemDisplay? display = _umbracoMapper.Map<MediaItemDisplay>(contentItem.PersistedContent);
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
if (!ModelState.IsValid)
{
return ValidationProblem(display, ModelState, StatusCodes.Status403Forbidden);
}
//put the correct msgs in
// put the correct msgs in
switch (contentItem.Action)
{
case ContentSaveAction.Save:
@@ -513,7 +522,7 @@ public class MediaController : ContentControllerBase
{
AddCancelMessage(display);
//If the item is new and the operation was cancelled, we need to return a different
// If the item is new and the operation was cancelled, we need to return a different
// status code so the UI can handle it since it won't be able to redirect since there
// is no Id to redirect to!
if (saveStatus.Result?.Result == OperationResultType.FailedCancelledByEvent &&
@@ -547,6 +556,7 @@ public class MediaController : ContentControllerBase
/// </summary>
/// <param name="sorted"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
{
if (sorted == null)
@@ -554,7 +564,7 @@ public class MediaController : ContentControllerBase
return NotFound();
}
//if there's nothing to sort just return ok
// if there's nothing to sort just return ok
if (sorted.IdSortOrder?.Length == 0)
{
return Ok();
@@ -592,10 +602,11 @@ public class MediaController : ContentControllerBase
}
}
[HttpPost]
public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder)
{
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true);
if (!(parentIdResult?.Result is null))
if (parentIdResult?.Result is not null)
{
return new ActionResult<MediaItemDisplay?>(parentIdResult.Result);
}
@@ -625,6 +636,7 @@ public class MediaController : ContentControllerBase
/// <remarks>
/// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data.
/// </remarks>
[HttpPost]
public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder,
[FromForm] string contentTypeAlias, List<IFormFile> file)
{
@@ -632,15 +644,15 @@ public class MediaController : ContentControllerBase
//ensure it exists
Directory.CreateDirectory(root);
//must have a file
// must have a file
if (file is null || file.Count == 0)
{
return NotFound("No file was uploaded");
}
//get the string json from the request
// get the string json from the request
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(currentFolder, true);
if (!(parentIdResult?.Result is null))
if (parentIdResult?.Result is not null)
{
return parentIdResult.Result;
}
@@ -653,7 +665,7 @@ public class MediaController : ContentControllerBase
var tempFiles = new PostedFiles();
//in case we pass a path with a folder in it, we will create it and upload media to it.
// in case we pass a path with a folder in it, we will create it and upload media to it.
if (!string.IsNullOrEmpty(path))
{
if (!IsFolderCreationAllowedHere(parentId.Value))
@@ -669,16 +681,16 @@ public class MediaController : ContentControllerBase
var folderName = folders[i];
IMedia? folderMediaItem;
//if uploading directly to media root and not a subfolder
// if uploading directly to media root and not a subfolder
if (parentId == Constants.System.Root)
{
//look for matching folder
// look for matching folder
folderMediaItem =
_mediaService.GetRootMedia()?.FirstOrDefault(x =>
x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder);
if (folderMediaItem == null)
{
//if null, create a folder
// if null, create a folder
folderMediaItem =
_mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder);
_mediaService.Save(folderMediaItem);
@@ -686,10 +698,10 @@ public class MediaController : ContentControllerBase
}
else
{
//get current parent
// get current parent
IMedia? mediaRoot = _mediaService.GetById(parentId.Value);
//if the media root is null, something went wrong, we'll abort
// if the media root is null, something went wrong, we'll abort
if (mediaRoot == null)
{
return Problem(
@@ -697,7 +709,7 @@ public class MediaController : ContentControllerBase
" returned null");
}
//look for matching folder
// look for matching folder
folderMediaItem = FindInChildren(mediaRoot.Id, folderName, Constants.Conventions.MediaTypes.Folder);
if (folderMediaItem == null)
@@ -709,7 +721,7 @@ public class MediaController : ContentControllerBase
}
}
//set the media root to the folder id so uploaded files will end there.
// set the media root to the folder id so uploaded files will end there.
parentId = folderMediaItem.Id;
}
}
@@ -749,7 +761,7 @@ public class MediaController : ContentControllerBase
}
}
//Only set the permission-based mediaType if we only allow 1 specific file under this parent.
// Only set the permission-based mediaType if we only allow 1 specific file under this parent.
if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
{
mediaTypeAlias = mediaTypeItem.Alias;
@@ -762,7 +774,7 @@ public class MediaController : ContentControllerBase
allowedContentTypes.UnionWith(typesAllowedAtRoot);
}
//get the files
// get the files
foreach (IFormFile formFile in file)
{
var fileName = formFile.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
@@ -821,6 +833,11 @@ public class MediaController : ContentControllerBase
continue;
}
if (allowedContentTypes.Any(x => x.Alias == mediaTypeItem.Alias) == false)
{
continue;
}
mediaTypeAlias = mediaTypeItem.Alias;
break;
}
@@ -866,8 +883,8 @@ public class MediaController : ContentControllerBase
IMedia createdMediaItem = _mediaService.CreateMedia(mediaItemName, parentId.Value, mediaTypeAlias,
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
createdMediaItem.SetValue(_mediaFileManager, _mediaUrlGenerators, _shortStringHelper,
_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, fileName, stream);
Attempt<OperationResult?> saveResult = _mediaService.Save(createdMediaItem,
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1);
@@ -878,13 +895,13 @@ public class MediaController : ContentControllerBase
}
}
//Different response if this is a 'blueimp' request
// Different response if this is a 'blueimp' request
if (HttpContext.Request.Query.Any(x => x.Key == "origin"))
{
KeyValuePair<string, StringValues> origin = HttpContext.Request.Query.First(x => x.Key == "origin");
if (origin.Value == "blueimp")
{
return new JsonResult(tempFiles); //Don't output the angular xsrf stuff, blue imp doesn't like that
return new JsonResult(tempFiles); // Don't output the angular xsrf stuff, blue imp doesn't like that
}
}
@@ -923,7 +940,11 @@ public class MediaController : ContentControllerBase
var total = long.MaxValue;
while (page * pageSize < total)
{
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(mediaId, page++, pageSize, out total,
IEnumerable<IMedia> children = _mediaService.GetPagedChildren(
mediaId,
page++,
pageSize,
out total,
_sqlContext.Query<IMedia>().Where(x => x.Name == nameToFind));
IMedia? match = children.FirstOrDefault(c => c.ContentType.Alias == contentTypeAlias);
if (match != null)
@@ -946,14 +967,13 @@ public class MediaController : ContentControllerBase
/// <returns></returns>
private async Task<ActionResult<int?>?> GetParentIdAsIntAsync(string? parentId, bool validatePermissions)
{
// test for udi
if (UdiParser.TryParse(parentId, out GuidUdi? parentUdi))
{
parentId = parentUdi?.Guid.ToString();
}
//if it's not an INT then we'll check for GUID
// if it's not an INT then we'll check for GUID
if (int.TryParse(parentId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intParentId) == false)
{
// if a guid then try to look up the entity
@@ -977,7 +997,7 @@ public class MediaController : ContentControllerBase
}
// Authorize...
//ensure the user has access to this folder by parent id!
// ensure the user has access to this folder by parent id!
if (validatePermissions)
{
var requirement = new MediaPermissionsResourceRequirement();
@@ -1018,14 +1038,14 @@ public class MediaController : ContentControllerBase
if (model.ParentId < 0)
{
//cannot move if the content item is not allowed at the root unless there are
//none allowed at root (in which case all should be allowed at root)
// cannot move if the content item is not allowed at the root unless there are
// none allowed at root (in which case all should be allowed at root)
IMediaTypeService mediaTypeService = _mediaTypeService;
if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot))
{
var notificationModel = new SimpleNotificationModel();
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedAtRoot"),
"");
string.Empty);
return ValidationProblem(notificationModel);
}
}
@@ -1037,7 +1057,7 @@ public class MediaController : ContentControllerBase
return NotFound();
}
//check if the item is allowed under this one
// check if the item is allowed under this one
IMediaType? parentContentType = _mediaTypeService.Get(parent.ContentTypeId);
if (parentContentType?.AllowedContentTypes?.Select(x => x.Id).ToArray()
.Any(x => x.Value == toMove.ContentType.Id) == false)
@@ -1049,12 +1069,12 @@ public class MediaController : ContentControllerBase
}
// Check on paths
if (string.Format(",{0},", parent.Path)
.IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1)
if ($",{parent.Path},"
.IndexOf($",{toMove.Id},", StringComparison.Ordinal) > -1)
{
var notificationModel = new SimpleNotificationModel();
notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath"),
"");
string.Empty);
return ValidationProblem(notificationModel);
}
}
@@ -1110,7 +1130,8 @@ public class MediaController : ContentControllerBase
/// Returns the child media objects - using the entity INT id
/// </summary>
[FilterAllowedOutgoingMedia(typeof(IEnumerable<ContentItemBasic<ContentPropertyBasic>>), "Items")]
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(int id,
public PagedResult<ContentItemBasic<ContentPropertyBasic>> GetChildren(
int id,
int pageNumber = 0,
int pageSize = 0,
string orderBy = "SortOrder",
@@ -1118,7 +1139,7 @@ public class MediaController : ContentControllerBase
bool orderBySystemField = true,
string filter = "")
{
//if a request is made for the root node data but the user's start node is not the default, then
// if a request is made for the root node data but the user's start node is not the default, then
// we need to return their start nodes
if (id == Constants.System.Root && UserStartNodes.Length > 0 &&
UserStartNodes.Contains(Constants.System.Root) == false)
@@ -1148,7 +1169,6 @@ public class MediaController : ContentControllerBase
}
// else proceed as usual
long totalChildren;
List<IMedia> children;
if (pageNumber > 0 && pageSize > 0)
@@ -1156,7 +1176,7 @@ public class MediaController : ContentControllerBase
IQuery<IMedia>? queryFilter = null;
if (filter.IsNullOrWhiteSpace() == false)
{
//add the default text filter
// add the default text filter
queryFilter = _sqlContext.Query<IMedia>()
.Where(x => x.Name != null)
.Where(x => x.Name!.Contains(filter));
@@ -1164,14 +1184,16 @@ public class MediaController : ContentControllerBase
children = _mediaService
.GetPagedChildren(
id, pageNumber - 1, pageSize,
id,
pageNumber - 1,
pageSize,
out totalChildren,
queryFilter,
Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)).ToList();
}
else
{
//better to not use this without paging where possible, currently only the sort dialog does
// better to not use this without paging where possible, currently only the sort dialog does
children = _mediaService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList();
totalChildren = children.Count;
}
@@ -1184,7 +1206,7 @@ public class MediaController : ContentControllerBase
var pagedResult = new PagedResult<ContentItemBasic<ContentPropertyBasic>>(totalChildren, pageNumber, pageSize)
{
Items = children
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
.Select(_umbracoMapper.Map<IMedia, ContentItemBasic<ContentPropertyBasic>>).WhereNotNull()
};
return pagedResult;
@@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
@@ -11,6 +12,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -129,6 +131,11 @@ public class PreviewController : Controller
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
public ActionResult Frame(int id, string culture)
{
if (ValidateProvidedCulture(culture) is false)
{
throw new InvalidOperationException($"Could not recognise the provided culture: {culture}");
}
EnterPreview(id);
// use a numeric URL because content may not be in cache and so .Url would fail
@@ -137,6 +144,28 @@ public class PreviewController : Controller
return RedirectPermanent($"../../{id}{query}");
}
private static bool ValidateProvidedCulture(string culture)
{
if (string.IsNullOrEmpty(culture))
{
return true;
}
// We can be confident the backoffice will have provided a valid culture in linking to the
// preview, so we don't need to check that the culture matches an Umbraco language.
// We are only concerned here with protecting against XSS attacks from a fiddled preview
// URL, so we can just confirm we have a valid culture.
try
{
CultureInfo.GetCultureInfo(culture, true);
return true;
}
catch (CultureNotFoundException)
{
return false;
}
}
public ActionResult? EnterPreview(int id)
{
IUser? user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
@@ -152,8 +181,7 @@ public class PreviewController : Controller
// Expire Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website.
_cookieManager.ExpireCookie(Constants.Web.AcceptPreviewCookieName);
if (Uri.IsWellFormedUriString(redir, UriKind.Relative)
&& redir.StartsWith("//") == false
if (WebPath.IsWellFormedWebPath(redir, UriKind.Relative)
&& Uri.TryCreate(redir, UriKind.Relative, out Uri? url))
{
return Redirect(url.ToString());
@@ -5,9 +5,20 @@ namespace Umbraco.Extensions;
public static class HttpContextExtensions
{
private const string PasswordResetFlowSessionKey = nameof(PasswordResetFlowSessionKey);
public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors)
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors;
public static BackOfficeExternalLoginProviderErrors? GetExternalLoginProviderErrors(this HttpContext httpContext)
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors;
internal static void StartPasswordResetFlowSession(this HttpContext httpContext, int userId)
=> httpContext.Session.SetInt32(PasswordResetFlowSessionKey, userId);
internal static void EndPasswordResetFlowSession(this HttpContext httpContext)
=> httpContext.Session.Remove(PasswordResetFlowSessionKey);
internal static bool HasActivePasswordResetFlowSession(this HttpContext httpContext, int userId)
=> httpContext.Session.GetInt32(PasswordResetFlowSessionKey) == userId;
}
@@ -59,6 +59,14 @@ public static class HttpContextExtensions
await httpContext.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType);
}
// Update the HttpContext's user with the authenticated user's principal to ensure
// that subsequent requests within the same context will recognize the user
// as authenticated.
if (result.Succeeded)
{
httpContext.User = result.Principal;
}
return result;
}
@@ -141,7 +141,10 @@ public abstract class UmbracoViewPage<TModel> : RazorPage<TModel>
string.Format(
ContentSettings.PreviewBadge,
HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoPath),
Context.Request.GetEncodedUrl(),
System.Web.HttpUtility.HtmlEncode(Context.Request.GetEncodedUrl()), // Belt and braces - via a browser at least it doesn't seem possible to have anything other than
// a valid culture code provided in the querystring of this URL.
// But just to be sure of prevention of an XSS vulnterablity we'll HTML encode here too.
// An expected URL is untouched by this encoding.
UmbracoContext.PublishedRequest?.PublishedContent?.Id);
}
else
@@ -69,15 +69,7 @@
editorService.mediaTypeEditor(editor);
};
scope.openSVG = () => {
var popup = window.open('', '_blank');
var html = '<!DOCTYPE html><body><img src="' + scope.nodeUrl + '"/>' +
'<script>history.pushState(null, null,"' + $location.$$absUrl + '");</script></body>';
popup.document.open();
popup.document.write(html);
popup.document.close();
}
scope.openSVG = () => mediaHelper.openSVG(scope.nodeUrl);
// watch for content updates - reload content when node is saved, published etc.
scope.$watch('node.updateDate', function(newValue, oldValue){
@@ -3,7 +3,7 @@
* @name umbraco.services.mediaHelper
* @description A helper object used for dealing with media items
**/
function mediaHelper(umbRequestHelper, $http, $log) {
function mediaHelper(umbRequestHelper, $http, $log, $location) {
//container of fileresolvers
var _mediaFileResolvers = {};
@@ -449,7 +449,29 @@ function mediaHelper(umbRequestHelper, $http, $log) {
cropY2: options.crop ? options.crop.y2 : null
})),
"Failed to retrieve processed image URL for image: " + imagePath);
}
},
/**
* @ngdoc function
* @name umbraco.services.mediaHelper#openSVG
* @methodOf umbraco.services.mediaHelper
* @function
*
* @description
* Opens an SVG file in a new window as an image file, to prevent any potential XSS exploits.
*
* @param {string} imagePath File path, ex /media/1234/my-image.svg
*/
openSVG: function (imagePath) {
var popup = window.open('', '_blank');
var html = '<!DOCTYPE html><body style="background-image: linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%); background-size:30px 30px; background-position:0 0, 15px 0, 15px -15px, 0px 15px;">'
+ '<img src="' + imagePath + '"/>'
+ '<script>history.pushState(null, null,"' + $location.$$absUrl + '");</script></body>';
popup.document.open();
popup.document.write(html);
popup.document.close();
}
};
} angular.module('umbraco.services').factory('mediaHelper', mediaHelper);
@@ -3,6 +3,7 @@ angular.module('umbraco.services')
var currentUser = null;
var lastUserId = null;
var countdownCounter = null;
//this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
// this is used so that we know when to go and get the user's remaining seconds directly.
@@ -43,6 +44,10 @@ angular.module('umbraco.services')
}
currentUser = usr;
lastServerTimeoutSet = new Date();
//don't start the timer if it is already going
if (countdownCounter) {
return;
}
//start the timer
countdownUserTimeout();
}
@@ -54,23 +59,23 @@ angular.module('umbraco.services')
*/
function countdownUserTimeout() {
$timeout(function () {
countdownCounter = $timeout(function () {
if (currentUser) {
//countdown by 5 seconds since that is how long our timer is for.
currentUser.remainingAuthSeconds -= 5;
//if there are more than 30 remaining seconds, recurse!
if (currentUser.remainingAuthSeconds > 30) {
//if there are more than 20 remaining seconds, recurse!
if (currentUser.remainingAuthSeconds > 20) {
//we need to check when the last time the timeout was set from the server, if
// it has been more than 30 seconds then we'll manually go and retrieve it from the
// it has been more than 20 seconds then we'll manually go and retrieve it from the
// server - this helps to keep our local countdown in check with the true timeout.
if (lastServerTimeoutSet != null) {
var now = new Date();
var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
if (seconds > 30) {
if (seconds > 20) {
//first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
// wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
@@ -95,18 +100,23 @@ angular.module('umbraco.services')
if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
//NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
angularHelper.safeApply($rootScope, function () {
try {
//NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
// don't actually care about this result.
authResource.getRemainingTimeoutSeconds();
}
finally {
userAuthExpired();
}
//NOTE: We are calling this again so that the server can create a log that the timeout has expired
//and we will show the login screen as close to the server's timout time as possible
authResource.getRemainingTimeoutSeconds().then(function (result) {
setUserTimeoutInternal(result);
//the client auth can expire a second earlier as the client internal clock is behind
if (result < 1) {
userAuthExpired();
}
});
});
//recurse the countdown!
countdownUserTimeout();
}
else {
//we've got less than 30 seconds remaining so let's check the server
//we've got less than 20 seconds remaining so let's check the server
if (lastServerTimeoutSet != null) {
//first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
@@ -155,6 +165,7 @@ angular.module('umbraco.services')
lastServerTimeoutSet = null;
currentUser = null;
countdownCounter = null;
openLoginDialog(isLogout === undefined ? true : !isLogout);
}
@@ -1,6 +1,6 @@
<div class="umb-image-preview" ng-controller="umbImagePreviewController as controller">
<img class="umb-image-preview--image" ng-if="vm.clientSide" ng-init="previewUrl = controller.getClientSideUrl(vm.clientSideData)" ng-src="{{previewUrl}}" alt="{{vm.name}}" />
<a ng-if="!vm.clientSide" href="#" ng-href="{{vm.source}}" target="_blank" rel="noopener">
<a ng-if="!vm.clientSide" href="" ng-attr-href="{{vm.extension !== 'svg' ? vm.source : undefined}}" ng-click="vm.extension === 'svg' && controller.openSVG(vm.source)" target="_blank" rel="noopener">
<img class="umb-image-preview--image" ng-init="previewUrl = controller.getThumbnail(vm.source)" ng-src="{{previewUrl}}" alt="{{vm.name}}" />
</a>
</div>
@@ -1,18 +1,16 @@
angular.module("umbraco")
.controller("umbImagePreviewController",
function (mediaHelper) {
.controller("umbImagePreviewController",
function (mediaHelper) {
var vm = this;
var vm = this;
vm.getThumbnail = function(source) {
return mediaHelper.getThumbnailFromPath(source) || source;
}
vm.getClientSideUrl = function(sourceData) {
return URL.createObjectURL(sourceData);
}
vm.getThumbnail = function (source) {
return mediaHelper.getThumbnailFromPath(source) || source;
}
});
vm.getClientSideUrl = function (sourceData) {
return URL.createObjectURL(sourceData);
}
vm.openSVG = (source) => mediaHelper.openSVG(source);
});
@@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
@@ -15,7 +10,6 @@ using Umbraco.Cms.Persistence.Sqlite.Interceptors;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence;
@@ -126,6 +120,7 @@ public class LocksTests : UmbracoIntegrationTest
}
}
[NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")]
[Test]
public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted()
{
@@ -155,6 +150,37 @@ public class LocksTests : UmbracoIntegrationTest
Assert.AreEqual(0, sqlCount);
}
[Test]
public void GivenNonEagerLocking_WhenDbIsAccessed_ThenSqlIsExecuted()
{
var sqlCount = 0;
using (var scope = ScopeProvider.CreateScope())
{
var db = ScopeAccessor.AmbientScope.Database;
try
{
db.EnableSqlCount = true;
// Issue a lock request, but we are using non-eager
// locks so this only queues the request.
// The lock will not be issued unless we resolve
// scope.Database
scope.WriteLock(Constants.Locks.Servers);
scope.Database.ExecuteScalar<int>("SELECT 1");
sqlCount = db.SqlCount;
}
finally
{
db.EnableSqlCount = false;
}
}
Assert.AreEqual(2,sqlCount);
}
[Test]
[LongRunning]
public void ConcurrentWritersTest()
@@ -31,4 +31,87 @@ public class WebPathTests
[Test]
public void Combine_must_handle_null() => Assert.Throws<ArgumentNullException>(() => WebPath.Combine(null));
[Test]
[TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
[TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)]
[TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)]
[TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)]
[TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)]
[TestCase("/test", UriKind.Absolute, ExpectedResult = false)]
[TestCase("test", UriKind.Absolute, ExpectedResult = false)]
[TestCase("", UriKind.Absolute, ExpectedResult = false)]
[TestCase(null, UriKind.Absolute, ExpectedResult = false)]
[TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)]
[TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)]
[TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)]
[TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)]
[TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
[TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)]
[TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)]
[TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)]
[TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)]
[TestCase("/test", UriKind.Relative, ExpectedResult = true)]
[TestCase("test", UriKind.Relative, ExpectedResult = true)]
[TestCase("", UriKind.Relative, ExpectedResult = false)]
[TestCase(null, UriKind.Relative, ExpectedResult = false)]
[TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)]
[TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
[TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
[TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)]
[TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
public bool IsWellFormedWebPath(string? webPath, UriKind uriKind) => WebPath.IsWellFormedWebPath(webPath, uriKind);
}
@@ -2,9 +2,7 @@
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -35,7 +33,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -47,7 +45,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
await sut.HandleAsync(authHandlerContext);
@@ -60,7 +58,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -72,7 +70,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -85,7 +83,21 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
await sut.HandleAsync(authHandlerContext);
Assert.IsFalse(authHandlerContext.HasSucceeded);
AssertContentCached(mockHttpContextAccessor);
}
[Test]
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided()
{
// Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() });
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
await sut.HandleAsync(authHandlerContext);
@@ -98,7 +110,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -111,7 +123,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
await sut.HandleAsync(authHandlerContext);
@@ -124,7 +136,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -137,7 +149,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
await sut.HandleAsync(authHandlerContext);
@@ -150,7 +162,7 @@ public class ContentPermissionsQueryStringHandlerTests
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
await sut.HandleAsync(authHandlerContext);
@@ -169,14 +181,20 @@ public class ContentPermissionsQueryStringHandlerTests
return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource);
}
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
string queryStringName = QueryStringName,
string queryStringValue = "")
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue });
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
string queryStringName = QueryStringName,
string[]? queryStringValues = null)
{
queryStringValues ??= Array.Empty<string>();
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
var mockHttpContext = new Mock<HttpContext>();
var mockHttpRequest = new Mock<HttpRequest>();
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
@@ -2,9 +2,7 @@
// See LICENSE for more details.
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
@@ -34,7 +32,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Id_Missing_From_QueryString_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
await sut.HandleAsync(authHandlerContext);
@@ -46,7 +44,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
await sut.HandleAsync(authHandlerContext);
@@ -59,7 +57,21 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
await sut.HandleAsync(authHandlerContext);
Assert.IsFalse(authHandlerContext.HasSucceeded);
AssertMediaCached(mockHttpContextAccessor);
}
[Test]
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided()
{
// Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() });
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
await sut.HandleAsync(authHandlerContext);
@@ -72,7 +84,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
await sut.HandleAsync(authHandlerContext);
@@ -85,7 +97,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
await sut.HandleAsync(authHandlerContext);
@@ -98,7 +110,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
await sut.HandleAsync(authHandlerContext);
@@ -111,7 +123,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001);
await sut.HandleAsync(authHandlerContext);
@@ -124,7 +136,7 @@ public class MediaPermissionsQueryStringHandlerTests
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext();
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId);
await sut.HandleAsync(authHandlerContext);
@@ -140,14 +152,21 @@ public class MediaPermissionsQueryStringHandlerTests
return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource);
}
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
string queryStringName = QueryStringName,
string queryStringValue = "")
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue });
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
string queryStringName = QueryStringName,
string[]? queryStringValues = null)
{
queryStringValues ??= Array.Empty<string>();
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
var mockHttpContext = new Mock<HttpContext>();
var mockHttpRequest = new Mock<HttpRequest>();
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
@@ -155,6 +174,13 @@ public class MediaPermissionsQueryStringHandlerTests
return mockHttpContextAccessor;
}
/// <summary>
///
/// </summary>
/// <param name="httpContextAccessor"></param>
/// <param name="nodeId"></param>
/// <param name="startMediaId">the startMediaId of the user being setup</param>
/// <returns></returns>
private MediaPermissionsQueryStringHandler CreateHandler(
IHttpContextAccessor httpContextAccessor,
int nodeId,
@@ -179,7 +205,7 @@ public class MediaPermissionsQueryStringHandlerTests
mockEntityService
.Setup(x => x.GetId(
It.Is<Guid>(y => y == s_nodeGuid),
It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Document)))
It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Media)))
.Returns(Attempt<int>.Succeed(NodeId));
return mockEntityService;
}
@@ -44,7 +44,7 @@ public class MediaPermissionsResourceHandlerTests
}
[Test]
public async Task Resource_With_Node_Id_Withou_Permission_Is_Not_Authorized()
public async Task Resource_With_Node_Id_Without_Permission_Is_Not_Authorized()
{
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId, true);
var sut = CreateHandler(NodeId, 1001);