diff --git a/build/build.ps1 b/build/build.ps1 index 3ba347a6dc..185d1b8ff6 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -438,14 +438,11 @@ Write-Host "Prepare C# Documentation" $src = "$($this.SolutionRoot)\src" - $tmp = $this.BuildTemp - $out = $this.BuildOutput + $tmp = $this.BuildTemp + $out = $this.BuildOutput $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - - #restore nuget packages - $this.RestoreNuGet() # run DocFx $DocFx = $this.BuildEnv.DocFx @@ -463,13 +460,18 @@ $src = "$($this.SolutionRoot)\src" $out = $this.BuildOutput + # Check if the solution has been built + if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"} + "Moving to Umbraco.Web.UI.Docs folder" - cd ..\src\Umbraco.Web.UI.Docs + cd $src\Umbraco.Web.UI.Docs "Generating the docs and waiting before executing the next commands" & npm install & npx gulp docs + Pop-Location + # change baseUrl $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" $IndexPath = "./api/index.html" diff --git a/src/Umbraco.Configuration/Models/GlobalSettings.cs b/src/Umbraco.Configuration/Models/GlobalSettings.cs index e4995cfb0b..908ba71590 100644 --- a/src/Umbraco.Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Configuration/Models/GlobalSettings.cs @@ -39,7 +39,7 @@ namespace Umbraco.Configuration.Models } public int TimeOutInMinutes => _configuration.GetValue(Prefix + "TimeOutInMinutes", 20); - public string DefaultUILanguage => _configuration.GetValue(Prefix + "TimeOutInMinutes", "en-US"); + public string DefaultUILanguage => _configuration.GetValue(Prefix + "DefaultUILanguage", "en-US"); public bool HideTopLevelNodeFromPath => _configuration.GetValue(Prefix + "HideTopLevelNodeFromPath", false); diff --git a/src/Umbraco.Configuration/Models/SecuritySettings.cs b/src/Umbraco.Configuration/Models/SecuritySettings.cs index 9244eace96..297c95b1af 100644 --- a/src/Umbraco.Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Configuration/Models/SecuritySettings.cs @@ -14,7 +14,7 @@ namespace Umbraco.Configuration.Models _configuration = configuration; } - public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", true); + public bool KeepUserLoggedIn => _configuration.GetValue(Prefix + "KeepUserLoggedIn", false); public bool HideDisabledUsersInBackoffice => _configuration.GetValue(Prefix + "HideDisabledUsersInBackoffice", false); diff --git a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs index 15db0e0dc7..93acb279dc 100644 --- a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs @@ -36,7 +36,6 @@ namespace Umbraco.Core.BackOffice private UmbracoBackOfficeIdentity(ClaimsIdentity identity) : base(identity.Claims, Constants.Security.BackOfficeAuthenticationType) { - Actor = identity; } /// @@ -89,7 +88,7 @@ namespace Umbraco.Core.BackOffice if (string.IsNullOrWhiteSpace(realName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(realName)); if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(culture)); if (string.IsNullOrWhiteSpace(securityStamp)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(securityStamp)); - Actor = childIdentity; + AddRequiredClaims(userId, username, realName, startContentNodes, startMediaNodes, culture, securityStamp, allowedApps, roles); } @@ -204,5 +203,18 @@ namespace Umbraco.Core.BackOffice public string[] Roles => this.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToArray(); + /// + /// Overridden to remove any temporary claims that shouldn't be copied + /// + /// + public override ClaimsIdentity Clone() + { + var clone = base.Clone(); + + foreach (var claim in clone.FindAll(x => x.Type == Constants.Security.TicketExpiresClaimType).ToList()) + clone.RemoveClaim(claim); + + return clone; + } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 3551aa1c31..94370930de 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core /// public static class AppSettings { + // TODO: Are these all obsolete in netcore now? + public const string MainDomLock = "Umbraco.Core.MainDom.Lock"; // TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27 diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 7e96c6a912..d18e23a6c3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -48,6 +48,7 @@ public static class Mvc { public const string InstallArea = "UmbracoInstall"; + public const string BackOfficePathSegment = "BackOffice"; // The path segment prefix for all back office controllers public const string BackOfficeArea = "UmbracoBackOffice"; // Used for area routes of non-api controllers public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same } diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 8fd0e365e7..8f0c7beff8 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -76,13 +76,13 @@ namespace Umbraco.Core } //check for special back office paths - if (urlPath.InvariantStartsWith("/" + mvcArea + "/BackOffice/") - || urlPath.InvariantStartsWith("/" + mvcArea + "/Preview/")) + if (urlPath.InvariantStartsWith("/" + mvcArea + "/" + Constants.Web.Mvc.BackOfficePathSegment + "/")) { return true; } //check for special front-end paths + // TODO: These should be constants - will need to update when we do front-end routing if (urlPath.InvariantStartsWith("/" + mvcArea + "/Surface/") || urlPath.InvariantStartsWith("/" + mvcArea + "/Api/")) { diff --git a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs index a64bf15b7f..c21b09523d 100644 --- a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs @@ -29,5 +29,7 @@ namespace Umbraco.Web.Models public string EventElement { get; set; } [DataMember(Name = "customProperties")] public JObject CustomProperties { get; set; } + [DataMember(Name = "skipStepIfVisible")] + public string SkipStepIfVisible { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs new file mode 100644 index 0000000000..3464259052 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice.Security/BackOfficeCookieManagerTests.cs @@ -0,0 +1,172 @@ + + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Moq; +using NUnit.Framework; +using System; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Hosting; +using Umbraco.Extensions; +using Umbraco.Tests.Integration.Implementations; +using Umbraco.Web; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Security; + +namespace Umbraco.Tests.Security +{ + [TestFixture] + public class BackOfficeCookieManagerTests + { + [Test] + public void ShouldAuthenticateRequest_When_Not_Configured() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(), + globalSettings, + Mock.Of(), + Mock.Of()); + + var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); + + Assert.IsFalse(result); + } + + [Test] + public void ShouldAuthenticateRequest_When_Configured() + { + var testHelper = new TestHelper(); + + + //hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"), + globalSettings, + Mock.Of(), + Mock.Of()); + + var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco")); + + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Is_Back_Office() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{remainingTimeoutSecondsPath}")); + Assert.IsTrue(result); + + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{isAuthPath}")); + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Force_Auth() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); + Assert.IsTrue(result); + } + + [Test] + public void ShouldAuthenticateRequest_Not_Back_Office() + { + var testHelper = new TestHelper(); + + var httpContextAccessor = testHelper.GetHttpContextAccessor(); + var globalSettings = testHelper.SettingsForTests.GenerateMockGlobalSettings(); + + var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); + + var mgr = new BackOfficeCookieManager( + Mock.Of(), + runtime, + Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"), + globalSettings, + Mock.Of(), + GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath)); + + var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice")); + Assert.IsFalse(result); + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/umbraco/api/notbackoffice")); + Assert.IsFalse(result); + result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/umbraco/surface/notbackoffice")); + Assert.IsFalse(result); + } + + private LinkGenerator GetMockLinkGenerator(out string remainingTimeoutSecondsPath, out string isAuthPath) + { + var controllerName = ControllerExtensions.GetControllerName(); + + // this path is not a back office request even though it's in the same controller - it's a 'special' endpoint + var rPath = remainingTimeoutSecondsPath = $"/umbraco/{Constants.Web.Mvc.BackOfficePathSegment}/{Constants.Web.Mvc.BackOfficeApiArea}/{controllerName}/{nameof(AuthenticationController.GetRemainingTimeoutSeconds)}".ToLower(); + + // this is on the same controller but is considered a back office request + var aPath = isAuthPath = $"/umbraco/{Constants.Web.Mvc.BackOfficePathSegment}/{Constants.Web.Mvc.BackOfficeApiArea}/{controllerName}/{nameof(AuthenticationController.IsAuthenticated)}".ToLower(); + + var linkGenerator = new Mock(); + linkGenerator.Setup(x => x.GetPathByAddress( + //It.IsAny(), + It.IsAny(), + //It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())).Returns((RouteValuesAddress address, RouteValueDictionary routeVals1, PathString path, FragmentString fragment, LinkOptions options) => + { + if (routeVals1["action"].ToString() == nameof(AuthenticationController.GetRemainingTimeoutSeconds)) + return rPath; + if (routeVals1["action"].ToString() == nameof(AuthenticationController.IsAuthenticated).ToLower()) + return aPath; + return null; + }); + + return linkGenerator.Object; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs index 1aa1010458..87cff07475 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/BackOfficeClaimsPrincipalFactoryTests.cs @@ -71,7 +71,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice } [TestCase(ClaimTypes.NameIdentifier, _testUserId)] - [TestCase(ClaimTypes.Name, _testUserName)] + [TestCase(ClaimTypes.Name, _testUserName)] public async Task CreateAsync_Should_Include_Claim(string expectedClaimType, object expectedClaimValue) { var sut = CreateSut(); @@ -79,7 +79,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var claimsPrincipal = await sut.CreateAsync(_testUser); Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue.ToString())); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue.ToString())); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue.ToString())); } [Test] @@ -94,9 +94,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var sut = CreateSut(); var claimsPrincipal = await sut.CreateAsync(_testUser); - + Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue)); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue)); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue)); } [Test] @@ -112,7 +112,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var sut = CreateSut(); var claimsPrincipal = await sut.CreateAsync(_testUser); - + Assert.True(claimsPrincipal.HasClaim(expectedClaimType, expectedClaimValue)); } @@ -131,7 +131,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var claimsPrincipal = await sut.CreateAsync(_testUser); - Assert.True(claimsPrincipal.GetUmbracoIdentity().Actor.HasClaim(expectedClaimType, expectedClaimValue)); + Assert.True(claimsPrincipal.GetUmbracoIdentity().HasClaim(expectedClaimType, expectedClaimValue)); } [SetUp] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs index 47e1261c09..9e9d29a123 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentityTests.cs @@ -37,6 +37,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) Assert.Fail(); + Assert.IsNull(backofficeIdentity.Actor); Assert.AreEqual(1234, backofficeIdentity.Id); //Assert.AreEqual(sessionId, backofficeIdentity.SessionId); Assert.AreEqual(securityStamp, backofficeIdentity.SecurityStamp); @@ -60,7 +61,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), }); - if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) + if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out _)) Assert.Fail(); Assert.Pass(); @@ -83,7 +84,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer), }); - if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out var backofficeIdentity)) + if (UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity, out _)) Assert.Fail(); Assert.Pass(); @@ -105,6 +106,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); Assert.AreEqual(12, identity.Claims.Count()); + Assert.IsNull(identity.Actor); } @@ -116,7 +118,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.BackOffice var identity = new UmbracoBackOfficeIdentity( 1234, "testing", "hello world", new[] { 654 }, new[] { 654 }, "en-us", securityStamp, new[] { "content", "media" }, new[] { "admin" }); + // this will be filtered out during cloning + identity.AddClaim(new Claim(Constants.Security.TicketExpiresClaimType, "test")); + var cloned = identity.Clone(); + Assert.IsNull(cloned.Actor); Assert.AreEqual(10, cloned.Claims.Count()); } diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs new file mode 100644 index 0000000000..1b83c048d2 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -0,0 +1,405 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class NestedContentPropertyComponentTests + { + [Test] + public void Invalid_Json() + { + var component = new NestedContentPropertyComponent(); + + Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true)); + } + + [Test] + public void No_Nesting() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"": [{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ] + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + + [Test] + public void No_Nesting_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + + // Ensure that the original key is NOT changed/modified & still exists + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); + } + + [Test] + public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""name"": ""Item 1 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString())); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + } + } +} diff --git a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs deleted file mode 100644 index 1e451cf57d..0000000000 --- a/src/Umbraco.Tests/Security/BackOfficeCookieManagerTests.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Web; -using Microsoft.Owin; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Composing; -using Umbraco.Tests.Testing; -using Umbraco.Tests.Testing.Objects.Accessors; -using Umbraco.Web; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Umbraco.Tests.Common; - -namespace Umbraco.Tests.Security -{ - [TestFixture] - [UmbracoTest(WithApplication = true)] - public class BackOfficeCookieManagerTests : UmbracoTestBase - { - [Test] - public void ShouldAuthenticateRequest_When_Not_Configured() - { - //should force app ctx to show not-configured - ConfigurationManager.AppSettings.Set(Constants.AppSettings.ConfigurationStatus, ""); - - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - var globalSettings = TestObjects.GetGlobalSettings(); - var umbracoContext = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - globalSettings, - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var runtime = Mock.Of(x => x.Level == RuntimeLevel.Install); - var mgr = new BackOfficeCookieManager( - Mock.Of(accessor => accessor.UmbracoContext == umbracoContext), runtime, HostingEnvironment, globalSettings, AppCaches.RequestCache); - - var result = mgr.ShouldAuthenticateRequest(Mock.Of(), new Uri("http://localhost/umbraco")); - - Assert.IsFalse(result); - } - - [Test] - public void ShouldAuthenticateRequest_When_Configured() - { - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - var globalSettings = TestObjects.GetGlobalSettings(); - var umbCtx = new UmbracoContext( - httpContextAccessor, - Mock.Of(), - Mock.Of(), - globalSettings, - HostingEnvironment, - new TestVariationContextAccessor(), - UriUtility, - new AspNetCookieManager(httpContextAccessor)); - - var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run); - var mgr = new BackOfficeCookieManager(Mock.Of(accessor => accessor.UmbracoContext == umbCtx), runtime, HostingEnvironment, globalSettings, AppCaches.RequestCache); - - var request = new Mock(); - request.Setup(owinRequest => owinRequest.Uri).Returns(new Uri("http://localhost/umbraco")); - - var result = mgr.ShouldAuthenticateRequest( - Mock.Of(context => context.Request == request.Object), - new Uri("http://localhost/umbraco")); - - Assert.IsTrue(result); - } - - // TODO: Write remaining tests for `ShouldAuthenticateRequest` - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f2a6dee0bc..fc5b97c2f3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -162,6 +162,7 @@ + @@ -276,7 +277,6 @@ - diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index e96e9cba48..08aa255b89 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -1,13 +1,17 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; using System; using System.Net; using System.Threading.Tasks; +using Umbraco.Core; using Umbraco.Core.BackOffice; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Net; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; @@ -33,6 +37,8 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IUserService _userService; private readonly UmbracoMapper _umbracoMapper; private readonly IGlobalSettings _globalSettings; + private readonly ILogger _logger; + private readonly IIpResolver _ipResolver; // TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here @@ -43,7 +49,8 @@ namespace Umbraco.Web.BackOffice.Controllers BackOfficeSignInManager signInManager, IUserService userService, UmbracoMapper umbracoMapper, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + ILogger logger, IIpResolver ipResolver) { _webSecurity = webSecurity; _userManager = backOfficeUserManager; @@ -51,6 +58,27 @@ namespace Umbraco.Web.BackOffice.Controllers _userService = userService; _umbracoMapper = umbracoMapper; _globalSettings = globalSettings; + _logger = logger; + _ipResolver = ipResolver; + } + + [HttpGet] + public double GetRemainingTimeoutSeconds() + { + var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity(); + var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds(); + if (remainingSeconds <= 30 && backOfficeIdentity != null) + { + //NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in + // the timeout process. + + _logger.Info( + "User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}", + backOfficeIdentity.Name, + _ipResolver.GetCurrentRequestIpAddress()); + } + + return remainingSeconds; } /// @@ -156,6 +184,22 @@ namespace Umbraco.Web.BackOffice.Controllers throw new HttpResponseException(HttpStatusCode.BadRequest); } + /// + /// Logs the current user out + /// + /// + [TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))] + public IActionResult PostLogout() + { + HttpContext.SignOutAsync(Core.Constants.Security.BackOfficeAuthenticationType); + + _logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress); + + _userManager.RaiseLogoutSuccessEvent(User, int.Parse(User.Identity.GetUserId())); + + return Ok(); + } + /// /// Return the for the given /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs index d5490edb9a..7cbeb8e86e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.IO; @@ -11,7 +12,7 @@ using Umbraco.Web.Common.Attributes; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class BackOfficeAssetsController : UmbracoAuthorizedJsonController { private readonly IFileSystem _jsLibFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index a02220774d..439ae2e79c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.BackOffice.Controllers { // TODO: Put some exception filters in our webapi to return 404 instead of 500 when we throw ArgumentNullException // ref: https://www.exceptionnotfound.net/the-asp-net-web-api-exception-handling-pipeline-a-guided-tour/ - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] //[PrefixlessBodyModelValidator] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Settings)] public class CodeFileController : BackOfficeNotificationsController diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index e204a3431e..68026d9c89 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -25,7 +25,7 @@ using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.BackOffice.Controllers { //we need to fire up the controller like this to enable loading of remote css directly from this controller - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [ValidationFilter] [AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions [IsBackOffice] diff --git a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs index 0d66a5e329..3f0ab63c65 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DataTypeController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The security for this controller is defined to allow full CRUD access to data types if the user has access to either: /// Content Types, Member Types or Media Types ... and of course to Data Types /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] public class DataTypeController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index a997c3dacf..7d362e52b6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either: /// Dictionary /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] public class DictionaryController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs index 5f5439f046..14fc25cfeb 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs @@ -18,7 +18,7 @@ using SearchResult = Umbraco.Web.Models.ContentEditing.SearchResult; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ExamineManagementController : UmbracoAuthorizedJsonController { private readonly IExamineManager _examineManager; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs index 47a5efdcbe..ee8d113abd 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImageUrlGeneratorController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core; using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Web.Common.Attributes; @@ -22,7 +23,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// building to generate correct URLs /// /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController { private readonly IImageUrlGenerator _imageUrlGenerator; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs index e79a481701..6ce6d3a1c6 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ImagesController.cs @@ -1,6 +1,7 @@ using System; using System.IO; using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Media; @@ -13,7 +14,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used to return images for media /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class ImagesController : UmbracoAuthorizedApiController { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 03e4ad163d..f2cbe571db 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Backoffice controller supporting the dashboard for language administration. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] //[PrefixlessBodyModelValidator] public class LanguageController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs index 97dc74ac31..297c39a450 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller used for getting log history /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class LogController : UmbracoAuthorizedJsonController { private readonly IMediaFileSystem _mediaFileSystem; diff --git a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs index 444ebbe988..258c3817aa 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LogViewerController.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Backoffice controller supporting the dashboard for viewing logs with some simple graphs & filtering /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class LogViewerController : UmbracoAuthorizedJsonController { private readonly ILogViewer _logViewer; diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 8994046cb2..e5e39f944f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used for managing packages in the back office /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs index 0216e6f09d..b61e86746a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageInstallController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// A controller used for installing packages and managing all of the data in the packages section in the back office /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Packages)] public class PackageInstallController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs index bee20f58e7..5086919b83 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RedirectUrlManagementController.cs @@ -15,7 +15,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RedirectUrlManagementController : UmbracoAuthorizedApiController { private readonly ILogger _logger; diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs index 686afe284b..bf40e5722f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationController.cs @@ -17,7 +17,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] public class RelationController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index f679dd6b8e..31c77ce0a4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller for editing relation types. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.RelationTypes)] public class RelationTypeController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs index eb1694c34f..5239994e04 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/SectionController.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc.Controllers; +using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -17,7 +18,7 @@ namespace Umbraco.Web.Editors /// /// The API controller used for using the list of sections /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class SectionController : UmbracoAuthorizedJsonController { private readonly IControllerFactory _controllerFactory; diff --git a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs index 852bff28c1..4dbfda1148 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/StylesheetController.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// The API controller used for retrieving available stylesheets /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class StylesheetController : UmbracoAuthorizedJsonController { private readonly IFileService _fileService; diff --git a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs index 7a2d4de4b0..0c1798fa8d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TemplateController.cs @@ -16,7 +16,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoTreeAuthorizeAttribute(Constants.Trees.Templates)] public class TemplateController : BackOfficeNotificationsController { diff --git a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs index dd8620d8ee..dd7c539922 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TinyMceController.cs @@ -19,7 +19,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] [UmbracoApplicationAuthorize( Constants.Applications.Content, Constants.Applications.Media, diff --git a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs index bf8e89ae0d..f85bdb1bd5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/TourController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/TourController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Hosting; using Umbraco.Core.Services; @@ -13,7 +14,7 @@ using Umbraco.Web.Tour; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class TourController : UmbracoAuthorizedJsonController { private readonly TourFilterCollection _filters; diff --git a/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs b/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs index 6ae0d1f612..4212bace72 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UpdateCheckController.cs @@ -15,7 +15,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.BackOffice.Controllers { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class UpdateCheckController : UmbracoAuthorizedJsonController { private readonly IUpgradeService _upgradeService; diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs index 6105383a3d..acd468191a 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBackOfficeServiceCollectionExtensions.cs @@ -23,6 +23,9 @@ namespace Umbraco.Extensions { services.AddAntiforgery(); + // TODO: We had this check in v8 where we don't enable these unless we can run... + //if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app; + services .AddAuthentication(Constants.Security.BackOfficeAuthenticationType) .AddCookie(Constants.Security.BackOfficeAuthenticationType); diff --git a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 2fd37e2875..6431911a1f 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -24,6 +24,8 @@ namespace Umbraco.Web.BackOffice.Filters /// public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute { + // TODO: Either make this inherit from TypeFilter or make this just a normal IActionFilter + private readonly ILogger _logger; private readonly IBackOfficeAntiforgery _antiforgery; private readonly ICookieManager _cookieManager; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs index fc922ca835..942b9dd6ea 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/NestedContentController.cs @@ -4,10 +4,11 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Services; using Umbraco.Web.Common.Attributes; using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Core; namespace Umbraco.Web.BackOffice.PropertyEditors { - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class NestedContentController : UmbracoAuthorizedJsonController { private readonly IContentTypeService _contentTypeService; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs index 2250f85f9b..1f302294de 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/RichTextPreValueController.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.BackOffice.PropertyEditors /// /// ApiController to provide RTE configuration with available plugins and commands from the RTE config /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RichTextPreValueController : UmbracoAuthorizedJsonController { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs index 8d9bc06acd..5ce8e09280 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/RteEmbedController.cs @@ -5,13 +5,14 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Core.Media; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Media.EmbedProviders; +using Umbraco.Core; namespace Umbraco.Web.BackOffice.PropertyEditors { /// /// A controller used for the embed dialog /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class RteEmbedController : UmbracoAuthorizedJsonController { private readonly EmbedProvidersCollection _embedCollection; diff --git a/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs b/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs index 37dbd84bfc..aa2b413abd 100644 --- a/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs +++ b/src/Umbraco.Web.BackOffice/PropertyEditors/TagsDataController.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.PropertyEditors /// DO NOT inherit from UmbracoAuthorizedJsonController since we don't want to use the angularized /// json formatter as it causes problems. /// - [PluginController("UmbracoApi")] + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class TagsDataController : UmbracoAuthorizedApiController { private readonly ITagQuery _tagQuery; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs index ca71f8f8af..f63b2380af 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs @@ -1,13 +1,16 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Routing; using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Extensions; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; +using Umbraco.Web.BackOffice.Controllers; namespace Umbraco.Web.BackOffice.Security { @@ -20,7 +23,7 @@ namespace Umbraco.Web.BackOffice.Security /// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install and /base therefore we cannot just set the cookie path to be /umbraco, /// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path. /// - internal class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager + public class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager { private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IRuntimeState _runtime; @@ -28,13 +31,25 @@ namespace Umbraco.Web.BackOffice.Security private readonly IGlobalSettings _globalSettings; private readonly IRequestCache _requestCache; private readonly string[] _explicitPaths; - private readonly string _getRemainingSecondsPath; - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache) - : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, null) + public BackOfficeCookieManager( + IUmbracoContextAccessor umbracoContextAccessor, + IRuntimeState runtime, + IHostingEnvironment hostingEnvironment, + IGlobalSettings globalSettings, + IRequestCache requestCache, + LinkGenerator linkGenerator) + : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, linkGenerator, null) { } - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache, IEnumerable explicitPaths) + public BackOfficeCookieManager( + IUmbracoContextAccessor umbracoContextAccessor, + IRuntimeState runtime, + IHostingEnvironment hostingEnvironment, + IGlobalSettings globalSettings, + IRequestCache requestCache, + LinkGenerator linkGenerator, + IEnumerable explicitPaths) { _umbracoContextAccessor = umbracoContextAccessor; _runtime = runtime; @@ -42,9 +57,6 @@ namespace Umbraco.Web.BackOffice.Security _globalSettings = globalSettings; _requestCache = requestCache; _explicitPaths = explicitPaths?.ToArray(); - var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); - // TODO: We shouldn't hard code this path - _getRemainingSecondsPath = $"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"; } /// @@ -60,7 +72,7 @@ namespace Umbraco.Web.BackOffice.Security /// * it is a /base request /// * it is a preview request /// - internal bool ShouldAuthenticateRequest(Uri requestUri, bool checkForceAuthTokens = true) + public bool ShouldAuthenticateRequest(Uri requestUri, bool checkForceAuthTokens = true) { // Do not authenticate the request if we are not running (don't have a db, are not configured) - since we will never need // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues @@ -75,9 +87,6 @@ namespace Umbraco.Web.BackOffice.Security if (_explicitPaths != null) return _explicitPaths.Any(x => x.InvariantEquals(requestUri.AbsolutePath)); - //check user seconds path - if (requestUri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; - if (//check the explicit flag checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null //check back office diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs index 3a23ae6c88..91b982b5f6 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSecureDataFormat.cs @@ -9,7 +9,6 @@ namespace Umbraco.Web.BackOffice.Security /// /// Custom secure format that ensures the Identity in the ticket is and not just a ClaimsIdentity /// - // TODO: Unsure if we really need this, there's no real reason why we have a custom Identity instead of just a ClaimsIdentity internal class BackOfficeSecureDataFormat : ISecureDataFormat { private readonly int _loginTimeoutMinutes; @@ -23,7 +22,7 @@ namespace Umbraco.Web.BackOffice.Security public string Protect(AuthenticationTicket data, string purpose) { - //create a new ticket based on the passed in tickets details, however, we'll adjust the expires utc based on the specified timeout mins + // create a new ticket based on the passed in tickets details, however, we'll adjust the expires utc based on the specified timeout mins var ticket = new AuthenticationTicket(data.Principal, new AuthenticationProperties(data.Properties.Items) { diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index fd009eab23..d51db33a55 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -18,6 +18,7 @@ using Umbraco.Core.Security; using Umbraco.Extensions; using Microsoft.Extensions.DependencyInjection; using Umbraco.Web.Common.Security; +using Microsoft.AspNetCore.Routing; namespace Umbraco.Web.BackOffice.Security { @@ -35,7 +36,9 @@ namespace Umbraco.Web.BackOffice.Security private readonly IRequestCache _requestCache; private readonly IUserService _userService; private readonly IIpResolver _ipResolver; + private readonly ISystemClock _systemClock; private readonly BackOfficeSessionIdValidator _sessionIdValidator; + private readonly LinkGenerator _linkGenerator; public ConfigureBackOfficeCookieOptions( IUmbracoContextAccessor umbracoContextAccessor, @@ -47,7 +50,9 @@ namespace Umbraco.Web.BackOffice.Security IRequestCache requestCache, IUserService userService, IIpResolver ipResolver, - BackOfficeSessionIdValidator sessionIdValidator) + ISystemClock systemClock, + BackOfficeSessionIdValidator sessionIdValidator, + LinkGenerator linkGenerator) { _umbracoContextAccessor = umbracoContextAccessor; _securitySettings = securitySettings; @@ -58,7 +63,9 @@ namespace Umbraco.Web.BackOffice.Security _requestCache = requestCache; _userService = userService; _ipResolver = ipResolver; + _systemClock = systemClock; _sessionIdValidator = sessionIdValidator; + _linkGenerator = linkGenerator; } public void Configure(string name, CookieAuthenticationOptions options) @@ -98,7 +105,8 @@ namespace Umbraco.Web.BackOffice.Security _runtimeState, _hostingEnvironment, _globalSettings, - _requestCache); + _requestCache, + _linkGenerator); // _explicitPaths); TODO: Implement this once we do OAuth somehow @@ -111,7 +119,7 @@ namespace Umbraco.Web.BackOffice.Security // It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else // our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because // the defaults work fine with our setup. - + OnValidatePrincipal = async ctx => { // We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this) @@ -131,6 +139,7 @@ namespace Umbraco.Web.BackOffice.Security await EnsureValidSessionId(ctx); await securityStampValidator.ValidateAsync(ctx); + EnsureTicketRenewalIfKeepUserLoggedIn(ctx); // add a claim to track when the cookie expires, we use this to track time remaining backOfficeIdentity.AddClaim(new Claim( @@ -140,7 +149,7 @@ namespace Umbraco.Web.BackOffice.Security UmbracoBackOfficeIdentity.Issuer, UmbracoBackOfficeIdentity.Issuer, backOfficeIdentity)); - + }, OnSigningIn = ctx => { @@ -175,7 +184,6 @@ namespace Umbraco.Web.BackOffice.Security OnSigningOut = ctx => { //Clear the user's session on sign out - // TODO: We need to test this once we have signout functionality, not sure if the httpcontext.user.identity will still be set here if (ctx.HttpContext?.User?.Identity != null) { var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity; @@ -192,7 +200,9 @@ namespace Umbraco.Web.BackOffice.Security BackOfficeSessionIdValidator.CookieName, _securitySettings.AuthCookieName, Constants.Web.PreviewCookieName, - Constants.Security.BackOfficeExternalCookieName + Constants.Security.BackOfficeExternalCookieName, + Constants.Web.AngularCookieName, + Constants.Web.CsrfValidationCookieName, }; foreach (var cookie in cookies) { @@ -218,5 +228,31 @@ namespace Umbraco.Web.BackOffice.Security if (_runtimeState.Level == RuntimeLevel.Run) await _sessionIdValidator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context); } + + /// + /// Ensures the ticket is renewed if the is set to true + /// and the current request is for the get user seconds endpoint + /// + /// + private void EnsureTicketRenewalIfKeepUserLoggedIn(CookieValidatePrincipalContext context) + { + if (!_securitySettings.KeepUserLoggedIn) return; + + var currentUtc = _systemClock.UtcNow; + var issuedUtc = context.Properties.IssuedUtc; + var expiresUtc = context.Properties.ExpiresUtc; + + if (expiresUtc.HasValue && issuedUtc.HasValue) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + //if it's time to renew, then do it + if (timeRemaining < timeElapsed) + { + context.ShouldRenew = true; + } + } + } } } diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 76d773d1da..e4a91c114b 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -7,6 +7,7 @@ using Umbraco.Web.Common.Install; using Umbraco.Core.Hosting; using System.Linq.Expressions; using Umbraco.Web.Common.Controllers; +using System.Linq; namespace Umbraco.Extensions { @@ -58,6 +59,23 @@ namespace Umbraco.Extensions return linkGenerator.GetUmbracoApiService(actionName, typeof(T), id); } + public static string GetUmbracoApiService(this LinkGenerator url, Expression> methodSelector) + where T : UmbracoApiControllerBase + { + var method = ExpressionHelper.GetMethodInfo(methodSelector); + var methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + + if (methodParams.Any() == false) + { + return url.GetUmbracoApiService(method.Name); + } + return url.GetUmbracoApiService(method.Name, methodParams.Values.First()); + } + public static string GetUmbracoApiServiceBaseUrl(this LinkGenerator linkGenerator, Expression> methodSelector) where T : UmbracoApiControllerBase { diff --git a/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs index e5ebdd177c..1349145357 100644 --- a/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensions.cs @@ -128,7 +128,7 @@ namespace Umbraco.Web.Common.Routing object constraints = null) => endpoints.MapUmbracoRoute(controllerType, rootSegment, areaName, isBackOffice - ? (areaName.IsNullOrWhiteSpace() ? "BackOffice/Api" : $"BackOffice/{areaName}") + ? (areaName.IsNullOrWhiteSpace() ? $"{Core.Constants.Web.Mvc.BackOfficePathSegment}/Api" : $"{Core.Constants.Web.Mvc.BackOfficePathSegment}/{areaName}") : (areaName.IsNullOrWhiteSpace() ? "Api" : areaName), defaultAction, true, constraints); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 6f98dbca6e..01d73568ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -5,14 +5,14 @@ @scope @description -Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. -In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. -You can easily add you own tours to the Help-drawer or show and start tours from +Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. +In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. +You can easily add you own tours to the Help-drawer or show and start tours from anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install The Starter Kit in Umbraco 7.8

Extending the help drawer with custom tours

-The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. -Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be +The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. +Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be picked up by Umbraco and shown in the Help-drawer.

The tour object

@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list "groupOrder": 200 // Control the order of tour groups "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step "culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users - "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. + "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. "steps": [] // tour steps - see next example } @@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list "backdropOpacity": 0.4 // the backdrop opacity "view": "" // add a custom view "customProperties" : {} // add any custom properties needed for the custom view + "skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step }

Adding tours to other parts of the Umbraco backoffice

-It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, +It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.

Using the tour service

@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um "element": "[data-element='my-tour-button']", "title": "Click the button", "content": "Click the button", - "event": "click" + "event": "click", + "skipStepIfVisible": "[data-element='my-other-tour-button']" } ] }; @@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes // make sure we don't go too far if (scope.model.currentStepIndex !== scope.model.steps.length) { + + var upcomingStep = scope.model.steps[scope.model.currentStepIndex]; + + // If the currentStep JSON object has 'skipStepIfVisible' + // It's a DOM selector - if we find it then we ship over this step + if(upcomingStep.skipStepIfVisible) { + let tryFindDomEl = document.querySelector(upcomingStep.element); + if(tryFindDomEl) { + // check if element is visible: + if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) { + // if it was visible then we skip the step. + nextStep(); + } + } + } + startStep(); - // tour completed - final step + } else { + // tour completed - final step scope.loadingStep = true; waitForPendingRerequests().then(function () { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 9a3760444d..8a24d948ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -48,6 +48,9 @@ label.umb-form-check--checkbox{ &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; } + &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check { + background-color: @ui-option-type; + } &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { background: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index d0aa1a1c34..3709e703d9 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -188,27 +188,21 @@ "event": "click" }, { - "element": "[data-element~='editor-data-type-picker']", + "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

In the editor picker dialog we can pick one of the many built-in editors.

You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors).

" + "content": "

In the editor picker dialog we can pick one of the many built-in editors.

" }, { - "element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']", + "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", "title": "Select editor", "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", "event": "click" }, { - "element": "[data-element~='editor-data-type-settings']", - "elementPreventClick": true, + "element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a", "title": "Editor settings", - "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed." - }, - { - "element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']", - "title": "Save editor", - "content": "Click Submit to save the changes.", + "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.", "event": "click" }, { @@ -317,7 +311,8 @@ "content": "

To see all our templates click the small triangle to the left of the templates node.

", "event": "click", "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", - "view": "templatetree" + "view": "templatetree", + "skipStepIfVisible": "#tree [data-element='tree-item-templates'] > div > button[data-element=tree-item-expand].icon-navigation-down" }, { "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 8ae88b62ef..fe8a93f67a 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -386,30 +386,7 @@ namespace Umbraco.Web.Editors } - /// - /// Logs the current user out - /// - /// - [ClearAngularAntiForgeryToken] - [ValidateAngularAntiForgeryToken] - public HttpResponseMessage PostLogout() - { - var owinContext = Request.TryGetOwinContext().Result; - - owinContext.Authentication.SignOut( - Core.Constants.Security.BackOfficeAuthenticationType, - Core.Constants.Security.BackOfficeExternalAuthenticationType); - - Logger.Info("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, owinContext.Request.RemoteIpAddress); - - if (UserManager != null) - { - int.TryParse(User.Identity.GetUserId(), out var userId); - UserManager.RaiseLogoutSuccessEvent(User, userId); - } - - return Request.CreateResponse(HttpStatusCode.OK); - } + // NOTE: This has been migrated to netcore, but in netcore we don't explicitly set the principal in this method, that's done in ConfigureUmbracoBackOfficeCookieOptions so don't worry about that private HttpResponseMessage SetPrincipalAndReturnUserDetail(IUser user, IPrincipal principal) diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index f766a142d9..d95c19bedf 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -29,228 +29,6 @@ namespace Umbraco.Web.Security ///
public static class AppBuilderExtensions { - /// - /// Configure Default Identity User Manager for Umbraco - /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - ServiceContext services, - IGlobalSettings globalSettings, - UmbracoMapper mapper, - // TODO: This could probably be optional? - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - { - if (services == null) throw new ArgumentNullException(nameof(services)); - - //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeOwinUserManager.Create( - services.UserService, - services.EntityService, - services.ExternalLoginService, - globalSettings, - mapper, - passwordConfiguration, - ipResolver, - new BackOfficeIdentityErrorDescriber(), - app.GetDataProtectionProvider(), - new NullLogger>())); - - app.SetBackOfficeUserManagerType(); - - //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger())); - } - - /// - /// Configure a custom UserStore with the Identity User Manager for Umbraco - /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - IRuntimeState runtimeState, - IGlobalSettings globalSettings, - BackOfficeUserStore customUserStore, - // TODO: This could probably be optional? - IPasswordConfiguration passwordConfiguration, - IIpResolver ipResolver) - { - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - if (customUserStore == null) throw new ArgumentNullException(nameof(customUserStore)); - - //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext( - (options, owinContext) => BackOfficeOwinUserManager.Create( - passwordConfiguration, - ipResolver, - customUserStore, - new BackOfficeIdentityErrorDescriber(), - app.GetDataProtectionProvider(), - new NullLogger>())); - - app.SetBackOfficeUserManagerType(); - - //Create a sign in manager per request - app.CreatePerOwinContext((options, context) => BackOfficeSignInManager.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager).FullName))); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// By default this will be configured to execute on PipelineStage.Authenticate - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IRuntimeState runtimeState, - IUserService userService, - IGlobalSettings globalSettings, - ISecuritySettings securitySettings, - IHostingEnvironment hostingEnvironment, - IRequestCache requestCache) - { - return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, userService, globalSettings, securitySettings, hostingEnvironment, requestCache, PipelineStage.Authenticate); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Configurable pipeline stage - /// - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IRuntimeState runtimeState, - IUserService userService, - IGlobalSettings globalSettings, - ISecuritySettings securitySettings, - IHostingEnvironment hostingEnvironment, - IRequestCache requestCache, - PipelineStage stage) - { - //Create the default options and provider - var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); - - return app.UseUmbracoBackOfficeCookieAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySettings, hostingEnvironment, requestCache, authOptions, stage); - } - - /// - /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline - /// - /// - /// - /// - /// - /// - /// - /// - /// Custom auth cookie options can be specified to have more control over the cookie authentication logic - /// - /// Configurable pipeline stage - /// - /// - public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, - ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache, CookieAuthenticationOptions cookieOptions, PipelineStage stage) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - if (cookieOptions == null) throw new ArgumentNullException(nameof(cookieOptions)); - if (cookieOptions.Provider == null) - throw new ArgumentNullException("cookieOptions.Provider cannot be null.", nameof(cookieOptions)); - if (cookieOptions.Provider is BackOfficeCookieAuthenticationProvider == false) - throw new ArgumentException($"cookieOptions.Provider must be of type {typeof(BackOfficeCookieAuthenticationProvider)}.", nameof(cookieOptions)); - - app.UseUmbracoBackOfficeCookieAuthenticationInternal(cookieOptions, runtimeState, requestCache, stage); - - //don't apply if app is not ready - if (runtimeState.Level != RuntimeLevel.Upgrade && runtimeState.Level != RuntimeLevel.Run) return app; - - var backOfficePath = globalSettings.GetBackOfficePath(hostingEnvironment); - var cookieAuthOptions = app.CreateUmbracoCookieAuthOptions( - umbracoContextAccessor, globalSettings, runtimeState, securitySettings, - //This defines the explicit path read cookies from for this middleware - hostingEnvironment, requestCache, new[] {$"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"}); - cookieAuthOptions.Provider = cookieOptions.Provider; - - //This is a custom middleware, we need to return the user's remaining logged in seconds - app.Use( - cookieAuthOptions, - Current.Configs.Global(), - Current.Configs.Security(), - app.CreateLogger(), - Current.HostingEnvironment); - - //This is required so that we can read the auth ticket format outside of this pipeline - app.CreatePerOwinContext( - (options, context) => new UmbracoAuthTicketDataProtector(cookieOptions.TicketDataFormat)); - - return app; - } - - private static bool _markerSet = false; - - /// - /// This registers the exact type of the user manager in owin so we can extract it - /// when required in order to extract the user manager instance - /// - /// - /// - /// - /// - /// This is required because a developer can specify a custom user manager and due to generic types the key name will registered - /// differently in the owin context - /// - private static void SetBackOfficeUserManagerType(this IAppBuilder app) - where TManager : BackOfficeUserManager - where TUser : BackOfficeIdentityUser - { - if (_markerSet) throw new InvalidOperationException("The back office user manager marker has already been set, only one back office user manager can be configured"); - - //on each request set the user manager getter - - // this is required purely because Microsoft.Owin.IOwinContext is super inflexible with it's Get since it can only be - // a generic strongly typed instance - app.Use((context, func) => - { - context.Set(BackOfficeOwinUserManager.OwinMarkerKey, new BackOfficeUserManagerMarker()); - return func(); - }); - } - - private static void UseUmbracoBackOfficeCookieAuthenticationInternal(this IAppBuilder app, CookieAuthenticationOptions options, IRuntimeState runtimeState, IRequestCache requestCache, PipelineStage stage) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - - //First the normal cookie middleware - app.Use(typeof(CookieAuthenticationMiddleware), app, options); - //don't apply if app is not ready - if (runtimeState.Level == RuntimeLevel.Upgrade || runtimeState.Level == RuntimeLevel.Run) - { - //Then our custom middlewares - app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, Current.UmbracoContextAccessor, requestCache); - app.Use(typeof(FixWindowsAuthMiddlware)); - } - - //Marks all of the above middlewares to execute on Authenticate - app.UseStageMarker(stage); - } - /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct @@ -297,8 +75,6 @@ namespace Umbraco.Web.Security AuthenticationMode = AuthenticationMode.Passive, CookieName = Constants.Security.BackOfficeExternalCookieName, ExpireTimeSpan = TimeSpan.FromMinutes(5), - //Custom cookie manager so we can filter requests - CookieManager = new BackOfficeCookieManager(umbracoContextAccessor, runtimeState, hostingEnvironment, globalSettings, requestCache), CookiePath = "/", CookieSecure = globalSettings.UseHttps ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest, CookieHttpOnly = true, @@ -353,8 +129,8 @@ namespace Umbraco.Web.Security { if (runtimeState.Level != RuntimeLevel.Run) return app; - var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); - app.Use(typeof(PreviewAuthenticationMiddleware), authOptions, globalSettings, hostingEnvironment); + //var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache); + app.Use(typeof(PreviewAuthenticationMiddleware), /*authOptions*/null, globalSettings, hostingEnvironment); // This middleware must execute at least on PostAuthentication, by default it is on Authorize // The middleware needs to execute after the RoleManagerModule executes which is during PostAuthenticate, @@ -372,40 +148,6 @@ namespace Umbraco.Web.Security Thread.CurrentThread.SanitizeThreadCulture(); } - /// - /// Create the default umb cookie auth options - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static UmbracoBackOfficeCookieAuthOptions CreateUmbracoCookieAuthOptions(this IAppBuilder app, - IUmbracoContextAccessor umbracoContextAccessor, - IGlobalSettings globalSettings, IRuntimeState runtimeState, ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache, string[] explicitPaths = null) - { - //this is how aspnet wires up the default AuthenticationTicket protector so we'll use the same code - var ticketDataFormat = new TicketDataFormat( - app.CreateDataProtector(typeof (CookieAuthenticationMiddleware).FullName, - Constants.Security.BackOfficeAuthenticationType, - "v1")); - - var authOptions = new UmbracoBackOfficeCookieAuthOptions( - explicitPaths, - umbracoContextAccessor, - securitySettings, - globalSettings, - hostingEnvironment, - runtimeState, - ticketDataFormat, - requestCache); - - return authOptions; - } public static IAppBuilder CreatePerOwinContext(this IAppBuilder app, Func createCallback) where T : class, IDisposable { diff --git a/src/Umbraco.Web/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/BackOfficeCookieManager.cs deleted file mode 100644 index 73c143afd4..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeCookieManager.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using Microsoft.Owin; -using Microsoft.Owin.Infrastructure; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Security; - -namespace Umbraco.Web.Security -{ - /// - /// A custom cookie manager that is used to read the cookie from the request. - /// - /// - /// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install and /base therefore we cannot just set the cookie path to be /umbraco, - /// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path. - /// - internal class BackOfficeCookieManager : ChunkingCookieManager, Microsoft.Owin.Infrastructure.ICookieManager - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IRuntimeState _runtime; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IGlobalSettings _globalSettings; - private readonly IRequestCache _requestCache; - private readonly string[] _explicitPaths; - private readonly string _getRemainingSecondsPath; - - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache) - : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, null) - { } - - public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtime, IHostingEnvironment hostingEnvironment, IGlobalSettings globalSettings, IRequestCache requestCache, IEnumerable explicitPaths) - { - _umbracoContextAccessor = umbracoContextAccessor; - _runtime = runtime; - _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings; - _requestCache = requestCache; - _explicitPaths = explicitPaths?.ToArray(); - var backOfficePath = _globalSettings.GetBackOfficePath(_hostingEnvironment); - _getRemainingSecondsPath = $"{backOfficePath}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds"; - } - - /// - /// Explicitly implement this so that we filter the request - /// - /// - /// - /// - string Microsoft.Owin.Infrastructure.ICookieManager.GetRequestCookie(IOwinContext context, string key) - { - if (_umbracoContextAccessor.UmbracoContext == null || context.Request.Uri.IsClientSideRequest()) - { - return null; - } - - return ShouldAuthenticateRequest( - context, - _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl) == false - //Don't auth request, don't return a cookie - ? null - //Return the default implementation - : GetRequestCookie(context, key); - } - - /// - /// Determines if we should authenticate the request - /// - /// - /// - /// - /// - /// - /// We auth the request when: - /// * it is a back office request - /// * it is an installer request - /// * it is a /base request - /// * it is a preview request - /// - internal bool ShouldAuthenticateRequest(IOwinContext owinContext, Uri originalRequestUrl, bool checkForceAuthTokens = true) - { - // Do not authenticate the request if we are not running (don't have a db, are not configured) - since we will never need - // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues - // when people have older invalid cookies on the same domain since our user managers might attempt to lookup a user - // and we don't even have a db. - // was: app.IsConfigured == false (equiv to !Run) && dbContext.IsDbConfigured == false (equiv to Install) - // so, we handle .Install here and NOT .Upgrade - if (_runtime.Level == RuntimeLevel.Install) - return false; - - var request = owinContext.Request; - //check the explicit paths - if (_explicitPaths != null) - { - return _explicitPaths.Any(x => x.InvariantEquals(request.Uri.AbsolutePath)); - } - - //check user seconds path - if (request.Uri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; - - if (//check the explicit flag - (checkForceAuthTokens && owinContext.Get(Constants.Security.ForceReAuthFlag) != null) - || (checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null) - //check back office - || request.Uri.IsBackOfficeRequest(_globalSettings, _hostingEnvironment) - //check installer - || request.Uri.IsInstallerRequest(_hostingEnvironment)) - { - return true; - } - return false; - } - - } -} diff --git a/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs b/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs index b3092b90c9..deb8e9fd63 100644 --- a/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs +++ b/src/Umbraco.Web/Security/ForceRenewalCookieAuthenticationHandler.cs @@ -57,19 +57,19 @@ namespace Umbraco.Web.Security protected override Task ApplyResponseGrantAsync() { if (_umbracoContextAccessor.UmbracoContext == null || Context.Request.Uri.IsClientSideRequest()) - { + { return Task.FromResult(0); } //Now we need to check if we should force renew this based on a flag in the context and whether this is a request that is not normally renewed by OWIN... // which means that it is not a normal URL that is authenticated. - var normalAuthUrl = ((BackOfficeCookieManager) Options.CookieManager) - .ShouldAuthenticateRequest(Context, _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl, - //Pass in false, we want to know if this is a normal auth'd page - checkForceAuthTokens: false); - //This is auth'd normally, so OWIN will naturally take care of the cookie renewal - if (normalAuthUrl) return Task.FromResult(0); + //var normalAuthUrl = ((BackOfficeCookieManager) Options.CookieManager) + // .ShouldAuthenticateRequest(Context, _umbracoContextAccessor.UmbracoContext.OriginalRequestUrl, + // //Pass in false, we want to know if this is a normal auth'd page + // checkForceAuthTokens: false); + ////This is auth'd normally, so OWIN will naturally take care of the cookie renewal + //if (normalAuthUrl) return Task.FromResult(0); //check for the special flag in either the owin or http context var shouldRenew = Context.Get(Constants.Security.ForceReAuthFlag) != null || (_requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null); diff --git a/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs index 68155f47b7..34669bc5ae 100644 --- a/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/UmbracoBackOfficeCookieAuthOptions.cs @@ -18,34 +18,6 @@ namespace Umbraco.Web.Security { public int LoginTimeoutMinutes { get; } - public UmbracoBackOfficeCookieAuthOptions( - string[] explicitPaths, - IUmbracoContextAccessor umbracoContextAccessor, - ISecuritySettings securitySettings, - IGlobalSettings globalSettings, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - ISecureDataFormat secureDataFormat, - IRequestCache requestCache) - { - var secureDataFormat1 = secureDataFormat ?? throw new ArgumentNullException(nameof(secureDataFormat)); - LoginTimeoutMinutes = globalSettings.TimeOutInMinutes; - AuthenticationType = Constants.Security.BackOfficeAuthenticationType; - - SlidingExpiration = true; - ExpireTimeSpan = TimeSpan.FromMinutes(LoginTimeoutMinutes); - CookieDomain = securitySettings.AuthCookieDomain; - CookieName = securitySettings.AuthCookieName; - CookieHttpOnly = true; - CookieSecure = globalSettings.UseHttps ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; - - TicketDataFormat = new UmbracoSecureDataFormat(LoginTimeoutMinutes, secureDataFormat1); - - //Custom cookie manager so we can filter requests - CookieManager = new BackOfficeCookieManager(umbracoContextAccessor, runtimeState, hostingEnvironment, globalSettings, requestCache, explicitPaths); - } - /// /// Creates the cookie options for saving the auth cookie /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 22da135dd3..11965d12d9 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -319,7 +319,6 @@ - @@ -328,7 +327,6 @@ - diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 22d7bd77d7..f800707476 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -63,7 +63,6 @@ namespace Umbraco.Web protected virtual void ConfigureServices(IAppBuilder app, ServiceContext services) { app.SetUmbracoLoggerFactory(); - ConfigureUmbracoUserManager(app); } /// @@ -81,21 +80,6 @@ namespace Umbraco.Web .FinalizeMiddlewareConfiguration(); } - /// - /// Configure the Identity user manager for use with Umbraco Back office - /// - /// - protected virtual void ConfigureUmbracoUserManager(IAppBuilder app) - { - // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) - app.ConfigureUserManagerForUmbracoBackOffice( - Services, - GlobalSettings, - Mapper, - UserPasswordConfig, - IpResolver); - } - /// /// Configure external/OAuth login providers /// @@ -105,7 +89,8 @@ namespace Umbraco.Web // Ensure owin is configured for Umbraco back office authentication. // Front-end OWIN cookie configuration must be declared after this code. app - .UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) + // already moved to netcore + //.UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) .UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate) .UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authorize); } diff --git a/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs deleted file mode 100644 index 2aa2f1c52f..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/ClearAngularAntiForgeryTokenAttribute.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Web.Http.Filters; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Clears the angular csrf cookie if the request was successful - /// - public sealed class ClearAngularAntiForgeryTokenAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(HttpActionExecutedContext context) - { - if (context.Response == null) return; - if (context.Response.IsSuccessStatusCode == false) return; - - //remove the cookies - var angularCookie = new CookieHeaderValue(Core.Constants.Web.AngularCookieName, "null") - { - Expires = DateTime.Now.AddYears(-1), - //must be js readable - HttpOnly = false, - Path = "/" - }; - var validationCookie = new CookieHeaderValue(Core.Constants.Web.CsrfValidationCookieName, "null") - { - Expires = DateTime.Now.AddYears(-1), - HttpOnly = true, - Path = "/" - }; - context.Response.Headers.AddCookies(new[] { angularCookie, validationCookie }); - } - } -}