using System.Globalization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; /// /// Controller to back the User.Resource service, used for fetching user data when already authenticated. user.service /// is currently used for handling authentication /// [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] public class CurrentUserController : UmbracoAuthorizedJsonController { private readonly AppCaches _appCaches; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly IBackOfficeUserManager _backOfficeUserManager; private readonly ContentSettings _contentSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly ILocalizedTextService _localizedTextService; private readonly MediaFileManager _mediaFileManager; private readonly IPasswordChanger _passwordChanger; private readonly IShortStringHelper _shortStringHelper; private readonly IUmbracoMapper _umbracoMapper; private readonly IUserDataService _userDataService; private readonly IUserService _userService; [ActivatorUtilitiesConstructor] public CurrentUserController( MediaFileManager mediaFileManager, IOptionsSnapshot contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IUserService userService, IUmbracoMapper umbracoMapper, IBackOfficeUserManager backOfficeUserManager, ILocalizedTextService localizedTextService, AppCaches appCaches, IShortStringHelper shortStringHelper, IPasswordChanger passwordChanger, IUserDataService userDataService) { _mediaFileManager = mediaFileManager; _contentSettings = contentSettings.Value; _hostingEnvironment = hostingEnvironment; _imageUrlGenerator = imageUrlGenerator; _backofficeSecurityAccessor = backofficeSecurityAccessor; _userService = userService; _umbracoMapper = umbracoMapper; _backOfficeUserManager = backOfficeUserManager; _localizedTextService = localizedTextService; _appCaches = appCaches; _shortStringHelper = shortStringHelper; _passwordChanger = passwordChanger; _userDataService = userDataService; } /// /// Returns permissions for all nodes passed in for the current user /// /// /// [HttpPost] public Dictionary GetPermissions(int[] nodeIds) { EntityPermissionCollection permissions = _userService .GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, nodeIds); var permissionsDictionary = new Dictionary(); foreach (var nodeId in nodeIds) { var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); permissionsDictionary.Add(nodeId, aggregatePerms); } return permissionsDictionary; } /// /// Checks a nodes permission for the current user /// /// /// /// [HttpGet] public bool HasPermission(string permissionToCheck, int nodeId) { IEnumerable p = _userService .GetPermissions(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, nodeId).GetAllPermissions(); if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) { return true; } return false; } /// /// Saves a tour status for the current user /// /// /// public IEnumerable PostSetUserTour(UserTourStatus? status) { if (status == null) { throw new ArgumentNullException(nameof(status)); } List? userTours = null; if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.TourData.IsNullOrWhiteSpace() ?? true) { userTours = new List { status }; if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser is not null) { _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); } return userTours; } if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData is not null) { userTours = JsonConvert .DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity .CurrentUser.TourData)?.ToList(); UserTourStatus? found = userTours?.FirstOrDefault(x => x.Alias == status.Alias); if (found != null) { //remove it and we'll replace it next userTours?.Remove(found); } userTours?.Add(status); } _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.TourData = JsonConvert.SerializeObject(userTours); _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); return userTours ?? Enumerable.Empty(); } /// /// Returns the user's tours /// /// public IEnumerable? GetUserTours() { if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.TourData.IsNullOrWhiteSpace() ?? true) { return Enumerable.Empty(); } IEnumerable? userTours = JsonConvert.DeserializeObject>(_backofficeSecurityAccessor.BackOfficeSecurity .CurrentUser.TourData!); return userTours ?? Enumerable.Empty(); } public IEnumerable GetUserData() => _userDataService.GetUserData(); /// /// When a user is invited and they click on the invitation link, they will be partially logged in /// where they can set their username/password /// /// /// /// /// This only works when the user is logged in (partially) /// [AllowAnonymous] public async Task> PostSetInvitedUserPassword([FromBody] string newPassword) { var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().ResultOr(0).ToString(); if (userId is null) { throw new InvalidOperationException("Could not find user Id"); } var user = await _backOfficeUserManager.FindByIdAsync(userId); if (user == null) throw new InvalidOperationException("Could not find user"); IdentityResult result = await _backOfficeUserManager.AddPasswordAsync(user, newPassword); if (result.Succeeded == false) { //it wasn't successful, so add the change error to the model state, we've name the property alias _umb_password on the form // so that is why it is being used here. ModelState.AddModelError("value", result.Errors.ToErrorMessage()); return ValidationProblem(ModelState); } if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser is not null) { //They've successfully set their password, we can now update their user account to be approved _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.IsApproved = true; //They've successfully set their password, and will now get fully logged into the back office, so the lastlogindate is set so the backoffice shows they have logged in _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.LastLoginDate = DateTime.UtcNow; _userService.Save(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser); } //now we can return their full object since they are now really logged into the back office UserDetail? userDisplay = _umbracoMapper.Map(_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser); if (userDisplay is not null) { userDisplay.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds(); } return userDisplay; } [AppendUserModifiedHeader] public IActionResult PostSetAvatar(IList file) { Attempt? userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId(); var result = userId?.ResultOr(0); //borrow the logic from the user controller return UsersController.PostSetAvatarInternal( file, _userService, _appCaches.RuntimeCache, _mediaFileManager, _shortStringHelper, _contentSettings, _hostingEnvironment, _imageUrlGenerator, _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0); } /// /// Changes the users password /// /// The changing password model /// /// If the password is being reset it will return the newly reset password, otherwise will return an empty value /// public async Task>?> PostChangePassword( ChangingPasswordModel changingPasswordModel) { IUser? currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; if (currentUser is null) { return null; } changingPasswordModel.Id = currentUser.Id; // all current users have access to reset/manually change their password Attempt passwordChangeResult = await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _backOfficeUserManager, currentUser); if (passwordChangeResult.Success) { // even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result?.ResetPassword); result.AddSuccessNotification(_localizedTextService.Localize("user", "password"), _localizedTextService.Localize("user", "passwordChanged")); return result; } if (passwordChangeResult.Result?.ChangeError?.MemberNames is not null) { foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames) { ModelState.AddModelError(memberName, passwordChangeResult.Result.ChangeError.ErrorMessage ?? string.Empty); } } return ValidationProblem(ModelState); } // TODO: Why is this necessary? This inherits from UmbracoAuthorizedApiController [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().ResultOr(0).ToString(CultureInfo.InvariantCulture); if (userId is null) { throw new InvalidOperationException("Could not find user Id"); } BackOfficeIdentityUser? identityUser = await _backOfficeUserManager.FindByIdAsync(userId); if (identityUser is null) { throw new InvalidOperationException("Could not find user"); } // 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(); foreach (IIdentityUserLogin l in identityUser.Logins) { result[l.LoginProvider] = l.ProviderKey; } return result; } }