Adds new event so we know when umbraco routes a value, ensure the IUmbracoWebsiteSecurity is initialized for front-end requests, cleans up some of the routing middleware, adds lots of notes

This commit is contained in:
Shannon
2021-03-01 12:51:07 +11:00
parent abb5911b24
commit 6148336d04
23 changed files with 180 additions and 85 deletions

View File

@@ -53,6 +53,7 @@ using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Web.Common.Templates;
using Umbraco.Cms.Web.Common.UmbracoContext;
using Umbraco.Core.Security;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Extensions
@@ -263,6 +264,7 @@ namespace Umbraco.Extensions
builder.Services.AddUnique<IUmbracoContextFactory, UmbracoContextFactory>();
builder.Services.AddUnique<IBackOfficeSecurityFactory, BackOfficeSecurityFactory>();
builder.Services.AddUnique<IBackOfficeSecurityAccessor, HybridBackofficeSecurityAccessor>();
builder.AddNotificationHandler<UmbracoRoutedRequest, UmbracoWebsiteSecurityFactory>();
builder.Services.AddUnique<IUmbracoWebsiteSecurityAccessor, HybridUmbracoWebsiteSecurityAccessor>();
var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList();

View File

@@ -10,9 +10,11 @@ using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.PublishedCache;
using Umbraco.Cms.Web.Common.Profiler;
using Umbraco.Core.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Middleware
@@ -83,27 +85,26 @@ namespace Umbraco.Cms.Web.Common.Middleware
EnsureContentCacheInitialized();
_backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext, TODO: Why?
// TODO: This dependency chain is broken and needs to be fixed.
// This is required to be called before EnsureUmbracoContext else the UmbracoContext's IBackOfficeSecurity instance is null
// This is ugly Temporal Coupling which also means that developers can no longer just use IUmbracoContextFactory the
// way it was intended.
_backofficeSecurityFactory.EnsureBackOfficeSecurity();
UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
Uri currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request);
_hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl);
bool isFrontEndRequest = umbracoContextReference.UmbracoContext.IsFrontEndUmbracoRequest();
var pathAndQuery = context.Request.GetEncodedPathAndQuery();
try
{
if (isFrontEndRequest)
{
LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache);
_logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, pathAndQuery);
}
// Verbose log start of every request
LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache);
_logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, pathAndQuery);
try
{
{
await _eventAggregator.PublishAsync(new UmbracoRequestBegin(umbracoContextReference.UmbracoContext));
}
catch (Exception ex)
@@ -126,11 +127,10 @@ namespace Umbraco.Cms.Web.Common.Middleware
}
finally
{
if (isFrontEndRequest)
{
LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache);
_logger.LogTrace("End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds);
}
// Verbose log end of every request (in v8 we didn't log the end request of ALL requests, only the front-end which was
// strange since we always logged the beginning, so now we just log start/end of all requests)
LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache);
_logger.LogTrace("End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds);
try
{

View File

@@ -1,11 +1,11 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
// TODO: This is only for the back office, does it need to be in common?
// TODO: This is only for the back office, does it need to be in common? YES currently UmbracoContext has an transitive dependency on this which needs to be fixed/reviewed.
public class BackOfficeSecurityFactory: IBackOfficeSecurityFactory
{
@@ -14,11 +14,11 @@ namespace Umbraco.Cms.Web.Common.Security
private readonly IHttpContextAccessor _httpContextAccessor;
public BackOfficeSecurityFactory(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IUserService userService,
IHttpContextAccessor httpContextAccessor)
{
_backOfficeSecurityAccessor = backofficeSecurityAccessor;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_userService = userService;
_httpContextAccessor = httpContextAccessor;
}

View File

@@ -0,0 +1,242 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Security;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Web.Common.Security
{
public class UmbracoWebsiteSecurity : IUmbracoWebsiteSecurity
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMemberService _memberService;
private readonly IMemberTypeService _memberTypeService;
private readonly IShortStringHelper _shortStringHelper;
public UmbracoWebsiteSecurity(IHttpContextAccessor httpContextAccessor,
IMemberService memberService,
IMemberTypeService memberTypeService,
IShortStringHelper shortStringHelper)
{
_httpContextAccessor = httpContextAccessor;
_memberService = memberService;
_memberTypeService = memberTypeService;
_shortStringHelper = shortStringHelper;
}
/// <inheritdoc/>
public RegisterModel CreateRegistrationModel(string memberTypeAlias = null)
{
var providedOrDefaultMemberTypeAlias = memberTypeAlias ?? Core.Constants.Conventions.MemberTypes.DefaultAlias;
var memberType = _memberTypeService.Get(providedOrDefaultMemberTypeAlias);
if (memberType == null)
{
throw new InvalidOperationException($"Could not find a member type with alias: {providedOrDefaultMemberTypeAlias}.");
}
var model = RegisterModel.CreateModel();
model.MemberTypeAlias = providedOrDefaultMemberTypeAlias;
model.MemberProperties = GetMemberPropertiesViewModel(memberType);
return model;
}
private List<UmbracoProperty> GetMemberPropertiesViewModel(IMemberType memberType, IMember member = null)
{
var viewProperties = new List<UmbracoProperty>();
var builtIns = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
var propertyTypes = memberType.PropertyTypes
.Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias))
.OrderBy(p => p.SortOrder);
foreach (var prop in propertyTypes)
{
var value = string.Empty;
if (member != null)
{
var propValue = member.Properties[prop.Alias];
if (propValue != null && propValue.GetValue() != null)
{
value = propValue.GetValue().ToString();
}
}
var viewProperty = new UmbracoProperty
{
Alias = prop.Alias,
Name = prop.Name,
Value = value
};
// TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers
// can just render their own.
////This is a rudimentary check to see what data template we should render
//// if developers want to change the template they can do so dynamically in their views or controllers
//// for a given property.
////These are the default built-in MVC template types: “Boolean”, “Decimal”, “EmailAddress”, “HiddenInput”, “HTML”, “Object”, “String”, “Text”, and “Url”
//// by default we'll render a text box since we've defined that metadata on the UmbracoProperty.Value property directly.
//if (prop.DataTypeId == new Guid(Constants.PropertyEditors.TrueFalse))
//{
// viewProperty.EditorTemplate = "UmbracoBoolean";
//}
//else
//{
// switch (prop.DataTypeDatabaseType)
// {
// case DataTypeDatabaseType.Integer:
// viewProperty.EditorTemplate = "Decimal";
// break;
// case DataTypeDatabaseType.Ntext:
// viewProperty.EditorTemplate = "Text";
// break;
// case DataTypeDatabaseType.Date:
// case DataTypeDatabaseType.Nvarchar:
// break;
// }
//}
viewProperties.Add(viewProperty);
}
return viewProperties;
}
public Task<RegisterMemberStatus> RegisterMemberAsync(RegisterModel model, bool logMemberIn = true)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public async Task<ProfileModel> GetCurrentMemberProfileModelAsync()
{
if (IsLoggedIn() == false)
{
return null;
}
var member = GetCurrentPersistedMember();
// This shouldn't happen but will if the member is deleted in the back office while the member is trying
// to use the front-end!
if (member == null)
{
// Log them out since they've been removed
await LogOutAsync();
return null;
}
var model = new ProfileModel
{
Name = member.Name,
MemberTypeAlias = member.ContentTypeAlias,
// TODO: get ASP.NET Core Identity equiavlant of MemberShipUser in order to get common membership properties such as Email
// and UserName (see MembershipProviderExtensions.GetCurrentUserName()for legacy membership provider implementation).
//Email = membershipUser.Email,
//UserName = membershipUser.UserName,
//Comment = membershipUser.Comment,
//IsApproved = membershipUser.IsApproved,
//IsLockedOut = membershipUser.IsLockedOut,
//LastLockoutDate = membershipUser.LastLockoutDate,
//CreationDate = membershipUser.CreationDate,
//LastLoginDate = membershipUser.LastLoginDate,
//LastActivityDate = membershipUser.LastActivityDate,
//LastPasswordChangedDate = membershipUser.LastPasswordChangedDate
};
var memberType = _memberTypeService.Get(member.ContentTypeId);
model.MemberProperties = GetMemberPropertiesViewModel(memberType, member);
return model;
}
/// <inheritdoc/>
public Task<UpdateMemberProfileResult> UpdateMemberProfileAsync(ProfileModel model)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public bool IsLoggedIn()
{
var httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User != null && httpContext.User.Identity.IsAuthenticated;
}
/// <inheritdoc/>
public async Task<LoginStatusModel> GetCurrentLoginStatusAsync()
{
var model = LoginStatusModel.CreateModel();
if (IsLoggedIn() == false)
{
model.IsLoggedIn = false;
return model;
}
var member = GetCurrentPersistedMember();
// This shouldn't happen but will if the member is deleted in the back office while the member is trying
// to use the front-end!
if (member == null)
{
// Log them out since they've been removed.
await LogOutAsync();
model.IsLoggedIn = false;
return model;
}
model.Name = member.Name;
model.Username = member.Username;
model.Email = member.Email;
model.IsLoggedIn = true;
return model;
}
/// <summary>
/// Returns the currently logged in IMember object - this should never be exposed to the front-end since it's returning a business logic entity!
/// </summary>
/// <returns></returns>
private IMember GetCurrentPersistedMember()
{
// TODO: get user name from ASP.NET Core Identity (see MembershipProviderExtensions.GetCurrentUserName()
// for legacy membership provider implementation).
var username = "";
// The result of this is cached by the MemberRepository
return _memberService.GetByUsername(username);
}
/// <inheritdoc/>
public Task<bool> LoginAsync(string username, string password)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public async Task LogOutAsync()
{
await _httpContextAccessor.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
/// <inheritdoc/>
public bool IsMemberAuthorized(IEnumerable<string> allowTypes = null, IEnumerable<string> allowGroups = null, IEnumerable<int> allowMembers = null)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Web.Common.Security
{
/// <summary>
/// Ensures that the <see cref="IUmbracoWebsiteSecurity"/> is populated on a front-end request
/// </summary>
internal sealed class UmbracoWebsiteSecurityFactory : INotificationHandler<UmbracoRoutedRequest>
{
private readonly IUmbracoWebsiteSecurityAccessor _umbracoWebsiteSecurityAccessor;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMemberService _memberService;
private readonly IMemberTypeService _memberTypeService;
private readonly IShortStringHelper _shortStringHelper;
public UmbracoWebsiteSecurityFactory(
IUmbracoWebsiteSecurityAccessor umbracoWebsiteSecurityAccessor,
IHttpContextAccessor httpContextAccessor,
IMemberService memberService,
IMemberTypeService memberTypeService,
IShortStringHelper shortStringHelper)
{
_umbracoWebsiteSecurityAccessor = umbracoWebsiteSecurityAccessor;
_httpContextAccessor = httpContextAccessor;
_memberService = memberService;
_memberTypeService = memberTypeService;
_shortStringHelper = shortStringHelper;
}
public void Handle(UmbracoRoutedRequest notification)
{
if (_umbracoWebsiteSecurityAccessor.WebsiteSecurity is null)
{
_umbracoWebsiteSecurityAccessor.WebsiteSecurity = new UmbracoWebsiteSecurity(
_httpContextAccessor,
_memberService,
_memberTypeService,
_shortStringHelper);
}
}
}
}