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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
242
src/Umbraco.Web.Common/Security/UmbracoWebsiteSecurity.cs
Normal file
242
src/Umbraco.Web.Common/Security/UmbracoWebsiteSecurity.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user