Merge tag 'v5.0.1' into dev

This commit is contained in:
Mark Davis
2023-12-29 11:54:39 -08:00
128 changed files with 1858 additions and 1002 deletions

View File

@ -76,7 +76,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Host)]
public Alias Put(int id, [FromBody] Alias alias)
{
if (ModelState.IsValid && _aliases.GetAlias(alias.AliasId, false) != null)
if (ModelState.IsValid && alias.AliasId == id && _aliases.GetAlias(alias.AliasId, false) != null)
{
alias = _aliases.UpdateAlias(alias);
_syncManager.AddSyncEvent(alias.TenantId, EntityNames.Alias, alias.AliasId, SyncEventActions.Update);

View File

@ -35,8 +35,8 @@ namespace Oqtane.Controllers
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
private readonly ISettingRepository _settingRepository;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_environment = environment;
_files = files;
@ -45,6 +45,7 @@ namespace Oqtane.Controllers
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
_settingRepository = settingRepository;
}
// GET: api/<controller>?folder=x
@ -207,7 +208,7 @@ namespace Oqtane.Controllers
public Models.File Put(int id, [FromBody] Models.File file)
{
var File = _files.GetFile(file.FileId, false);
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && File != null // ensure file exists
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && file.FileId == id && File != null // ensure file exists
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, File.FolderId, PermissionNames.Edit) // ensure user had edit rights to original folder
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit)) // ensure user has edit rights to new folder
{
@ -287,6 +288,8 @@ namespace Oqtane.Controllers
folder = _folders.GetFolder(FolderId);
}
var _UploadableFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue ?? Constants.UploadableFiles) ?? Constants.UploadableFiles;
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList))
{
string folderPath = _folders.GetFolderPath(folder);
@ -297,7 +300,7 @@ namespace Oqtane.Controllers
name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
// check for allowable file extensions
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
if (!_UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
@ -362,6 +365,10 @@ namespace Oqtane.Controllers
return;
}
// Get the UploadableFiles extensions
string uploadfilesSetting = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
string _UploadableFiles = uploadfilesSetting ?? Constants.UploadableFiles;
// ensure filename is valid
string token = ".part_";
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token))
@ -371,7 +378,7 @@ namespace Oqtane.Controllers
// check for allowable file extensions (ignore token)
var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", "");
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
if (!_UploadableFiles.Split(',').Contains(extension.ToLower()))
{
return;
}
@ -604,9 +611,11 @@ namespace Oqtane.Controllers
public IActionResult GetImage(int id, int width, int height, string mode, string position, string background, string rotate, string recreate)
{
var file = _files.GetFile(id);
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
@ -658,8 +667,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (file != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png");
@ -763,6 +779,7 @@ namespace Oqtane.Controllers
private Models.File CreateFile(string filename, int folderid, string filepath)
{
var file = _files.GetFile(folderid, filename);
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
int size = 0;
var folder = _folders.GetFolder(folderid, false);
@ -789,7 +806,7 @@ namespace Oqtane.Controllers
file.ImageHeight = 0;
file.ImageWidth = 0;
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
try
{

View File

@ -204,7 +204,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Folder Put(int id, [FromBody] Folder folder)
{
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && folder.FolderId == id && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
{
if (folder.IsPathValid())
{

View File

@ -30,11 +30,12 @@ namespace Oqtane.Controllers
private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor;
private readonly IAliasRepository _aliases;
private readonly ISiteRepository _sites;
private readonly ILogger<InstallationController> _filelogger;
private readonly ITenantManager _tenantManager;
private readonly IServerStateManager _serverState;
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ISiteRepository sites, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
{
_configManager = configManager;
_installationManager = installationManager;
@ -43,6 +44,7 @@ namespace Oqtane.Controllers
_cache = cache;
_accessor = accessor;
_aliases = aliases;
_sites = sites;
_filelogger = filelogger;
_tenantManager = tenantManager;
_serverState = serverState;
@ -108,6 +110,70 @@ namespace Oqtane.Controllers
return GetAssemblyList().Select(item => item.HashedName).ToList();
}
private List<ClientAssembly> GetAssemblyList()
{
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"assemblieslist:{alias.SiteKey}", entry =>
{
var assemblyList = new List<ClientAssembly>();
var site = _sites.GetSite(alias.SiteId);
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
bool hashfilename = true;
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
{
hashfilename = false;
}
// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(alias.SiteKey).Assemblies;
// populate assembly list
foreach (var assembly in assemblies)
{
if (assembly != Constants.ClientId)
{
var filepath = Path.Combine(binFolder, assembly) + ".dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
}
}
}
// insert satellite assemblies at beginning of list
foreach (var culture in _localizationManager.GetInstalledCultures())
{
if (culture != Constants.DefaultCulture)
{
var assembliesFolderPath = Path.Combine(binFolder, culture);
if (Directory.Exists(assembliesFolderPath))
{
foreach (var assembly in assemblies)
{
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
}
}
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
}
}
}
}
return assemblyList;
});
}
// GET api/<controller>/load?list=x,y
[HttpGet("load")]
public IActionResult Load(string list = "*")
@ -115,126 +181,79 @@ namespace Oqtane.Controllers
return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll");
}
private List<ClientAssembly> GetAssemblyList()
{
var siteKey = _tenantManager.GetAlias().SiteKey;
return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry =>
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblyList = new List<ClientAssembly>();
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
bool hashfilename = true;
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
{
hashfilename = false;
}
// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(siteKey).Assemblies;
// populate assembly list
foreach (var assembly in assemblies)
{
if (assembly != Constants.ClientId)
{
var filepath = Path.Combine(binFolder, assembly) + ".dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
}
}
}
// insert satellite assemblies at beginning of list
foreach (var culture in _localizationManager.GetInstalledCultures())
{
if (culture != Constants.DefaultCulture)
{
var assembliesFolderPath = Path.Combine(binFolder, culture);
if (Directory.Exists(assembliesFolderPath))
{
foreach (var assembly in assemblies)
{
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
}
}
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
}
}
}
return assemblyList;
});
}
private byte[] GetAssemblies(string list)
{
var siteKey = _tenantManager.GetAlias().SiteKey;
var alias = _tenantManager.GetAlias();
if (list == "*")
{
return _cache.GetOrCreate($"assemblies:{siteKey}", entry =>
return _cache.GetOrCreate($"assemblies:{alias.SiteKey}", entry =>
{
return GetZIP(list);
return GetZIP(list, alias);
});
}
else
{
return GetZIP(list);
return GetZIP(list, alias);
}
}
private byte[] GetZIP(string list)
private byte[] GetZIP(string list, Alias alias)
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// get list of assemblies which should be downloaded to client
List<ClientAssembly> assemblies = GetAssemblyList();
if (list != "*")
var site = _sites.GetSite(alias.SiteId);
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
{
var filter = list.Split(',').ToList();
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
}
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// create zip file containing assemblies and debug symbols
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
// get list of assemblies which should be downloaded to client
List<ClientAssembly> assemblies = GetAssemblyList();
if (list != "*")
{
foreach (var assembly in assemblies)
var filter = list.Split(',').ToList();
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
}
// create zip file containing assemblies and debug symbols
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
foreach (var assembly in assemblies)
{
if (System.IO.File.Exists(assembly.FilePath))
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
{
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
if (System.IO.File.Exists(assembly.FilePath))
{
filestream.CopyTo(entrystream);
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
{
filestream.CopyTo(entrystream);
}
}
}
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
if (System.IO.File.Exists(pdb))
{
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
if (System.IO.File.Exists(pdb))
{
filestream.CopyTo(entrystream);
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
{
filestream.CopyTo(entrystream);
}
}
}
}
}
}
return memoryStream.ToArray();
return memoryStream.ToArray();
}
}
else
{
// return empty zip
using (var memoryStream = new MemoryStream())
{
using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create)) {}
return memoryStream.ToArray();
}
}
}

View File

@ -67,7 +67,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Host)]
public Job Put(int id, [FromBody] Job job)
{
if (ModelState.IsValid && _jobs.GetJob(job.JobId, false) != null)
if (ModelState.IsValid && job.JobId == id && _jobs.GetJob(job.JobId, false) != null)
{
job = _jobs.UpdateJob(job);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", job);

View File

@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
@ -9,9 +12,6 @@ using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
using System.Linq;
using System.Diagnostics;
using System.Globalization;
namespace Oqtane.Controllers
{
@ -102,6 +102,24 @@ namespace Oqtane.Controllers
}
}
[HttpPut]
[Authorize(Roles = RoleNames.Admin)]
public void Put([FromBody] Language language)
{
if (ModelState.IsValid && language.SiteId == _alias.SiteId)
{
_languages.UpdateLanguage(language);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Language, language.LanguageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Language Updated {Language}", language);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Put Attempt {Language}", language);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
public Language Post([FromBody] Language language)

View File

@ -154,7 +154,7 @@ namespace Oqtane.Controllers
{
var _module = _modules.GetModule(module.ModuleId, false);
if (ModelState.IsValid && module.SiteId == _alias.SiteId && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
if (ModelState.IsValid && module.SiteId == _alias.SiteId && module.ModuleId == id && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
{
module = _modules.UpdateModule(module);

View File

@ -167,7 +167,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public void Put(int id, [FromBody] ModuleDefinition moduleDefinition)
{
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && moduleDefinition.ModuleDefinitionId == id && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
{
_moduleDefinitions.UpdateModuleDefinition(moduleDefinition);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update);

View File

@ -161,6 +161,12 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId))
{
if (!User.IsInRole(RoleNames.Admin))
{
// content must be HTML encoded for non-admins to prevent HTML injection
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
notification.Body = WebUtility.HtmlEncode(notification.Body);
}
notification = _notifications.AddNotification(notification);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {NotificationId}", notification.NotificationId);
@ -179,8 +185,14 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Notification Put(int id, [FromBody] Notification notification)
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
{
if (!User.IsInRole(RoleNames.Admin))
{
// content must be HTML encoded for non-admins to prevent HTML injection
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
notification.Body = WebUtility.HtmlEncode(notification.Body);
}
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);

View File

@ -269,7 +269,7 @@ namespace Oqtane.Controllers
// get current page
var currentPage = _pages.GetPage(page.PageId, false);
if (ModelState.IsValid && page.SiteId == _alias.SiteId && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
if (ModelState.IsValid && page.SiteId == _alias.SiteId && page.PageId == id && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
{
// get current page permissions
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();

View File

@ -109,7 +109,7 @@ namespace Oqtane.Controllers
public PageModule Put(int id, [FromBody] PageModule pageModule)
{
var page = _pages.GetPage(pageModule.PageId);
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && pageModule.PageModuleId == id && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
{
pageModule = _pageModules.UpdatePageModule(pageModule);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.PageModule, pageModule.PageModuleId, SyncEventActions.Update);

View File

@ -94,7 +94,7 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}")]
public Profile Put(int id, [FromBody] Profile profile)
{
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && _profiles.GetProfile(profile.ProfileId, false) != null)
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && profile.ProfileId == id && _profiles.GetProfile(profile.ProfileId, false) != null)
{
profile = _profiles.UpdateProfile(profile);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Profile, profile.ProfileId, SyncEventActions.Update);

View File

@ -98,7 +98,7 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}")]
public Role Put(int id, [FromBody] Role role)
{
if (ModelState.IsValid && role.SiteId == _alias.SiteId && _roles.GetRole(role.RoleId, false) != null)
if (ModelState.IsValid && role.SiteId == _alias.SiteId && role.RoleId == id && _roles.GetRole(role.RoleId, false) != null)
{
role = _roles.UpdateRole(role);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Role, role.RoleId, SyncEventActions.Update);

View File

@ -128,7 +128,7 @@ namespace Oqtane.Controllers
[HttpPut("{id}")]
public Setting Put(int id, [FromBody] Setting setting)
{
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
if (ModelState.IsValid && setting.SettingId == id && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.UpdateSetting(setting);
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Update);

View File

@ -86,6 +86,12 @@ namespace Oqtane.Controllers
.Where(item => !item.IsPrivate || User.IsInRole(RoleNames.Admin))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
// populate File Extensions
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
? site.Settings["ImageFiles"] : Constants.ImageFiles;
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
// pages
List<Setting> settings = _settings.GetSettings(EntityNames.Page).ToList();
site.Pages = new List<Page>();
@ -192,7 +198,7 @@ namespace Oqtane.Controllers
public Site Put(int id, [FromBody] Site site)
{
var current = _sites.GetSite(site.SiteId, false);
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && current != null)
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && site.SiteId == id && current != null)
{
site = _sites.UpdateSite(site);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, site.SiteId, SyncEventActions.Update);

View File

@ -71,7 +71,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public void Put(int id, [FromBody] Theme theme)
{
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && theme.ThemeId == id && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
{
_themes.UpdateTheme(theme);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);

View File

@ -118,7 +118,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public UrlMapping Put(int id, [FromBody] UrlMapping urlMapping)
{
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && urlMapping.UrlMappingId == id && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
{
urlMapping = _urlMappings.UpdateUrlMapping(urlMapping);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UrlMapping, urlMapping.UrlMappingId, SyncEventActions.Update);

View File

@ -173,15 +173,16 @@ namespace Oqtane.Controllers
[Authorize]
public async Task<User> Put(int id, [FromBody] User user)
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
user = await _userManager.UpdateUser(user);
}
else
{
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null;
}

View File

@ -149,7 +149,7 @@ namespace Oqtane.Controllers
public UserRole Put(int id, [FromBody] UserRole userRole)
{
var role = _roles.GetRole(userRole.RoleId);
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && userRole.UserRoleId == id && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
{
userRole = _userRoles.UpdateUserRole(userRole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Update);

View File

@ -0,0 +1,54 @@
using System.Linq;
using System.Security.Claims;
namespace Oqtane.Extensions
{
public static class ClaimsPrincipalExtensions
{
public static string Username(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name).Value;
}
else
{
return "";
}
}
public static int UserId(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{
return int.Parse(claimsPrincipal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
else
{
return -1;
}
}
public static string Roles(this ClaimsPrincipal claimsPrincipal)
{
var roles = "";
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
roles += ((roles == "") ? "" : ";") + claim.Value;
}
return roles;
}
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
}
else
{
return "";
}
}
}
}

View File

@ -124,7 +124,7 @@ namespace Microsoft.Extensions.DependencyInjection
// note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application"
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Events.OnRedirectToLogin = context =>
@ -179,7 +179,7 @@ namespace Microsoft.Extensions.DependencyInjection
options.Lockout.AllowedForNewUsers = false;
// SignIn settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
// User settings

View File

@ -50,7 +50,6 @@ namespace Oqtane.Extensions
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // authorization code flow
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
// cookie config is required to avoid Correlation Failed errors
@ -62,6 +61,7 @@ namespace Oqtane.Extensions
options.MetadataAddress = sitesettings.GetValue("ExternalLogin:MetadataUrl", "");
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", "")))
{
@ -150,15 +150,16 @@ namespace Oqtane.Extensions
private static async Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
// OAuth 2.0
var email = "";
var id = "";
var claims = "";
var id = "";
var name = "";
var email = "";
if (context.Options.UserInformationEndpoint != "")
{
try
{
// call user information endpoint
// call user information endpoint using access token
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
@ -167,32 +168,49 @@ namespace Oqtane.Extensions
response.EnsureSuccessStatusCode();
claims = await response.Content.ReadAsStringAsync();
// parse json output
// get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!claims.StartsWith("[") && !claims.EndsWith("]"))
// some user endpoints can return multiple objects (ie. GitHub) so convert single object to array (if necessary)
var jsonclaims = claims;
if (!jsonclaims.StartsWith("[") && !jsonclaims.EndsWith("]"))
{
claims = "[" + claims + "]"; // convert to json array
jsonclaims = "[" + jsonclaims + "]";
}
JsonNode items = JsonNode.Parse(claims)!;
// parse claim values
JsonNode items = JsonNode.Parse(jsonclaims)!;
foreach (var item in items.AsArray())
{
if (item[emailClaimType] != null)
name = "";
email = "";
// id claim is required
if (!string.IsNullOrEmpty(idClaimType) && item[idClaimType] != null)
{
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
id = item[idClaimType].ToString();
// name claim is optional
if (!string.IsNullOrEmpty(nameClaimType) && item[nameClaimType] != null)
{
email = item[emailClaimType].ToString().ToLower();
if (item[idClaimType] != null)
name = item[nameClaimType].ToString();
}
// email claim is optional
if (!string.IsNullOrEmpty(emailClaimType) && item[emailClaimType] != null)
{
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
id = item[idClaimType].ToString();
email = item[emailClaimType].ToString().ToLower();
}
break;
}
}
}
if (string.IsNullOrEmpty(id))
{
id = email;
if (!string.IsNullOrEmpty(id))
{
break;
}
}
}
catch (Exception ex)
@ -203,7 +221,7 @@ namespace Oqtane.Extensions
}
// validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.AccessToken));
@ -213,6 +231,14 @@ namespace Oqtane.Extensions
// pass properties to OnTicketReceived
context.Properties.SetParameter("status", identity.Label);
context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri);
// set cookie expiration
string cookieExpStr = context.HttpContext.GetSiteSettings().GetValue("LoginOptions:CookieExpiration", "");
if (!string.IsNullOrEmpty(cookieExpStr) && TimeSpan.TryParse(cookieExpStr, out TimeSpan cookieExpTS))
{
context.Properties.ExpiresUtc = DateTime.Now.Add(cookieExpTS);
context.Properties.IsPersistent = true;
}
}
private static Task OnTicketReceived(TicketReceivedContext context)
@ -231,28 +257,46 @@ namespace Oqtane.Extensions
private static async Task OnTokenValidated(TokenValidatedContext context)
{
// OpenID Connect
var claims = "";
var id = "";
var name = "";
var email = "";
// serialize claims
foreach (var claim in context.Principal.Claims)
{
claims += "\"" + claim.Type + "\":\"" + claim.Value + "\",";
}
claims = "{" + claims.Substring(0, claims.Length - 1) + "}";
// get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var id = context.Principal.FindFirstValue(idClaimType);
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType);
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
// parse claim values - id claim is required
id = context.Principal.FindFirstValue(idClaimType);
// name claim is optional
if (!string.IsNullOrEmpty(nameClaimType) && context.Principal.FindFirstValue(nameClaimType) != null)
{
name = context.Principal.FindFirstValue(nameClaimType);
}
// email claim is optional
if (!string.IsNullOrEmpty(emailClaimType) && context.Principal.FindFirstValue(emailClaimType) != null)
{
if (EmailValid(context.Principal.FindFirstValue(emailClaimType), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = context.Principal.FindFirstValue(emailClaimType);
}
}
// validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success)
{
// external roles
if (!string.IsNullOrEmpty(context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
foreach (var claim in context.Principal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
// include access token
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity);
}
@ -284,12 +328,20 @@ namespace Oqtane.Extensions
return Task.CompletedTask;
}
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
private static async Task<ClaimsIdentity> ValidateUser(string id, string name, string email, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
{
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
// use identity.Label as a temporary location to store validation status information
// review claims feature (for testing - external login is disabled)
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false")))
{
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims);
identity.Label = ExternalLoginStatus.ReviewClaims;
return identity;
}
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", "");
var alias = httpContext.GetAlias();
@ -308,136 +360,158 @@ namespace Oqtane.Extensions
}
else
{
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
bool duplicates = false;
if (!string.IsNullOrEmpty(email))
{
bool duplicates = false;
try
{
identityuser = await _identityUserManager.FindByEmailAsync(email);
}
catch
{
// FindByEmailAsync will throw an error if the email matches multiple user accounts
catch // FindByEmailAsync will throw an error if the email matches multiple user accounts
{
duplicates = true;
}
if (identityuser == null)
}
if (identityuser == null)
{
if (duplicates)
{
if (duplicates)
identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss", CultureInfo.InvariantCulture));
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = email,
DisplayName = email,
Email = email,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
// user identifiers
var username = "";
var emailaddress = "";
var displayname = "";
bool emailconfirmed = false;
if (user != null)
if (!string.IsNullOrEmpty(email)) // email claim provided
{
username = email;
emailaddress = email;
displayname = (!string.IsNullOrEmpty(name)) ? name : email;
emailconfirmed = true;
}
else if (!string.IsNullOrEmpty(name)) // name claim provided
{
username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = name;
}
else // neither email nor name provided
{
username = Guid.NewGuid().ToString("N");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = username;
}
identityuser = new IdentityUser();
identityuser.UserName = username;
identityuser.Email = emailaddress;
identityuser.EmailConfirmed = emailconfirmed;
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
var password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
var result = await _identityUserManager.CreateAsync(identityuser, password);
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = username,
DisplayName = displayname,
Email = emailaddress,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null)
{
if (!string.IsNullOrEmpty(email))
{
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
}
else
{
identity.Label = ExternalLoginStatus.UserDoesNotExist;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
}
else
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
else
{
// provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
identity.Label = ExternalLoginStatus.UserDoesNotExist;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
}
else // email invalid
else
{
identity.Label = ExternalLoginStatus.InvalidEmail;
if (!string.IsNullOrEmpty(email))
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email);
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims);
// provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
}
}
}
@ -455,6 +529,25 @@ namespace Oqtane.Extensions
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
// external roles
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
{
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
}
}
// user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{
@ -493,7 +586,7 @@ namespace Oqtane.Extensions
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. The Valid Claims Are {Claims}.", mapping.Split(":")[0], claims);
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]);
}
}
else
@ -506,9 +599,10 @@ namespace Oqtane.Extensions
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
}
}
else // id invalid
else // claims invalid
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims);
identity.Label = ExternalLoginStatus.MissingClaims;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
}
return identity;

View File

@ -105,12 +105,6 @@ namespace Oqtane.Infrastructure
IsNewTenant = false
};
// on upgrade install the associated Nuget package
if (!string.IsNullOrEmpty(install.ConnectionString))
{
InstallDatabase(install);
}
var installation = IsInstalled();
if (!installation.Success)
{
@ -209,57 +203,6 @@ namespace Oqtane.Infrastructure
return result;
}
private Installation InstallDatabase(InstallConfig install)
{
var result = new Installation {Success = false, Message = string.Empty};
try
{
bool installPackages = false;
// iterate database packages in installation folder
var packagesFolder = new DirectoryInfo(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder));
foreach (var package in packagesFolder.GetFiles("*.nupkg.bak"))
{
// determine if package needs to be upgraded or installed
bool upgrade = System.IO.File.Exists(package.FullName.Replace(".nupkg.bak",".log"));
if (upgrade || package.Name.StartsWith(Utilities.GetAssemblyName(install.DatabaseType)))
{
var packageName = Path.Combine(package.DirectoryName, package.Name);
packageName = packageName.Substring(0, packageName.IndexOf(".bak"));
package.MoveTo(packageName, true);
installPackages = true;
}
}
if (installPackages)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var installationManager = scope.ServiceProvider.GetRequiredService<IInstallationManager>();
installationManager.InstallPackages();
}
}
// load the installation database type (if necessary)
if (Type.GetType(install.DatabaseType) == null)
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
var assembliesFolder = new DirectoryInfo(assemblyPath);
var assemblyFile = new FileInfo($"{assembliesFolder}/{Utilities.GetAssemblyName(install.DatabaseType)}.dll");
AssemblyLoadContext.Default.LoadOqtaneAssembly(assemblyFile);
}
result.Success = true;
}
catch (Exception ex)
{
result.Message = ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
return result;
}
private Installation CreateDatabase(InstallConfig install)
{
var result = new Installation { Success = false, Message = string.Empty };
@ -268,8 +211,6 @@ namespace Oqtane.Infrastructure
{
try
{
InstallDatabase(install);
var databaseType = install.DatabaseType;
// get database type
@ -436,7 +377,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Migrating A Tenant Database. This Is Usually Related To A Tenant Database Not Being In A Supported State. " + ex.ToString();
result.Message = "An Error Occurred Migrating The Database For Tenant " + tenant.Name + ". This Is Usually Related To Database Permissions, Connection String Mappings, Or The Database Not Being In A Supported State. " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
@ -457,7 +398,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Executing Upgrade Logic. " + ex.ToString();
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
}
@ -527,7 +468,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.ToString();
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
}
}
}
@ -614,6 +555,7 @@ namespace Oqtane.Infrastructure
SiteTemplateType = install.SiteTemplate,
Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value,
RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value,
HybridEnabled = false
};
site = sites.AddSite(site);

View File

@ -15,10 +15,18 @@ namespace Oqtane.Infrastructure.EventSubscribers
public void EntityChanged(SyncEvent syncEvent)
{
// when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh)
{
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
// when a site entity is updated the hosting model may have changed, so the client assemblies cache items need to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Update)
{
_cache.Remove($"assemblieslist:{syncEvent.TenantId}:{syncEvent.EntityId}");
_cache.Remove($"assemblies:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
}
}
}

View File

@ -13,7 +13,6 @@ using System.Xml;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Oqtane.Controllers;
using Oqtane.Shared;
// ReSharper disable AssignNullToNotNullAttribute
@ -41,51 +40,25 @@ namespace Oqtane.Infrastructure
}
}
// method must be static as it is called in ConfigureServices during Startup
public static string InstallPackages(string webRootPath, string contentRootPath)
{
string errors = "";
string binPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
string sourceFolder = Path.Combine(contentRootPath, "Packages");
string sourceFolder = Path.Combine(contentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(sourceFolder))
{
Directory.CreateDirectory(sourceFolder);
}
// move packages to secure /Packages folder
foreach (var folderName in "Modules,Themes,Packages".Split(","))
{
string folder = Path.Combine(webRootPath, folderName);
if (Directory.Exists(folder))
{
foreach (var file in Directory.GetFiles(folder, "*.nupkg*"))
{
var destinationFile = Path.Combine(sourceFolder, Path.GetFileName(file));
if (File.Exists(destinationFile))
{
File.Delete(destinationFile);
}
// read assembly log
var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
if (destinationFile.ToLower().EndsWith(".nupkg.bak"))
{
// leave a copy in the current folder as it is distributed with the core framework
File.Copy(file, destinationFile);
}
else
{
// move to destination
File.Move(file, destinationFile);
}
}
}
else
{
Directory.CreateDirectory(folder);
}
}
// iterate through Nuget packages in source folder
foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg"))
// install Nuget packages in secure Packages folder
var packages = Directory.GetFiles(sourceFolder, "*.nupkg");
foreach (string packagename in packages)
{
try
{
@ -154,10 +127,29 @@ namespace Oqtane.Infrastructure
// ContentRootPath sometimes produces inconsistent path casing - so can't use string.Replace()
filename = Regex.Replace(filename, Regex.Escape(contentRootPath), "", RegexOptions.IgnoreCase);
assets.Add(filename);
if (!manifest && Path.GetExtension(filename) == ".log")
// packages can include a manifest (rather than relying on the framework to dynamically create one)
if (!manifest && filename.EndsWith(name + ".log"))
{
manifest = true;
}
// register assembly
if (Path.GetExtension(filename) == ".dll")
{
// if package version was not installed previously
if (!File.Exists(Path.Combine(sourceFolder, name + ".log")))
{
if (assemblies.ContainsKey(Path.GetFileName(filename)))
{
assemblies[Path.GetFileName(filename)] += 1;
}
else
{
assemblies.Add(Path.GetFileName(filename), 1);
}
}
}
}
}
@ -187,6 +179,12 @@ namespace Oqtane.Infrastructure
File.Delete(packagename);
}
if (packages.Length != 0)
{
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
}
return errors;
}
@ -232,6 +230,10 @@ namespace Oqtane.Infrastructure
{
if (!string.IsNullOrEmpty(PackageName))
{
// read assembly log
var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// get manifest with highest version
string packagename = "";
string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log");
@ -249,17 +251,31 @@ namespace Oqtane.Infrastructure
{
// legacy support for assets that were stored as absolute paths
string filepath = asset.StartsWith("\\") ? Path.Combine(_environment.ContentRootPath, asset.Substring(1)) : asset;
if (File.Exists(filepath))
// delete assets
if (Path.GetExtension(filepath) == ".dll")
{
// do not remove licensing assemblies - this is a temporary fix until a more robust dependency management solution is available
if (!filepath.Contains("Oqtane.Licensing."))
// use assembly log to determine if assembly is used in other packages
if (assemblies.ContainsKey(Path.GetFileName(filepath)))
{
File.Delete(filepath);
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
if (assemblies[Path.GetFileName(filepath)] == 1)
{
Directory.Delete(Path.GetDirectoryName(filepath), true);
DeleteFile(filepath);
assemblies.Remove(Path.GetFileName(filepath));
}
else
{
assemblies[Path.GetFileName(filepath)] -= 1;
}
}
else // does not exist in assembly log
{
DeleteFile(filepath);
}
}
else // not an assembly
{
DeleteFile(filepath);
}
}
@ -269,6 +285,9 @@ namespace Oqtane.Infrastructure
File.Delete(asset);
}
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
return true;
}
}
@ -276,6 +295,76 @@ namespace Oqtane.Infrastructure
return false;
}
private void DeleteFile(string filepath)
{
if (File.Exists(filepath))
{
File.Delete(filepath);
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
{
Directory.Delete(Path.GetDirectoryName(filepath), true);
}
}
}
public int RegisterAssemblies()
{
var assemblyLogPath = GetAssemblyLogPath();
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblies = GetAssemblyLog(assemblyLogPath);
// remove assemblies that no longer exist
foreach (var dll in assemblies)
{
if (!File.Exists(Path.Combine(binFolder, dll.Key)))
{
assemblies.Remove(dll.Key);
}
}
// add assemblies which are not registered
foreach (var dll in Directory.GetFiles(binFolder, "*.dll"))
{
if (!assemblies.ContainsKey(Path.GetFileName(dll)))
{
assemblies.Add(Path.GetFileName(dll), 1);
}
}
SetAssemblyLog(assemblyLogPath, assemblies);
return assemblies.Count;
}
private string GetAssemblyLogPath()
{
string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(packagesFolder))
{
Directory.CreateDirectory(packagesFolder);
}
return Path.Combine(packagesFolder, "assemblies.log");
}
private static Dictionary<string, int> GetAssemblyLog(string assemblyLogPath)
{
Dictionary<string, int> assemblies = new Dictionary<string, int>();
if (File.Exists(assemblyLogPath))
{
assemblies = JsonSerializer.Deserialize<Dictionary<string, int>>(File.ReadAllText(assemblyLogPath));
}
return assemblies;
}
private static void SetAssemblyLog(string assemblyLogPath, Dictionary<string, int> assemblies)
{
if (File.Exists(assemblyLogPath))
{
File.Delete(assemblyLogPath);
}
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true }));
}
public async Task UpgradeFramework()
{
string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);

View File

@ -6,6 +6,7 @@ namespace Oqtane.Infrastructure
{
void InstallPackages();
bool UninstallPackage(string PackageName);
int RegisterAssemblies();
Task UpgradeFramework();
void RestartApplication();
}

View File

@ -7,6 +7,7 @@ namespace Oqtane.Infrastructure
Alias GetAlias();
Tenant GetTenant();
void SetAlias(Alias alias);
void SetAlias(int tenantId, int siteId);
void SetTenant(int tenantId);
}
}

View File

@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure
var logRepository = provider.GetRequiredService<ILogRepository>();
var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>();
var installationManager = provider.GetRequiredService<IInstallationManager>();
// iterate through sites for current tenant
List<Site> sites = siteRepository.GetSites().ToList();
@ -96,6 +97,17 @@ namespace Oqtane.Infrastructure
}
}
// register assemblies
try
{
var assemblies = installationManager.RegisterAssemblies();
log += assemblies.ToString() + " Assemblies Registered<br />";
}
catch (Exception ex)
{
log += $"Error Registering Assemblies - {ex.Message}<br />";
}
return log;
}

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Repository;
using Oqtane.Shared;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace Oqtane.Infrastructure
{
@ -22,8 +23,7 @@ namespace Oqtane.Infrastructure
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
string path = context.Request.Path.ToString();
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
{
// get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
@ -57,9 +57,25 @@ namespace Oqtane.Infrastructure
{
if (path.StartsWith("/" + alias.Path) && (Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/"))))
{
context.Request.Path = path.Replace("/" + alias.Path, "");
context.Request.Path = path.Substring(alias.Path.Length + 1);
}
}
// handle sitemap.xml request
if (context.Request.Path.ToString().Contains("/sitemap.xml") && !context.Request.Path.ToString().Contains("/pages"))
{
context.Request.Path = "/pages/sitemap.xml";
}
// handle robots.txt root request (does not support subfolder aliases)
if (context.Request.Path.StartsWithSegments("/robots.txt") && string.IsNullOrEmpty(alias.Path))
{
// allow all user agents and specify site map
var robots = $"User-agent: *\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml";
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(robots);
return;
}
}
}

View File

@ -26,13 +26,14 @@ namespace Oqtane.Infrastructure
{
Alias alias = null;
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
// does not support mock Alias objects (GetTenant should be used to retrieve a TenantId)
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
{
alias = _siteState.Alias;
}
else
{
// if there is http context
// if there is HttpContext
var httpcontext = _httpContextAccessor.HttpContext;
if (httpcontext != null)
{
@ -78,15 +79,19 @@ namespace Oqtane.Infrastructure
return null;
}
// background processes can set the alias using the SiteState service
public void SetAlias(Alias alias)
{
// background processes can set the alias using the SiteState service
_siteState.Alias = alias;
}
public void SetAlias(int tenantId, int siteId)
{
_siteState.Alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.TenantId == tenantId && item.SiteId == siteId);
}
public void SetTenant(int tenantId)
{
// background processes can set the alias using the SiteState service
_siteState.Alias = new Alias { TenantId = tenantId, AliasId = -1, SiteId = -1 };
}
}

View File

@ -106,7 +106,7 @@ namespace Oqtane.Managers
{
if (string.IsNullOrEmpty(user.Password))
{
// create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12!
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
@ -152,7 +152,7 @@ namespace Oqtane.Managers
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Verification", body);
_notifications.AddNotification(notification);
}
@ -205,8 +205,22 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email)
{
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
// if email address changed and user is not administrator, email verification is required for new email address
if (!user.EmailConfirmed)
{
var alias = _tenantManager.GetAlias();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
}
}
user = _users.UpdateUser(user);
@ -308,7 +322,7 @@ namespace Oqtane.Managers
user = _users.GetUser(identityuser.UserName);
if (user != null)
{
if (identityuser.EmailConfirmed)
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
{
user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow;
@ -323,7 +337,7 @@ namespace Oqtane.Managers
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.05.00.01.00")]
public class AddSiteHybridEnabled : MultiDatabaseMigration
{
public AddSiteHybridEnabled(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.AddBooleanColumn("HybridEnabled", true);
siteEntityBuilder.UpdateColumn("HybridEnabled", "0", "bool", ""); // default to false
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropColumn("HybridEnabled");
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -11,13 +11,14 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>
<IsPackable>true</IsPackable>
<DefineConstants>$(DefineConstants);OQTANE;OQTANE3</DefineConstants>
<PreserveCompilationContext>true</PreserveCompilationContext>
<SatelliteResourceLanguages>none</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<Compile Remove="wwwroot\Modules\Templates\**" />
@ -35,7 +36,6 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.0-preview3.23201.1" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
@ -43,9 +43,10 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7-pre20231110210158" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.7" />
<PackageReference Include="Oqtane.Licensing" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
@ -55,10 +56,18 @@
<ItemGroup>
<ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" />
<ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" />
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" />
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" />
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" />
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" />
</ItemGroup>
<Target Name="AddPayloadsFolder" AfterTargets="Publish">
<Copy SourceFiles="@(ModuleTemplateFiles)" DestinationFiles="@(ModuleTemplateFiles->'$(PublishDir)wwwroot\Modules\Templates\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Copy SourceFiles="@(ThemeTemplateFiles)" DestinationFiles="@(ThemeTemplateFiles->'$(PublishDir)wwwroot\Themes\Templates\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Copy SourceFiles="@(MySQLFiles)" DestinationFiles="@(MySQLFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Copy SourceFiles="@(PostgreSQLFiles)" DestinationFiles="@(PostgreSQLFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Copy SourceFiles="@(SqliteFiles)" DestinationFiles="@(SqliteFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
<Copy SourceFiles="@(SqlServerFiles)" DestinationFiles="@(SqlServerFiles->'$(PublishDir)%(Filename)%(Extension)')" SkipUnchangedFiles="false" />
</Target>
<ItemGroup>
<!-- extends watching group to include *.dll files and exclude the ones cause an infinite loop -->

View File

@ -19,7 +19,7 @@ namespace Oqtane.Pages
var providertype = HttpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
if (providertype != "")
{
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl + (returnurl.Contains("?") ? "&" : "?") + "reload=post" });
}
else
{

View File

@ -53,7 +53,8 @@ namespace Oqtane.Pages
{
if (_userPermissions.IsAuthorized(null, PermissionNames.View, page.PermissionList) && page.IsNavigation)
{
sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow });
var rooturl = _alias.Protocol + (string.IsNullOrEmpty(_alias.Path) ? _alias.Name : _alias.Name.Substring(0, _alias.Name.IndexOf("/")));
sitemap.Add(new Sitemap { Url = rooturl + Utilities.NavigateUrl(_alias.Path, page.Path, ""), ModifiedOn = DateTime.UtcNow });
foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId))
{
@ -72,7 +73,7 @@ namespace Oqtane.Pages
var urls = ((ISitemap)moduleobject).GetUrls(_alias.Path, page.Path, pageModule.Module);
foreach (var url in urls)
{
sitemap.Add(new Sitemap { Url = _alias.Protocol + _alias.Name + url.Url, ModifiedOn = DateTime.UtcNow });
sitemap.Add(new Sitemap { Url = rooturl + url.Url, ModifiedOn = DateTime.UtcNow });
}
}
catch (Exception ex)

View File

@ -4,9 +4,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Oqtane.Infrastructure;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Oqtane.Shared;
using Oqtane.Documentation;
namespace Oqtane.Server

View File

@ -45,13 +45,20 @@ namespace Oqtane.Repository
Tenant tenant = _tenantManager.GetTenant();
if (tenant != null)
{
_connectionString = _config.GetConnectionString(tenant.DBConnectionString)
.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString());
_databaseType = tenant.DBType;
_connectionString = _config.GetConnectionString(tenant.DBConnectionString);
if (_connectionString != null)
{
_connectionString = _connectionString.Replace($"|{Constants.DataDirectory}|", AppDomain.CurrentDomain.GetData(Constants.DataDirectory)?.ToString());
_databaseType = tenant.DBType;
}
else
{
// tenant connection string does not exist in appsettings.json
}
}
}
if (!String.IsNullOrEmpty(_databaseType))
if (!string.IsNullOrEmpty(_databaseType))
{
var type = Type.GetType(_databaseType);
ActiveDatabase = Activator.CreateInstance(type) as IDatabase;

View File

@ -9,6 +9,8 @@ namespace Oqtane.Repository
Language AddLanguage(Language language);
void UpdateLanguage(Language language);
Language GetLanguage(int languageId);
void DeleteLanguage(int languageId);

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Oqtane.Models;
namespace Oqtane.Repository
@ -12,7 +13,7 @@ namespace Oqtane.Repository
{
_db = context;
}
public IEnumerable<Language> GetLanguages(int siteId)
{
return _db.Language.Where(l => l.SiteId == siteId);
@ -35,6 +36,25 @@ namespace Oqtane.Repository
return language;
}
public void UpdateLanguage(Language language)
{
if (language.LanguageId != 0)
{
_db.Entry(language).State = EntityState.Modified;
}
if (language.IsDefault)
{
// Ensure all other languages are not set to default
_db.Language
.Where(l => l.SiteId == language.SiteId &&
l.LanguageId != language.LanguageId)
.ToList()
.ForEach(l => l.IsDefault = false);
}
_db.SaveChanges();
}
public Language GetLanguage(int languageId)
{
return _db.Language.Find(languageId);

View File

@ -93,7 +93,15 @@ namespace Oqtane.Repository
public int ExecuteNonQuery(string connectionString, string databaseType, string query)
{
var db = GetActiveDatabase(databaseType);
return db.ExecuteNonQuery(GetConnectionString(connectionString), query);
var connectionstring = GetConnectionString(connectionString);
if (connectionstring != null)
{
return db.ExecuteNonQuery(GetConnectionString(connectionString), query);
}
else
{
return 0;
}
}
public string GetScriptFromAssembly(Assembly assembly, string fileName)

View File

@ -18,7 +18,7 @@ namespace Oqtane.Security
public string GenerateToken(Alias alias, ClaimsIdentity identity, string secret, string issuer, string audience, int lifetime)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var key = Encoding.ASCII.GetBytes(PadSecret(secret));
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(identity),
@ -36,7 +36,7 @@ namespace Oqtane.Security
if (!string.IsNullOrEmpty(token))
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var key = Encoding.ASCII.GetBytes(PadSecret(secret));
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
@ -66,5 +66,11 @@ namespace Oqtane.Security
}
return null;
}
private string PadSecret(string secret)
{
// ensure secret is 256 bits
return (secret.Length < 32) ? (secret + "????????????????????????????????").Substring(0, 32) : secret;
}
}
}

View File

@ -8,6 +8,7 @@ using Oqtane.Models;
using System.Collections.Generic;
using Oqtane.Extensions;
using Oqtane.Shared;
using System.IO;
namespace Oqtane.Security
{
@ -17,9 +18,11 @@ namespace Oqtane.Security
{
if (context != null && context.Principal.Identity.IsAuthenticated && context.Principal.Identity.Name != null)
{
// check if framework is installed
var config = context.HttpContext.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
if (config.IsInstalled())
string path = context.Request.Path.ToString().ToLower();
// check if framework is installed
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
{
// get current site
var alias = context.HttpContext.GetAlias();
@ -28,12 +31,11 @@ namespace Oqtane.Security
var claims = context.Principal.Claims;
// check if principal has roles and matches current site
if (!claims.Any(item => item.Type == ClaimTypes.Role) || claims.FirstOrDefault(item => item.Type == "sitekey")?.Value != alias.SiteKey)
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
{
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
string path = context.Request.Path.ToString().ToLower();
User user = userRepository.GetUser(context.Principal.Identity.Name);
if (user != null)

View File

@ -71,6 +71,9 @@ namespace Oqtane
{
options.DetailedErrors = true;
}
})
.AddHubOptions(options => {
options.MaximumReceiveMessageSize = null; // no limit (for large amnounts of data ie. textarea components)
});
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )

View File

@ -1,3 +1,4 @@
del "*.nupkg"
"..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y

View File

@ -81,7 +81,7 @@ namespace [Owner].Module.[Module].Controllers
[Authorize(Policy = PolicyNames.EditModule)]
public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module])
{
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null)
if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null)
{
[Module] = _[Module]Repository.Update[Module]([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);

View File

@ -4,6 +4,12 @@ body {
padding-top: 7rem;
}
/* App Logo */
.app-logo .img-fluid {
max-height: 90px;
padding: 0 5px 0 5px;
}
.table > :not(caption) > * > * {
box-shadow: none;
}

View File

@ -4,6 +4,12 @@ body {
padding-top: 7rem;
}
/* App Logo */
.app-logo .img-fluid {
max-height: 90px;
padding: 0 5px 0 5px;
}
.table > :not(caption) > * > * {
box-shadow: none;
}

View File

@ -1,2 +1,3 @@
del "*.nupkg"
"..\..\[RootFolder]\oqtane.package\nuget.exe" pack [Owner].Theme.[Theme].nuspec
XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\wwwroot\Themes\" /Y