It builds!

This commit is contained in:
Shannon
2020-10-23 14:18:53 +11:00
parent 1400a02798
commit 64d8b56eca
80 changed files with 601 additions and 2356 deletions

View File

@@ -31,6 +31,7 @@ using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
using Constants = Umbraco.Core.Constants;
using Microsoft.AspNetCore.Identity;
using Umbraco.Web.Editors.Filters;
namespace Umbraco.Web.BackOffice.Controllers
{
@@ -63,6 +64,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly Core.Hosting.IHostingEnvironment _hostingEnvironment;
private readonly IRequestAccessor _requestAccessor;
private readonly LinkGenerator _linkGenerator;
private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions;
// 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
@@ -83,7 +85,8 @@ namespace Umbraco.Web.BackOffice.Controllers
ISmsSender smsSender,
Core.Hosting.IHostingEnvironment hostingEnvironment,
IRequestAccessor requestAccessor,
LinkGenerator linkGenerator)
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalAuthenticationOptions)
{
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_userManager = backOfficeUserManager;
@@ -101,6 +104,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_hostingEnvironment = hostingEnvironment;
_requestAccessor = requestAccessor;
_linkGenerator = linkGenerator;
_externalAuthenticationOptions = externalAuthenticationOptions;
}
/// <summary>
@@ -123,6 +127,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// This will also update the security stamp for the user so it can only be used once
/// </remarks>
[ValidateAngularAntiForgeryToken]
[DenyLocalLoginAuthorization]
public async Task<ActionResult<UserDisplay>> PostVerifyInvite([FromQuery] int id, [FromQuery] string token)
{
if (string.IsNullOrWhiteSpace(token))
@@ -154,11 +159,29 @@ namespace Umbraco.Web.BackOffice.Controllers
[UmbracoAuthorize]
[ValidateAngularAntiForgeryToken]
public async Task<ActionResult> PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel)
public async Task<IActionResult> PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel)
{
var user = await _userManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null) throw new InvalidOperationException("Could not find user");
ExternalSignInAutoLinkOptions autoLinkOptions = null;
var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.FirstOrDefault(x => x.Name == unlinkLoginModel.LoginProvider);
if (authType == null)
{
_logger.LogWarning("Could not find external authentication provider registered: {LoginProvider}", unlinkLoginModel.LoginProvider);
}
else
{
autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name);
if (!autoLinkOptions.AllowManualLinking)
{
// If AllowManualLinking is disabled for this provider we cannot unlink
return BadRequest();
}
}
var result = await _userManager.RemoveLoginAsync(
user,
unlinkLoginModel.LoginProvider,
@@ -243,6 +266,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </remarks>
[UmbracoAuthorize(redirectToUmbracoLogin: false, requireApproval: false)]
[SetAngularAntiForgeryTokens]
[DenyLocalLoginAuthorization]
public ActionResult<UserDetail> GetCurrentInvitedUser()
{
var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
@@ -266,6 +290,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[DenyLocalLoginAuthorization]
public async Task<UserDetail> PostLogin(LoginModel loginModel)
{
// Sign the user in with username/password, this also gives a chance for developers to
@@ -332,6 +357,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </summary>
/// <returns></returns>
[SetAngularAntiForgeryTokens]
[DenyLocalLoginAuthorization]
public async Task<IActionResult> PostRequestPasswordReset(RequestPasswordResetModel model)
{
// If this feature is switched off in configuration the UI will be amended to not make the request to reset password available.
@@ -546,11 +572,19 @@ namespace Umbraco.Web.BackOffice.Controllers
[ValidateAngularAntiForgeryToken]
public IActionResult PostLogout()
{
HttpContext.SignOutAsync(Core.Constants.Security.BackOfficeAuthenticationType);
HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
_logger.LogInformation("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()));
var userId = int.Parse(User.Identity.GetUserId());
var args = _userManager.RaiseLogoutSuccessEvent(User, userId);
if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace())
{
return new ObjectResult(new
{
signOutRedirectUrl = args.SignOutRedirectUrl
});
}
return Ok();
}

View File

@@ -31,6 +31,7 @@ using Umbraco.Web.WebAssets;
using Constants = Umbraco.Core.Constants;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
using Umbraco.Web.Security;
namespace Umbraco.Web.BackOffice.Controllers
{
@@ -50,7 +51,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly ILogger<BackOfficeController> _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IExternalAuthenticationOptions _externalAuthenticationOptions;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
public BackOfficeController(
IBackOfficeUserManager userManager,
@@ -65,7 +66,7 @@ namespace Umbraco.Web.BackOffice.Controllers
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
ILogger<BackOfficeController> logger,
IJsonSerializer jsonSerializer,
IExternalAuthenticationOptions externalAuthenticationOptions)
IBackOfficeExternalLoginProviders externalLogins)
{
_userManager = userManager;
_runtimeMinifier = runtimeMinifier;
@@ -79,7 +80,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_logger = logger;
_jsonSerializer = jsonSerializer;
_externalAuthenticationOptions = externalAuthenticationOptions;
_externalLogins = externalLogins;
}
[HttpGet]
@@ -260,6 +261,9 @@ namespace Umbraco.Web.BackOffice.Controllers
}
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
// TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think
// we validate against that key?
// see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48
return Challenge(properties, provider);
}
@@ -275,6 +279,9 @@ namespace Umbraco.Web.BackOffice.Controllers
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action(nameof(ExternalLinkLoginCallback), this.GetControllerName());
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, User.Identity.GetUserId());
// TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think
// we validate against that key?
// see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48
return Challenge(properties, provider);
}
@@ -305,6 +312,11 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public async Task<IActionResult> ExternalLinkLoginCallback()
{
// TODO: Do we need/want to tell it an expected xsrf.
// In v8 the xsrf used to be set to the user id which was verified manually, in this case I think we don't specify
// the key and that is up to the underlying sign in manager to set so we'd just tell it to expect the user id,
// the XSRF value used to be set in our ChallengeResult but now we don't have that so this needs to be set in the
// BackOfficeController when we issue a Challenge, see TODO notes there.
var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
@@ -322,8 +334,8 @@ namespace Umbraco.Web.BackOffice.Controllers
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
}
var result2 = await _userManager.AddLoginAsync(user, loginInfo);
if (result2.Succeeded)
var addLoginResult = await _userManager.AddLoginAsync(user, loginInfo);
if (addLoginResult.Succeeded)
{
// Update any authentication tokens if login succeeded
// TODO: This is a new thing that we need to implement and because we can store data with the external login now, this is exactly
@@ -334,7 +346,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//Add errors and redirect for it to be displayed
TempData[ViewDataExtensions.TokenExternalSignInError] = result2.Errors;
TempData[ViewDataExtensions.TokenExternalSignInError] = addLoginResult.Errors;
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
}
@@ -352,15 +364,27 @@ namespace Umbraco.Web.BackOffice.Controllers
ViewData.SetUmbracoPath(_globalSettings.GetUmbracoMvcArea(_hostingEnvironment));
//check if there is the TempData with the any token name specified, if so, assign to view bag and render the view
if (ViewData.FromTempData(TempData, ViewDataExtensions.TokenExternalSignInError) ||
//check if there is the TempData or cookies with the any token name specified, if so, assign to view bag and render the view
if (ViewData.FromBase64CookieData<BackOfficeExternalLoginProviderErrors>(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();
if (loginInfo == null || loginInfo.Principal == null)
{
// if the user is not logged in, check if there's any auto login redirects specified
if (!_backofficeSecurityAccessor.BackOfficeSecurity.ValidateCurrentUser())
{
var oauthRedirectAuthProvider = _externalLogins.GetAutoLoginProvider();
if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace())
{
return ExternalLogin(oauthRedirectAuthProvider);
}
}
return defaultResponse();
}
@@ -383,7 +407,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
else
{
autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name);
autoLinkOptions = _externalLogins.Get(authType.Name);
}
// Sign in the user with this external login provider if the user already has a login
@@ -391,12 +415,6 @@ namespace Umbraco.Web.BackOffice.Controllers
var user = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
if (user != null)
{
// TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we
// wouldn't necessarily sign the user in here with the standard login, instead we'd update the
// UseUmbracoBackOfficeExternalCookieAuthentication extension method to have the correct provider and claims factory,
// ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure
// that the ticket is created and stored and that the user is logged in.
var shouldSignIn = true;
if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
{
@@ -417,7 +435,10 @@ namespace Umbraco.Web.BackOffice.Controllers
{
if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false)
{
ViewData.SetExternalSignInError(new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" });
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" }));
}
//Remove the cookie otherwise this message will keep appearing
@@ -440,7 +461,10 @@ namespace Umbraco.Web.BackOffice.Controllers
//we are allowing auto-linking/creating of local accounts
if (email.IsNullOrWhiteSpace())
{
ViewData.SetExternalSignInError(new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." });
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." }));
}
else
{
@@ -448,13 +472,26 @@ namespace Umbraco.Web.BackOffice.Controllers
var autoLinkUser = await _userManager.FindByEmailAsync(email);
if (autoLinkUser != null)
{
// TODO This will be filled out with 8.9 changes
throw new NotImplementedException("Merge 8.9 changes in!");
try
{
//call the callback if one is assigned
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
return true;
}
await LinkUser(autoLinkUser, loginInfo);
}
else
{
var name = loginInfo.Principal?.Identity?.Name;
if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name);
@@ -465,40 +502,76 @@ namespace Umbraco.Web.BackOffice.Controllers
}
//call the callback if one is assigned
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
try
{
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
return true;
}
var userCreationResult = await _userManager.CreateAsync(autoLinkUser);
if (userCreationResult.Succeeded == false)
{
ViewData.SetExternalSignInError(userCreationResult.Errors.Select(x => x.Description).ToList());
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
userCreationResult.Errors.Select(x => x.Description).ToList()));
}
else
{
var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
if (linkResult.Succeeded == false)
{
ViewData.SetExternalSignInError(linkResult.Errors.Select(x => x.Description).ToList());
//If this fails, we should really delete the user since it will be in an inconsistent state!
var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
if (deleteResult.Succeeded == false)
{
//DOH! ... this isn't good, combine all errors to be shown
ViewData.SetExternalSignInError(linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList());
}
}
else
{
//sign in
await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
}
await LinkUser(autoLinkUser, loginInfo);
}
}
}
return true;
}
private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
{
var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser);
var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
// if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
if (exists != null)
{
//sign in
await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
return;
}
var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
if (linkResult.Succeeded)
{
//we're good! sign in
await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
return;
}
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
linkResult.Errors.Select(x => x.Description).ToList()));
//If this fails, we should really delete the user since it will be in an inconsistent state!
var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
if (!deleteResult.Succeeded)
{
//DOH! ... this isn't good, combine all errors to be shown
ViewData.SetExternalSignInProviderErrors(
new BackOfficeExternalLoginProviderErrors(
loginInfo.LoginProvider,
linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList()));
}
}
// Used for XSRF protection when adding external logins
// TODO: This is duplicated in BackOfficeSignInManager
private const string XsrfKey = "XsrfId";

View File

@@ -19,6 +19,7 @@ using Umbraco.Web.BackOffice.Profiling;
using Umbraco.Web.BackOffice.PropertyEditors;
using Umbraco.Web.BackOffice.Routing;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Security;
using Umbraco.Web.Editors;
using Umbraco.Web.Features;
using Umbraco.Web.Models.ContentEditing;
@@ -44,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly RuntimeSettings _runtimeSettings;
private readonly SecuritySettings _securitySettings;
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly PreviewRoutes _previewRoutes;
@@ -61,7 +62,7 @@ namespace Umbraco.Web.BackOffice.Controllers
IOptions<RuntimeSettings> runtimeSettings,
IOptions<SecuritySettings> securitySettings,
IRuntimeMinifier runtimeMinifier,
IAuthenticationSchemeProvider authenticationSchemeProvider,
IBackOfficeExternalLoginProviders externalLogins,
IImageUrlGenerator imageUrlGenerator,
PreviewRoutes previewRoutes)
{
@@ -77,7 +78,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_runtimeSettings = runtimeSettings.Value;
_securitySettings = securitySettings.Value;
_runtimeMinifier = runtimeMinifier;
_authenticationSchemeProvider = authenticationSchemeProvider;
_externalLogins = externalLogins;
_imageUrlGenerator = imageUrlGenerator;
_previewRoutes = previewRoutes;
}
@@ -419,19 +420,11 @@ namespace Umbraco.Web.BackOffice.Controllers
"externalLogins", new Dictionary<string, object>
{
{
"providers", (await _authenticationSchemeProvider.GetAllSchemesAsync())
// Filter only external providers
.Where(x => !x.DisplayName.IsNullOrWhiteSpace())
// TODO: We need to filter only back office enabled schemes.
// Before we used to have a property bag to check, now we don't so need to investigate the easiest/best
// way to do this. We have the type so maybe we check for a marker interface, but maybe there's another way,
// just need to investigate.
//.Where(p => p.Properties.ContainsKey("UmbracoBackOffice"))
"providers", _externalLogins.GetBackOfficeProviders()
.Select(p => new
{
authType = p.Name, caption = p.DisplayName,
// TODO: See above, if we need this property bag in the vars then we'll need to figure something out
// properties = p.Properties
authType = p.AuthenticationType, caption = p.Name,
properties = p.Properties
})
.ToArray()
}

View File

@@ -201,10 +201,10 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[AppendUserModifiedHeader]
public async Task<IActionResult> PostSetAvatar(IList<IFormFile> files)
public IActionResult PostSetAvatar(IList<IFormFile> files)
{
//borrow the logic from the user controller
return await UsersController.PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0));
return UsersController.PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0));
}
/// <summary>
@@ -216,6 +216,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </returns>
public async Task<ModelWithNotifications<string>> PostChangePassword(ChangingPasswordModel data)
{
// TODO: Why don't we inject this? Then we can just inject a logger
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, data, _backOfficeUserManager);
@@ -240,7 +241,15 @@ namespace Umbraco.Web.BackOffice.Controllers
public async Task<Dictionary<string, string>> GetCurrentUserLinkedLogins()
{
var identityUser = await _backOfficeUserManager.FindByIdAsync(_backofficeSecurityAccessor.BackOfficeSecurity.GetUserId().ResultOr(0).ToString());
return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey);
// deduplicate in case there are duplicates (there shouldn't be now since we have a unique constraint on the external logins
// but there didn't used to be)
var result = new Dictionary<string, string>();
foreach (var l in identityUser.Logins)
{
result[l.LoginProvider] = l.ProviderKey;
}
return result;
}
}
}

View File

@@ -1,18 +0,0 @@
using System.Web.Http;
using System.Web.Http.Controllers;
using Umbraco.Web.WebApi;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
internal class DenyLocalLoginAuthorizationAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
var owinContext = actionContext.Request.TryGetOwinContext().Result;
// no authorization if any external logins deny local login
return !owinContext.Authentication.HasDenyLocalLogin();
}
}
}

View File

@@ -38,13 +38,12 @@ using Umbraco.Web.Common.ActionResults;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Editors;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
using IUser = Umbraco.Core.Models.Membership.IUser;
using Task = System.Threading.Tasks.Task;
using Umbraco.Net;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.Common.Security;
namespace Umbraco.Web.BackOffice.Controllers
{
@@ -72,9 +71,11 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IMediaService _mediaService;
private readonly IContentService _contentService;
private readonly GlobalSettings _globalSettings;
private readonly IBackOfficeUserManager _backOfficeUserManager;
private readonly IBackOfficeUserManager _userManager;
private readonly ILoggerFactory _loggerFactory;
private readonly LinkGenerator _linkGenerator;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly ILogger<UsersController> _logger;
public UsersController(
IMediaFileSystem mediaFileSystem,
@@ -97,7 +98,8 @@ namespace Umbraco.Web.BackOffice.Controllers
IOptions<GlobalSettings> globalSettings,
IBackOfficeUserManager backOfficeUserManager,
ILoggerFactory loggerFactory,
LinkGenerator linkGenerator)
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalLogins)
{
_mediaFileSystem = mediaFileSystem;
_contentSettings = contentSettings.Value;
@@ -117,9 +119,11 @@ namespace Umbraco.Web.BackOffice.Controllers
_mediaService = mediaService;
_contentService = contentService;
_globalSettings = globalSettings.Value;
_backOfficeUserManager = backOfficeUserManager;
_userManager = backOfficeUserManager;
_loggerFactory = loggerFactory;
_linkGenerator = linkGenerator;
_externalLogins = externalLogins;
_logger = _loggerFactory.CreateLogger<UsersController>();
}
/// <summary>
@@ -137,12 +141,12 @@ namespace Umbraco.Web.BackOffice.Controllers
[AppendUserModifiedHeader("id")]
[AdminUsersAuthorize]
public async Task<IActionResult> PostSetAvatar(int id, IList<IFormFile> files)
public IActionResult PostSetAvatar(int id, IList<IFormFile> files)
{
return await PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id);
return PostSetAvatarInternal(files, _userService, _appCaches.RuntimeCache, _mediaFileSystem, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, id);
}
internal static async Task<IActionResult> PostSetAvatarInternal(IList<IFormFile> files, IUserService userService, IAppCache cache, IMediaFileSystem mediaFileSystem, IShortStringHelper shortStringHelper, ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, int id)
internal static IActionResult PostSetAvatarInternal(IList<IFormFile> files, IUserService userService, IAppCache cache, IMediaFileSystem mediaFileSystem, IShortStringHelper shortStringHelper, ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, int id)
{
if (files is null)
{
@@ -375,16 +379,16 @@ namespace Umbraco.Web.BackOffice.Controllers
var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
var created = await _backOfficeUserManager.CreateAsync(identityUser);
var created = await _userManager.CreateAsync(identityUser);
if (created.Succeeded == false)
{
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
}
string resetPassword;
var password = _backOfficeUserManager.GeneratePassword();
var password = _userManager.GeneratePassword();
var result = await _backOfficeUserManager.AddPasswordAsync(identityUser, password);
var result = await _userManager.AddPasswordAsync(identityUser, password);
if (result.Succeeded == false)
{
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
@@ -416,7 +420,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// <remarks>
/// This will email the user an invite and generate a token that will be validated in the email
/// </remarks>
public async Task<UserDisplay> PostInviteUser(UserInvite userSave)
public async Task<ActionResult<UserDisplay>> PostInviteUser(UserInvite userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
@@ -425,7 +429,7 @@ namespace Umbraco.Web.BackOffice.Controllers
if (ModelState.IsValid == false)
{
throw new HttpResponseException(HttpStatusCode.BadRequest, ModelState);
return new ValidationErrorResult(ModelState);
}
IUser user;
@@ -441,12 +445,9 @@ namespace Umbraco.Web.BackOffice.Controllers
}
user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue);
var userMgr = TryGetOwinContext().Result.GetBackOfficeUserManager();
if (!EmailSender.CanSendRequiredEmail(GlobalSettings) && !userMgr.HasSendingUserInviteEventHandler)
if (!EmailSender.CanSendRequiredEmail(_globalSettings) && !_userManager.HasSendingUserInviteEventHandler)
{
throw new HttpResponseException(
Request.CreateNotificationValidationErrorResponse("No Email server is configured"));
return new ValidationErrorResult("No Email server is configured");
}
//Perform authorization here to see if the current user can actually save this user with the info being requested
@@ -454,7 +455,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var canSaveUser = authHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized, canSaveUser.Result);
return new ValidationErrorResult(canSaveUser.Result, StatusCodes.Status401Unauthorized);
}
if (user == null)
@@ -464,10 +465,10 @@ namespace Umbraco.Web.BackOffice.Controllers
var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
var created = await _backOfficeUserManager.CreateAsync(identityUser);
var created = await _userManager.CreateAsync(identityUser);
if (created.Succeeded == false)
{
throw HttpResponseException.CreateNotificationValidationErrorResponse(created.Errors.ToErrorMessage());
return ValidationErrorResult.CreateNotificationValidationErrorResult(created.Errors.ToErrorMessage());
}
//now re-look the user back up
@@ -484,21 +485,16 @@ namespace Umbraco.Web.BackOffice.Controllers
_userService.Save(user);
var display = _umbracoMapper.Map<UserDisplay>(user);
var inviteArgs = new UserInviteEventArgs(
Request.TryGetHttpContext().Result.GetCurrentRequestIpAddress(),
performingUser: Security.GetUserId().Result,
userSave,
user);
UserInviteEventArgs inviteArgs;
try
{
userMgr.RaiseSendingUserInvite(inviteArgs);
inviteArgs = _userManager.RaiseSendingUserInvite(User, userSave, user);
}
catch (Exception ex)
{
Logger.Error<UsersController>(ex, "An error occured in a custom event handler while inviting the user");
throw new HttpResponseException(
Request.CreateNotificationValidationErrorResponse($"An error occured inviting the user (check logs for more info): {ex.Message}"));
_logger.LogError(ex, "An error occured in a custom event handler while inviting the user");
return ValidationErrorResult.CreateNotificationValidationErrorResult($"An error occured inviting the user (check logs for more info): {ex.Message}");
}
// If the event is handled then no need to send the email
@@ -553,8 +549,8 @@ namespace Umbraco.Web.BackOffice.Controllers
private async Task SendUserInviteEmailAsync(UserBasic userDisplay, string from, string fromEmail, IUser to, string message)
{
var user = await _backOfficeUserManager.FindByIdAsync(((int) userDisplay.Id).ToString());
var token = await _backOfficeUserManager.GenerateEmailConfirmationTokenAsync(user);
var user = await _userManager.FindByIdAsync(((int) userDisplay.Id).ToString());
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var inviteToken = string.Format("{0}{1}{2}",
(int)userDisplay.Id,
@@ -625,8 +621,7 @@ namespace Umbraco.Web.BackOffice.Controllers
var hasErrors = false;
// we need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed
var owinContext = Request.TryGetOwinContext().Result;
var hasDenyLocalLogin = owinContext.Authentication.HasDenyLocalLogin();
var hasDenyLocalLogin = _externalLogins.HasDenyLocalLogin();
if (hasDenyLocalLogin)
{
userSave.Email = found.Email; // it cannot change, this would only happen if people are mucking around with the request
@@ -706,8 +701,9 @@ namespace Umbraco.Web.BackOffice.Controllers
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// TODO: Why don't we inject this? Then we can just inject a logger
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _backOfficeUserManager);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, found, changingPasswordModel, _userManager);
if (passwordChangeResult.Success)
{
@@ -792,14 +788,14 @@ namespace Umbraco.Web.BackOffice.Controllers
foreach (var u in userIds)
{
var user = await _backOfficeUserManager.FindByIdAsync(u.ToString());
var user = await _userManager.FindByIdAsync(u.ToString());
if (user == null)
{
notFound.Add(u);
continue;
}
var unlockResult = await _backOfficeUserManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now);
var unlockResult = await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now);
if (unlockResult.Succeeded == false)
{
throw HttpResponseException.CreateValidationErrorResponse(