Merge branch 'v9/dev' into v9/task/more-flexible-startup

# Conflicts:
#	src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs
This commit is contained in:
Shannon
2021-08-10 13:55:55 -06:00
215 changed files with 3180 additions and 1858 deletions

View File

@@ -181,14 +181,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
else
{
var opt = _externalAuthenticationOptions.Get(authType.Name);
BackOfficeExternaLoginProviderScheme opt = await _externalAuthenticationOptions.GetAsync(authType.Name);
if (opt == null)
{
return BadRequest($"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}");
}
else
{
if (!opt.Options.AutoLinkOptions.AllowManualLinking)
if (!opt.ExternalLoginProvider.Options.AutoLinkOptions.AllowManualLinking)
{
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest();

View File

@@ -4,7 +4,6 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -17,6 +16,7 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Grid;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Serialization;
@@ -33,6 +33,7 @@ using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Umbraco.Cms.Web.BackOffice.Controllers
{
@@ -49,6 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
// this controller itself doesn't require authz but it's more clear what the intention is.
private readonly IBackOfficeUserManager _userManager;
private readonly IRuntimeState _runtimeState;
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
@@ -63,10 +65,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
private readonly IManifestParser _manifestParser;
private readonly ServerVariablesParser _serverVariables;
public BackOfficeController(
IBackOfficeUserManager userManager,
IRuntimeState runtimeState,
IRuntimeMinifier runtimeMinifier,
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment,
@@ -81,9 +85,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IBackOfficeExternalLoginProviders externalLogins,
IHttpContextAccessor httpContextAccessor,
IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions,
IManifestParser manifestParser,
ServerVariablesParser serverVariables)
{
_userManager = userManager;
_runtimeState = runtimeState;
_runtimeMinifier = runtimeMinifier;
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
@@ -98,6 +104,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_externalLogins = externalLogins;
_httpContextAccessor = httpContextAccessor;
_backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
_manifestParser = manifestParser;
_serverVariables = serverVariables;
}
@@ -105,6 +112,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[AllowAnonymous]
public async Task<IActionResult> Default()
{
// TODO: It seems that if you login during an authorize upgrade and the upgrade fails, you can still
// access the back office. This should redirect to the installer in that case?
// force authentication to occur since this is not an authorized endpoint
var result = await this.AuthenticateBackOfficeAsync();
@@ -213,7 +223,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[AllowAnonymous]
public async Task<IActionResult> Application()
{
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(_globalSettings, _hostingEnvironment);
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(
_globalSettings,
_hostingEnvironment,
_manifestParser);
return new JavaScriptResult(result);
}
@@ -225,17 +238,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public Dictionary<string, Dictionary<string, string>> LocalizedText(string culture = null)
public async Task<Dictionary<string, Dictionary<string, string>>> LocalizedText(string culture = null)
{
var isAuthenticated = _backofficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated();
CultureInfo cultureInfo;
if (string.IsNullOrWhiteSpace(culture))
{
// Force authentication to occur since this is not an authorized endpoint, we need this to get a user.
AuthenticateResult authenticationResult = await this.AuthenticateBackOfficeAsync();
// We have to get the culture from the Identity, we can't rely on thread culture
// It's entirely likely for a user to have a different culture in the backoffice, than their system.
var user = authenticationResult.Principal?.Identity;
var cultureInfo = string.IsNullOrWhiteSpace(culture)
//if the user is logged in, get their culture, otherwise default to 'en'
? isAuthenticated
//current culture is set at the very beginning of each request
? Thread.CurrentThread.CurrentCulture
: CultureInfo.GetCultureInfo(_globalSettings.DefaultUILanguage)
: CultureInfo.GetCultureInfo(culture);
cultureInfo = (authenticationResult.Succeeded && user is not null)
? user.GetCulture()
: CultureInfo.GetCultureInfo(_globalSettings.DefaultUILanguage);
}
else
{
cultureInfo = CultureInfo.GetCultureInfo(culture);
}
var allValues = _textService.GetAllStoredValues(cultureInfo);
var pathedValues = allValues.Select(kv =>
@@ -400,7 +421,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
if (ViewData.FromBase64CookieData<BackOfficeExternalLoginProviderErrors>(_httpContextAccessor.HttpContext, ViewDataExtensions.TokenExternalSignInError, _jsonSerializer) ||
ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) ||
ViewData.FromTempData(TempData, ViewDataExtensions.TokenPasswordResetCode))
{
return defaultResponse();
}
//First check if there's external login info, if there's not proceed as normal
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
@@ -430,16 +453,23 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
if (response == null) throw new ArgumentNullException(nameof(response));
// Sign in the user with this external login provider (which auto links, etc...)
var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false);
SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false);
var errors = new List<string>();
if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
if (result == SignInResult.Success)
{
// Update any authentication tokens if succeeded
await _signInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
// Check if we are in an upgrade state, if so we need to redirect
if (_runtimeState.Level == Core.RuntimeLevel.Upgrade)
{
// redirect to the the installer
return Redirect("/");
}
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired)
else if (result == SignInResult.TwoFactorRequired)
{
var attemptedUser = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
@@ -467,17 +497,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return verifyResponse;
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.LockedOut)
else if (result == SignInResult.LockedOut)
{
errors.Add($"The local user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out.");
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.NotAllowed)
else if (result == SignInResult.NotAllowed)
{
// This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails
// however since we don't enforce those rules (yet) this shouldn't happen.
errors.Add($"The user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in.");
}
else if (result == Microsoft.AspNetCore.Identity.SignInResult.Failed)
else if (result == SignInResult.Failed)
{
// Failed only occurs when the user does not exist
errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office.");
@@ -494,6 +524,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
errors.AddRange(autoLinkSignInResult.Errors);
}
else if (!result.Succeeded)
{
// this shouldn't occur, the above should catch the correct error but we'll be safe just in case
errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred.");
}
if (errors.Count > 0)
{

View File

@@ -144,7 +144,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
/// Returns the server variables for authenticated users
/// </summary>
/// <returns></returns>
internal Task<Dictionary<string, object>> GetServerVariablesAsync()
internal async Task<Dictionary<string, object>> GetServerVariablesAsync()
{
var globalSettings = _globalSettings;
var backOfficeControllerName = ControllerExtensions.GetControllerName<BackOfficeController>();
@@ -432,12 +432,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
// TODO: It would be nicer to not have to manually translate these properties
// but then needs to be changed in quite a few places in angular
"providers", _externalLogins.GetBackOfficeProviders()
"providers", (await _externalLogins.GetBackOfficeProvidersAsync())
.Select(p => new
{
authType = p.AuthenticationType,
caption = p.Name,
properties = p.Options
authType = p.ExternalLoginProvider.AuthenticationType,
caption = p.AuthenticationScheme.DisplayName,
properties = p.ExternalLoginProvider.Options
})
.ToArray()
}
@@ -456,7 +456,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
}
}
};
return Task.FromResult(defaultVals);
return defaultVals;
}
[DataContract]

View File

@@ -28,12 +28,9 @@ using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.BackOffice.ActionResults;
using Umbraco.Cms.Web.BackOffice.Authorization;
using Umbraco.Cms.Web.BackOffice.Extensions;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.ModelBinders;
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
@@ -478,6 +475,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return result;
}
private ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeysInternal(Guid[] contentTypeKeys, int parentId)
{
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList();
return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey);
}
/// <summary>
/// Gets a collection of empty content items for all document types.
/// </summary>
@@ -486,9 +490,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[OutgoingEditorModelEvent]
public ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeys([FromQuery] Guid[] contentTypeKeys, [FromQuery] int parentId)
{
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList();
return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey);
return GetEmptyByKeysInternal(contentTypeKeys, parentId);
}
/// <summary>
/// Gets a collection of empty content items for all document types.
/// </summary>
/// <remarks>
/// This is a post request in order to support a large amount of GUIDs without hitting the URL length limit.
/// </remarks>
/// <param name="contentTypeByKeys"></param>
/// <returns></returns>
[HttpPost]
[OutgoingEditorModelEvent]
public ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeys(ContentTypesByKeys contentTypeByKeys)
{
return GetEmptyByKeysInternal(contentTypeByKeys.ContentTypeKeys, contentTypeByKeys.ParentId);
}
[OutgoingEditorModelEvent]

View File

@@ -21,7 +21,6 @@ using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Core.Dashboards;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;

View File

@@ -258,16 +258,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
/// <summary>
/// Gets the URL of an entity
/// </summary>
/// <param name="udi">UDI of the entity to fetch URL for</param>
/// <param name="id">UDI of the entity to fetch URL for</param>
/// <param name="culture">The culture to fetch the URL for</param>
/// <returns>The URL or path to the item</returns>
public IActionResult GetUrl(Udi udi, string culture = "*")
public IActionResult GetUrl(Udi id, string culture = "*")
{
var intId = _entityService.GetId(udi);
var intId = _entityService.GetId(id);
if (!intId.Success)
return NotFound();
UmbracoEntityTypes entityType;
switch (udi.EntityType)
switch (id.EntityType)
{
case Constants.UdiEntityType.Document:
entityType = UmbracoEntityTypes.Document;

View File

@@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
@@ -551,7 +552,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings),
new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail });
var mailMessage = new EmailMessage(fromEmail, to.Email, emailSubject, emailBody, true);
// This needs to be in the correct mailto format including the name, else
// the name cannot be captured in the email sending notification.
// i.e. "Some Person" <hello@example.com>
var toMailBoxAddress = new MailboxAddress(to.Name, to.Email);
var mailMessage = new EmailMessage(fromEmail, toMailBoxAddress.ToString(), emailSubject, emailBody, true);
await _emailSender.SendAsync(mailMessage, true);
}

View File

@@ -11,6 +11,7 @@ using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Web.BackOffice.Authorization;
using Umbraco.Cms.Web.Common.Middleware;
namespace Umbraco.Extensions

View File

@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -31,7 +32,7 @@ namespace Umbraco.Extensions
/// <summary>
/// Adds all required components to run the Umbraco back office
/// </summary>
public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder) => builder
public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action<IMvcBuilder> configureMvc = null) => builder
.AddConfiguration()
.AddUmbracoCore()
.AddWebComponents()
@@ -42,7 +43,7 @@ namespace Umbraco.Extensions
.AddMembersIdentity()
.AddBackOfficeAuthorizationPolicies()
.AddUmbracoProfiler()
.AddMvcAndRazor()
.AddMvcAndRazor(configureMvc)
.AddWebServer()
.AddPreviewSupport()
.AddHostedServices()

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -54,18 +54,18 @@ namespace Umbraco.Extensions
/// <param name="html"></param>
/// <param name="externalLogins"></param>
/// <returns></returns>
public static Task<IHtmlContent> AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html,
public static async Task<IHtmlContent> AngularValueExternalLoginInfoScriptAsync(this IHtmlHelper html,
IBackOfficeExternalLoginProviders externalLogins,
BackOfficeExternalLoginProviderErrors externalLoginErrors)
{
var providers = externalLogins.GetBackOfficeProviders();
var providers = await externalLogins.GetBackOfficeProvidersAsync();
var loginProviders = providers
.Select(p => new
{
authType = p.AuthenticationType,
caption = p.Name,
properties = p.Options
authType = p.ExternalLoginProvider.AuthenticationType,
caption = p.AuthenticationScheme.DisplayName,
properties = p.ExternalLoginProvider.Options
})
.ToArray();
@@ -89,7 +89,7 @@ namespace Umbraco.Extensions
sb.AppendLine(JsonConvert.SerializeObject(loginProviders));
sb.AppendLine(@"});");
return Task.FromResult(html.Raw(sb.ToString()));
return html.Raw(sb.ToString());
}
/// <summary>

View File

@@ -1,4 +1,6 @@
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Extensions
@@ -10,5 +12,6 @@ namespace Umbraco.Extensions
public static BackOfficeExternalLoginProviderErrors GetExternalLoginProviderErrors(this HttpContext httpContext)
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors;
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Manifest;
using Umbraco.Cms.Core.WebAssets;
using Umbraco.Cms.Infrastructure.WebAssets;
namespace Umbraco.Extensions
{
public static class RuntimeMinifierExtensions
{
/// <summary>
/// Returns the JavaScript to load the back office's assets
/// </summary>
/// <returns></returns>
public static async Task<string> GetScriptForLoadingBackOfficeAsync(
this IRuntimeMinifier minifier,
GlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IManifestParser manifestParser)
{
var files = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
foreach(var file in await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoCoreJsBundleName))
{
files.Add(file);
}
foreach (var file in await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoExtensionsJsBundleName))
{
files.Add(file);
}
// process the independent bundles
if (manifestParser.CombinedManifest.Scripts.TryGetValue(BundleOptions.Independent, out IReadOnlyList<ManifestAssets> independentManifestAssetsList))
{
foreach (ManifestAssets manifestAssets in independentManifestAssetsList)
{
var bundleName = BackOfficeWebAssets.GetIndependentPackageBundleName(manifestAssets, AssetType.Javascript);
foreach(var asset in await minifier.GetJsAssetPathsAsync(bundleName))
{
files.Add(asset);
}
}
}
// process the "None" bundles, meaning we'll just render the script as-is
foreach (var asset in await minifier.GetJsAssetPathsAsync(BackOfficeWebAssets.UmbracoNonOptimizedPackageJsBundleName))
{
files.Add(asset);
}
var result = BackOfficeJavaScriptInitializer.GetJavascriptInitialization(
files,
"umbraco",
globalSettings,
hostingEnvironment);
result += await GetStylesheetInitializationAsync(minifier, manifestParser);
return result;
}
/// <summary>
/// Gets the back office css bundle paths and formats a JS call to lazy load them
/// </summary>
private static async Task<string> GetStylesheetInitializationAsync(
IRuntimeMinifier minifier,
IManifestParser manifestParser)
{
var files = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
foreach(var file in await minifier.GetCssAssetPathsAsync(BackOfficeWebAssets.UmbracoCssBundleName))
{
files.Add(file);
}
// process the independent bundles
if (manifestParser.CombinedManifest.Stylesheets.TryGetValue(BundleOptions.Independent, out IReadOnlyList<ManifestAssets> independentManifestAssetsList))
{
foreach (ManifestAssets manifestAssets in independentManifestAssetsList)
{
var bundleName = BackOfficeWebAssets.GetIndependentPackageBundleName(manifestAssets, AssetType.Css);
foreach (var asset in await minifier.GetCssAssetPathsAsync(bundleName))
{
files.Add(asset);
}
}
}
// process the "None" bundles, meaning we'll just render the script as-is
foreach (var asset in await minifier.GetCssAssetPathsAsync(BackOfficeWebAssets.UmbracoNonOptimizedPackageCssBundleName))
{
files.Add(asset);
}
var sb = new StringBuilder();
foreach (string file in files)
{
sb.AppendFormat("{0}LazyLoad.css('{1}');", Environment.NewLine, file);
}
return sb.ToString();
}
}
}

View File

@@ -8,6 +8,7 @@ using Umbraco.Cms.Web.BackOffice.Middleware;
using Umbraco.Cms.Web.BackOffice.Routing;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Cms.Web.Common.Extensions;
using Umbraco.Cms.Web.Common.Middleware;
namespace Umbraco.Extensions
{

View File

@@ -9,12 +9,12 @@ namespace Umbraco.Cms.Web.BackOffice.Security
/// </summary>
public class AutoLinkSignInResult : SignInResult
{
public static AutoLinkSignInResult FailedNotLinked => new AutoLinkSignInResult()
public static AutoLinkSignInResult FailedNotLinked { get; } = new AutoLinkSignInResult()
{
Succeeded = false
};
public static AutoLinkSignInResult FailedNoEmail => new AutoLinkSignInResult()
public static AutoLinkSignInResult FailedNoEmail { get; } = new AutoLinkSignInResult()
{
Succeeded = false
};

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -12,18 +13,16 @@ namespace Umbraco.Cms.Web.BackOffice.Security
/// </summary>
public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
{
private readonly BackOfficeExternalLoginProviderOptions _loginProviderOptions;
private readonly Action<BackOfficeExternalLoginProviderOptions> _loginProviderOptions;
public BackOfficeAuthenticationBuilder(IServiceCollection services, BackOfficeExternalLoginProviderOptions loginProviderOptions)
public BackOfficeAuthenticationBuilder(
IServiceCollection services,
Action<BackOfficeExternalLoginProviderOptions> loginProviderOptions = null)
: base(services)
{
_loginProviderOptions = loginProviderOptions;
}
=> _loginProviderOptions = loginProviderOptions ?? (x => { });
public string SchemeForBackOffice(string scheme)
{
return Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme;
}
=> Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme;
/// <summary>
/// Overridden to track the final authenticationScheme being registered for the external login
@@ -43,7 +42,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security
}
// add our login provider to the container along with a custom options configuration
Services.AddSingleton(x => new BackOfficeExternalLoginProvider(displayName, authenticationScheme, _loginProviderOptions));
Services.Configure(authenticationScheme, _loginProviderOptions);
base.Services.AddSingleton(services =>
{
return new BackOfficeExternalLoginProvider(
authenticationScheme,
services.GetRequiredService<IOptionsMonitor<BackOfficeExternalLoginProviderOptions>>());
});
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureBackOfficeScheme<TOptions>>());
return base.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);

View File

@@ -2,7 +2,9 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
@@ -23,6 +25,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
private readonly IRuntimeState _runtime;
private readonly string[] _explicitPaths;
private readonly UmbracoRequestPaths _umbracoRequestPaths;
private readonly IBasicAuthService _basicAuthService;
/// <summary>
/// Initializes a new instance of the <see cref="BackOfficeCookieManager"/> class.
@@ -30,8 +33,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security
public BackOfficeCookieManager(
IUmbracoContextAccessor umbracoContextAccessor,
IRuntimeState runtime,
UmbracoRequestPaths umbracoRequestPaths)
: this(umbracoContextAccessor, runtime, null, umbracoRequestPaths)
UmbracoRequestPaths umbracoRequestPaths,
IBasicAuthService basicAuthService)
: this(umbracoContextAccessor, runtime, null, umbracoRequestPaths, basicAuthService)
{
}
@@ -42,12 +46,14 @@ namespace Umbraco.Cms.Web.BackOffice.Security
IUmbracoContextAccessor umbracoContextAccessor,
IRuntimeState runtime,
IEnumerable<string> explicitPaths,
UmbracoRequestPaths umbracoRequestPaths)
UmbracoRequestPaths umbracoRequestPaths,
IBasicAuthService basicAuthService)
{
_umbracoContextAccessor = umbracoContextAccessor;
_runtime = runtime;
_explicitPaths = explicitPaths?.ToArray();
_umbracoRequestPaths = umbracoRequestPaths;
_basicAuthService = basicAuthService;
}
/// <summary>
@@ -88,6 +94,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security
return true;
}
if (_basicAuthService.IsBasicAuthEnabled())
{
return true;
}
return false;
}

View File

@@ -0,0 +1,20 @@
using System;
using Microsoft.AspNetCore.Authentication;
namespace Umbraco.Cms.Web.BackOffice.Security
{
public class BackOfficeExternaLoginProviderScheme
{
public BackOfficeExternaLoginProviderScheme(
BackOfficeExternalLoginProvider externalLoginProvider,
AuthenticationScheme authenticationScheme)
{
ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider));
AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));
}
public BackOfficeExternalLoginProvider ExternalLoginProvider { get; }
public AuthenticationScheme AuthenticationScheme { get; }
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System;
using Microsoft.Extensions.Options;
namespace Umbraco.Cms.Web.BackOffice.Security
{
@@ -7,33 +8,29 @@ namespace Umbraco.Cms.Web.BackOffice.Security
/// </summary>
public class BackOfficeExternalLoginProvider : IEquatable<BackOfficeExternalLoginProvider>
{
public BackOfficeExternalLoginProvider(string name, string authenticationType, BackOfficeExternalLoginProviderOptions properties)
public BackOfficeExternalLoginProvider(
string authenticationType,
IOptionsMonitor<BackOfficeExternalLoginProviderOptions> properties)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
if (properties is null)
{
throw new ArgumentNullException(nameof(properties));
}
AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType));
Options = properties ?? throw new ArgumentNullException(nameof(properties));
Options = properties.Get(authenticationType);
}
public string Name { get; }
/// <summary>
/// The authentication "Scheme"
/// </summary>
public string AuthenticationType { get; }
public BackOfficeExternalLoginProviderOptions Options { get; }
public override bool Equals(object obj)
{
return Equals(obj as BackOfficeExternalLoginProvider);
}
public bool Equals(BackOfficeExternalLoginProvider other)
{
return other != null &&
Name == other.Name &&
AuthenticationType == other.AuthenticationType;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, AuthenticationType);
}
public override bool Equals(object obj) => Equals(obj as BackOfficeExternalLoginProvider);
public bool Equals(BackOfficeExternalLoginProvider other) => other != null && AuthenticationType == other.AuthenticationType;
public override int GetHashCode() => HashCode.Combine(AuthenticationType);
}
}

View File

@@ -1,14 +1,13 @@
namespace Umbraco.Cms.Web.BackOffice.Security
namespace Umbraco.Cms.Web.BackOffice.Security
{
/// <summary>
/// Options used to configure back office external login providers
/// </summary>
public class BackOfficeExternalLoginProviderOptions
{
public BackOfficeExternalLoginProviderOptions(
string buttonStyle, string icon,
string buttonStyle,
string icon,
ExternalSignInAutoLinkOptions autoLinkOptions = null,
bool denyLocalLogin = false,
bool autoRedirectLoginToExternalProvider = false,
@@ -22,18 +21,23 @@
CustomBackOfficeView = customBackOfficeView;
}
public string ButtonStyle { get; }
public string Icon { get; }
public BackOfficeExternalLoginProviderOptions()
{
}
public string ButtonStyle { get; set; } = "btn-openid";
public string Icon { get; set; } = "fa fa-user";
/// <summary>
/// Options used to control how users can be auto-linked/created/updated based on the external login provider
/// </summary>
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; }
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions();
/// <summary>
/// When set to true will disable all local user login functionality
/// </summary>
public bool DenyLocalLogin { get; }
public bool DenyLocalLogin { get; set; }
/// <summary>
/// When specified this will automatically redirect to the OAuth login provider instead of prompting the user to click on the OAuth button first.
@@ -42,7 +46,7 @@
/// This is generally used in conjunction with <see cref="DenyLocalLogin"/>. If more than one OAuth provider specifies this, the last registered
/// provider's redirect settings will win.
/// </remarks>
public bool AutoRedirectLoginToExternalProvider { get; }
public bool AutoRedirectLoginToExternalProvider { get; set; }
/// <summary>
/// A virtual path to a custom angular view that is used to replace the entire UI that renders the external login button that the user interacts with
@@ -51,6 +55,6 @@
/// If this view is specified it is 100% up to the user to render the html responsible for rendering the link/un-link buttons along with showing any errors
/// that occur. This overrides what Umbraco normally does by default.
/// </remarks>
public string CustomBackOfficeView { get; }
public string CustomBackOfficeView { get; set; }
}
}

View File

@@ -1,41 +1,71 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
namespace Umbraco.Cms.Web.BackOffice.Security
{
/// <inheritdoc />
public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
{
public BackOfficeExternalLoginProviders(IEnumerable<BackOfficeExternalLoginProvider> externalLogins)
private readonly Dictionary<string, BackOfficeExternalLoginProvider> _externalLogins;
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
public BackOfficeExternalLoginProviders(
IEnumerable<BackOfficeExternalLoginProvider> externalLogins,
IAuthenticationSchemeProvider authenticationSchemeProvider)
{
_externalLogins = externalLogins;
_externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType);
_authenticationSchemeProvider = authenticationSchemeProvider;
}
private readonly IEnumerable<BackOfficeExternalLoginProvider> _externalLogins;
/// <inheritdoc />
public BackOfficeExternalLoginProvider Get(string authenticationType)
public async Task<BackOfficeExternaLoginProviderScheme> GetAsync(string authenticationType)
{
return _externalLogins.FirstOrDefault(x => x.AuthenticationType == authenticationType);
if (!_externalLogins.TryGetValue(authenticationType, out BackOfficeExternalLoginProvider provider))
{
return null;
}
// get the associated scheme
AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType);
if (associatedScheme == null)
{
throw new InvalidOperationException("No authentication scheme registered for " + provider.AuthenticationType);
}
return new BackOfficeExternaLoginProviderScheme(provider, associatedScheme);
}
/// <inheritdoc />
public string GetAutoLoginProvider()
{
var found = _externalLogins.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList();
var found = _externalLogins.Values.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList();
return found.Count > 0 ? found[0].AuthenticationType : null;
}
/// <inheritdoc />
public IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders()
public async Task<IEnumerable<BackOfficeExternaLoginProviderScheme>> GetBackOfficeProvidersAsync()
{
return _externalLogins;
var providersWithSchemes = new List<BackOfficeExternaLoginProviderScheme>();
foreach (BackOfficeExternalLoginProvider login in _externalLogins.Values)
{
// get the associated scheme
AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType);
providersWithSchemes.Add(new BackOfficeExternaLoginProviderScheme(login, associatedScheme));
}
return providersWithSchemes;
}
/// <inheritdoc />
public bool HasDenyLocalLogin()
{
var found = _externalLogins.Where(x => x.Options.DenyLocalLogin).ToList();
var found = _externalLogins.Values.Where(x => x.Options.DenyLocalLogin).ToList();
return found.Count > 0;
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Umbraco.Cms.Web.BackOffice.Security
@@ -21,9 +21,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security
/// <param name="loginProviderOptions"></param>
/// <param name="build"></param>
/// <returns></returns>
public BackOfficeExternalLoginsBuilder AddBackOfficeLogin(
BackOfficeExternalLoginProviderOptions loginProviderOptions,
Action<BackOfficeAuthenticationBuilder> build)
public BackOfficeExternalLoginsBuilder AddBackOfficeLogin(
Action<BackOfficeAuthenticationBuilder> build,
Action<BackOfficeExternalLoginProviderOptions> loginProviderOptions = null)
{
build(new BackOfficeAuthenticationBuilder(_services, loginProviderOptions));
return this;

View File

@@ -64,7 +64,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// to be able to deal with auto-linking and reduce duplicate lookups
var autoLinkOptions = _externalLogins.Get(loginInfo.LoginProvider)?.Options?.AutoLinkOptions;
var autoLinkOptions = (await _externalLogins.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider?.Options?.AutoLinkOptions;
var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
if (user == null)
{

View File

@@ -34,6 +34,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security
private readonly IIpResolver _ipResolver;
private readonly ISystemClock _systemClock;
private readonly UmbracoRequestPaths _umbracoRequestPaths;
private readonly IBasicAuthService _basicAuthService;
private readonly IOptionsMonitor<BasicAuthSettings> _optionsSnapshot;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureBackOfficeCookieOptions"/> class.
@@ -59,7 +61,8 @@ namespace Umbraco.Cms.Web.BackOffice.Security
IUserService userService,
IIpResolver ipResolver,
ISystemClock systemClock,
UmbracoRequestPaths umbracoRequestPaths)
UmbracoRequestPaths umbracoRequestPaths,
IBasicAuthService basicAuthService)
{
_serviceProvider = serviceProvider;
_umbracoContextAccessor = umbracoContextAccessor;
@@ -72,6 +75,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
_ipResolver = ipResolver;
_systemClock = systemClock;
_umbracoRequestPaths = umbracoRequestPaths;
_basicAuthService = basicAuthService;
}
/// <inheritdoc />
@@ -115,7 +119,9 @@ namespace Umbraco.Cms.Web.BackOffice.Security
options.CookieManager = new BackOfficeCookieManager(
_umbracoContextAccessor,
_runtimeState,
_umbracoRequestPaths);
_umbracoRequestPaths,
_basicAuthService
);
options.Events = new CookieAuthenticationEvents
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Umbraco.Cms.Web.BackOffice.Security
{
@@ -13,13 +14,13 @@ namespace Umbraco.Cms.Web.BackOffice.Security
/// </summary>
/// <param name="authenticationType"></param>
/// <returns></returns>
BackOfficeExternalLoginProvider Get(string authenticationType);
Task<BackOfficeExternaLoginProviderScheme> GetAsync(string authenticationType);
/// <summary>
/// Get all registered <see cref="BackOfficeExternalLoginProvider"/>
/// </summary>
/// <returns></returns>
IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders();
Task<IEnumerable<BackOfficeExternaLoginProviderScheme>> GetBackOfficeProvidersAsync();
/// <summary>
/// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option

View File

@@ -1,27 +0,0 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.BackOffice.Security
{
/// <summary>
/// A <see cref="SignInManager{BackOfficeIdentityUser}"/> for the back office with a <seealso cref="BackOfficeIdentityUser"/>
/// </summary>
public interface IBackOfficeSignInManager
{
AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null);
Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false);
Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync();
Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null);
Task<BackOfficeIdentityUser> GetTwoFactorAuthenticationUserAsync();
Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure);
Task SignOutAsync();
Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string authenticationMethod = null);
Task<ClaimsPrincipal> CreateUserPrincipalAsync(BackOfficeIdentityUser user);
Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient);
Task<IdentityResult> UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin);
}
}

View File

@@ -0,0 +1,78 @@
using System.IO;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Trees;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Trees
{
[Tree(Constants.Applications.Settings, "staticFiles", TreeTitle = "Static Files", TreeUse = TreeUse.Dialog)]
public class StaticFilesTreeController : TreeController
{
private readonly IFileSystem _fileSystem;
private const string AppPlugins = "App_Plugins";
private const string Webroot = "wwwroot";
public StaticFilesTreeController(ILocalizedTextService localizedTextService,
UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, IEventAggregator eventAggregator,
IPhysicalFileSystem fileSystem) :
base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator)
{
_fileSystem = fileSystem;
}
protected override ActionResult<TreeNodeCollection> GetTreeNodes(string id, FormCollection queryStrings)
{
var path = string.IsNullOrEmpty(id) == false && id != Constants.System.RootString
? WebUtility.UrlDecode(id).TrimStart("/")
: "";
var nodes = new TreeNodeCollection();
var directories = _fileSystem.GetDirectories(path);
foreach (var directory in directories)
{
// We don't want any other directories under the root node other than the ones serving static files - App_Plugins and wwwroot
if (id == Constants.System.RootString && directory != AppPlugins && directory != Webroot)
{
continue;
}
var hasChildren = _fileSystem.GetFiles(directory).Any() || _fileSystem.GetDirectories(directory).Any();
var name = Path.GetFileName(directory);
var node = CreateTreeNode(WebUtility.UrlEncode(directory), path, queryStrings, name, "icon-folder", hasChildren);
if (node != null)
{
nodes.Add(node);
}
}
// Only get the files inside App_Plugins and wwwroot
var files = _fileSystem.GetFiles(path).Where(x => x.StartsWith(AppPlugins) || x.StartsWith(Webroot));
foreach (var file in files)
{
var name = Path.GetFileName(file);
var node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, "icon-document", false);
if (node != null)
{
nodes.Add(node);
}
}
return nodes;
}
// We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI
protected override ActionResult<MenuItemCollection> GetMenuForNode(string id, FormCollection queryStrings) => null;
}
}