/// <summary> /// Releases all shells so that new ones will be built for subsequent requests. /// Note: Can be used to free up resources after a given period of inactivity. /// </summary> public async static Task ReleaseAllShellContextsAsync(this IShellHost shellHost) { foreach (var settings in shellHost.GetAllSettings()) { await shellHost.ReleaseShellContextAsync(settings); } }
public ValueTask <FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context) { var tenantUrl = string.Empty; if (input.ToObjectValue() is ClaimsPrincipal principal) { var username = principal.Identity.Name; //if tenant is found with same name as logged in user return true //todo: also check tenant state var shellSettings = _shellHost .GetAllSettings() .FirstOrDefault(x => string.Equals(x.Name, username, StringComparison.OrdinalIgnoreCase)); if (shellSettings != null) { if (!context.AmbientValues.TryGetValue("Services", out var services)) { throw new ArgumentException("Services missing while invoking 'tags'"); } var _httpContext = ((IServiceProvider)services).GetRequiredService <IHttpContextAccessor>()?.HttpContext; tenantUrl = GetTenantUrl(_httpContext?.Request, shellSettings); } } return(new ValueTask <FluidValue>(new StringValue(tenantUrl))); }
public async Task <IEnumerable <ModelError> > ValidateAsync(TenantViewModel model) { var errors = new List <ModelError>(); var selectedProvider = _databaseProviders.FirstOrDefault(x => x.Value == model.DatabaseProvider); if (selectedProvider != null && selectedProvider.HasConnectionString && String.IsNullOrWhiteSpace(model.ConnectionString)) { errors.Add(new ModelError(nameof(model.ConnectionString), S["The connection string is mandatory for this provider."])); } if (String.IsNullOrWhiteSpace(model.Name)) { errors.Add(new ModelError(nameof(model.Name), S["The tenant name is mandatory."])); } if (!String.IsNullOrWhiteSpace(model.FeatureProfile)) { var featureProfiles = await _featureProfilesService.GetFeatureProfilesAsync(); if (!featureProfiles.ContainsKey(model.FeatureProfile)) { errors.Add(new ModelError(nameof(model.FeatureProfile), S["The feature profile does not exist."])); } } if (!String.IsNullOrEmpty(model.Name) && !Regex.IsMatch(model.Name, @"^\w+$")) { errors.Add(new ModelError(nameof(model.Name), S["Invalid tenant name. Must contain characters only and no spaces."])); } if (!_shellSettings.IsDefaultShell() && String.IsNullOrWhiteSpace(model.RequestUrlHost) && String.IsNullOrWhiteSpace(model.RequestUrlPrefix)) { errors.Add(new ModelError(nameof(model.RequestUrlPrefix), S["Host and url prefix can not be empty at the same time."])); } if (!String.IsNullOrWhiteSpace(model.RequestUrlPrefix)) { if (model.RequestUrlPrefix.Contains('/')) { errors.Add(new ModelError(nameof(model.RequestUrlPrefix), S["The url prefix can not contain more than one segment."])); } } var allSettings = _shellHost.GetAllSettings(); if (model.IsNewTenant && allSettings.Any(tenant => String.Equals(tenant.Name, model.Name, StringComparison.OrdinalIgnoreCase))) { errors.Add(new ModelError(nameof(model.Name), S["A tenant with the same name already exists."])); } var allOtherShells = allSettings.Where(t => !String.Equals(t.Name, model.Name, StringComparison.OrdinalIgnoreCase)); if (allOtherShells.Any(tenant => String.Equals(tenant.RequestUrlPrefix, model.RequestUrlPrefix?.Trim(), StringComparison.OrdinalIgnoreCase) && DoesUrlHostExist(tenant.RequestUrlHost, model.RequestUrlHost))) { errors.Add(new ModelError(nameof(model.RequestUrlPrefix), S["A tenant with the same host and prefix already exists."])); } return(errors); }
public async Task <IActionResult> Create(string returnUrl = null) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageScopes)) { return(Forbid()); } var model = new CreateOpenIdScopeViewModel(); foreach (var tenant in _shellHost.GetAllSettings().Where(s => s.State == TenantState.Running)) { model.Tenants.Add(new CreateOpenIdScopeViewModel.TenantEntry { Current = string.Equals(tenant.Name, _shellSettings.Name), Name = tenant.Name }); } ViewData["ReturnUrl"] = returnUrl; return(View(model)); }
public ValueTask <FluidValue> ProcessAsync(FluidValue input, FilterArguments arguments, TemplateContext context) { var ret = false; if (input.ToObjectValue() is ClaimsPrincipal principal) { var username = principal.Identity.Name; //if tenant is found with same name as logged in user return true //todo: also check tenant state var shellSettings = _shellHost .GetAllSettings() .FirstOrDefault(x => string.Equals(x.Name, username, StringComparison.OrdinalIgnoreCase)); if (shellSettings != null) { ret = true; } } return(new ValueTask <FluidValue>(ret ? BooleanValue.True : BooleanValue.False)); }
public async Task <IActionResult> Index(TenantIndexOptions options, PagerParameters pagerParameters) { var allSettings = _shellHost.GetAllSettings().OrderBy(s => s.Name); var dataProtector = _dataProtectorProvider.CreateProtector("Tokens").ToTimeLimitedDataProtector(); var siteSettings = await _siteService.GetSiteSettingsAsync(); var pager = new Pager(pagerParameters, siteSettings.PageSize); // default options if (options == null) { options = new TenantIndexOptions(); } var entries = allSettings.Select(x => { var entry = new ShellSettingsEntry { Name = x.Name, ShellSettings = x, IsDefaultTenant = string.Equals(x.Name, ShellHelper.DefaultShellName, StringComparison.OrdinalIgnoreCase) }; if (x.State == TenantState.Uninitialized && !string.IsNullOrEmpty(x["Secret"])) { entry.Token = dataProtector.Protect(x["Secret"], _clock.UtcNow.Add(new TimeSpan(24, 0, 0))); } return(entry); }).ToList(); if (!string.IsNullOrWhiteSpace(options.Search)) { entries = entries.Where(t => t.Name.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1 || (t.ShellSettings != null && t.ShellSettings != null && ((t.ShellSettings.RequestUrlHost != null && t.ShellSettings.RequestUrlHost.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1) || (t.ShellSettings.RequestUrlPrefix != null && t.ShellSettings.RequestUrlPrefix.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1)))).ToList(); } switch (options.Filter) { case TenantsFilter.Disabled: entries = entries.Where(t => t.ShellSettings.State == TenantState.Disabled).ToList(); break; case TenantsFilter.Running: entries = entries.Where(t => t.ShellSettings.State == TenantState.Running).ToList(); break; case TenantsFilter.Uninitialized: entries = entries.Where(t => t.ShellSettings.State == TenantState.Uninitialized).ToList(); break; } switch (options.OrderBy) { case TenantsOrder.Name: entries = entries.OrderBy(t => t.Name).ToList(); break; case TenantsOrder.State: entries = entries.OrderBy(t => t.ShellSettings?.State).ToList(); break; default: entries = entries.OrderByDescending(t => t.Name).ToList(); break; } var count = entries.Count(); var results = entries .Skip(pager.GetStartIndex()) .Take(pager.PageSize).ToList(); // Maintain previous route data when generating page links var routeData = new RouteData(); routeData.Values.Add("Options.Filter", options.Filter); routeData.Values.Add("Options.Search", options.Search); routeData.Values.Add("Options.OrderBy", options.OrderBy); var pagerShape = (await New.Pager(pager)).TotalItemCount(count).RouteData(routeData); var model = new AdminIndexViewModel { ShellSettingsEntries = results, Options = options, Pager = pagerShape }; return(View(model)); }
public async Task <IActionResult> Index(TenantIndexOptions options, PagerParameters pagerParameters) { if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants)) { return(Forbid()); } if (!_currentShellSettings.IsDefaultShell()) { return(Forbid()); } var allSettings = _shellHost.GetAllSettings().OrderBy(s => s.Name); var dataProtector = _dataProtectorProvider.CreateProtector("Tokens").ToTimeLimitedDataProtector(); var siteSettings = await _siteService.GetSiteSettingsAsync(); var pager = new Pager(pagerParameters, siteSettings.PageSize); var entries = allSettings.Select(x => { var entry = new ShellSettingsEntry { Category = x["Category"], Description = x["Description"], Name = x.Name, ShellSettings = x, IsDefaultTenant = String.Equals(x.Name, ShellHelper.DefaultShellName, StringComparison.OrdinalIgnoreCase) }; if (x.State == TenantState.Uninitialized && !String.IsNullOrEmpty(x["Secret"])) { entry.Token = dataProtector.Protect(x["Secret"], _clock.UtcNow.Add(new TimeSpan(24, 0, 0))); } return(entry); }).ToList(); if (!String.IsNullOrWhiteSpace(options.Search)) { entries = entries.Where(t => t.Name.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1 || (t.ShellSettings != null && ((t.ShellSettings.RequestUrlHost != null && t.ShellSettings.RequestUrlHost.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1) || (t.ShellSettings.RequestUrlPrefix != null && t.ShellSettings.RequestUrlPrefix.IndexOf(options.Search, StringComparison.OrdinalIgnoreCase) > -1)))).ToList(); } if (!String.IsNullOrWhiteSpace(options.Category)) { entries = entries.Where(t => t.Category?.Equals(options.Category, StringComparison.OrdinalIgnoreCase) == true).ToList(); } switch (options.Status) { case TenantsState.Disabled: entries = entries.Where(t => t.ShellSettings.State == TenantState.Disabled).ToList(); break; case TenantsState.Running: entries = entries.Where(t => t.ShellSettings.State == TenantState.Running).ToList(); break; case TenantsState.Uninitialized: entries = entries.Where(t => t.ShellSettings.State == TenantState.Uninitialized).ToList(); break; } switch (options.OrderBy) { case TenantsOrder.Name: entries = entries.OrderBy(t => t.Name).ToList(); break; case TenantsOrder.State: entries = entries.OrderBy(t => t.ShellSettings?.State).ToList(); break; default: entries = entries.OrderByDescending(t => t.Name).ToList(); break; } var count = entries.Count(); var results = entries .Skip(pager.GetStartIndex()) .Take(pager.PageSize).ToList(); // Maintain previous route data when generating page links var routeData = new RouteData(); routeData.Values.Add("Options.Category", options.Category); routeData.Values.Add("Options.Status", options.Status); routeData.Values.Add("Options.Search", options.Search); routeData.Values.Add("Options.OrderBy", options.OrderBy); var pagerShape = (await New.Pager(pager)).TotalItemCount(count).RouteData(routeData); var model = new AdminIndexViewModel { ShellSettingsEntries = results, Options = options, Pager = pagerShape }; // We populate the SelectLists model.Options.TenantsCategories = allSettings .GroupBy(t => t["Category"]) .Where(t => !String.IsNullOrEmpty(t.Key)) .Select(t => new SelectListItem(t.Key, t.Key, String.Equals(options.Category, t.Key, StringComparison.OrdinalIgnoreCase))) .ToList(); model.Options.TenantsCategories.Insert(0, new SelectListItem( S["All"], String.Empty, selected: String.IsNullOrEmpty(options.Category))); model.Options.TenantsStates = new List <SelectListItem>() { new SelectListItem() { Text = S["All states"], Value = nameof(TenantsState.All) }, new SelectListItem() { Text = S["Running"], Value = nameof(TenantsState.Running) }, new SelectListItem() { Text = S["Disabled"], Value = nameof(TenantsState.Disabled) }, new SelectListItem() { Text = S["Uninitialized"], Value = nameof(TenantsState.Uninitialized) } }; model.Options.TenantsSorts = new List <SelectListItem>() { new SelectListItem() { Text = S["Name"], Value = nameof(TenantsOrder.Name) }, new SelectListItem() { Text = S["State"], Value = nameof(TenantsOrder.State) } }; model.Options.TenantsBulkAction = new List <SelectListItem>() { new SelectListItem() { Text = S["Disable"], Value = nameof(TenantsBulkAction.Disable) }, new SelectListItem() { Text = S["Enable"], Value = nameof(TenantsBulkAction.Enable) } }; return(View(model)); }
/// <summary> /// Keep in sync tenants by sharing shell identifiers through an <see cref="IDistributedCache"/>. /// </summary> protected override async Task ExecuteAsync(CancellationToken stoppingToken) { stoppingToken.Register(() => { _logger.LogInformation("'{ServiceName}' is stopping.", nameof(DistributedShellHostedService)); }); // Init the idle time. var idleTime = MinIdleTime; while (!stoppingToken.IsCancellationRequested) { try { // Wait for the current idle time on each loop. if (!await TryWaitAsync(idleTime, stoppingToken)) { break; } // If there is no default tenant or it is not running, nothing to do. if (!_shellHost.TryGetShellContext(ShellHelper.DefaultShellName, out var defaultContext) || defaultContext.Settings.State != TenantState.Running) { continue; } // Get or create a new distributed context if the default tenant has changed. var context = await GetOrCreateDistributedContextAsync(defaultContext); // If the required distributed features are not enabled, nothing to do. var distributedCache = context?.DistributedCache; if (distributedCache == null) { continue; } // Try to retrieve the tenant changed global identifier from the distributed cache. string shellChangedId; try { shellChangedId = await distributedCache.GetStringAsync(ShellChangedIdKey); } catch (Exception ex) when(!ex.IsFatal()) { // Get the next idle time before retrying to read the distributed cache. idleTime = NextIdleTimeBeforeRetry(idleTime, ex); continue; } // Reset the idle time. idleTime = MinIdleTime; // Check if at least one tenant has changed. if (shellChangedId == null || _shellChangedId == shellChangedId) { continue; } // Try to retrieve the tenant created global identifier from the distributed cache. string shellCreatedId; try { shellCreatedId = await distributedCache.GetStringAsync(ShellCreatedIdKey); } catch (Exception ex) when(!ex.IsFatal()) { _logger.LogError(ex, "Unable to read the distributed cache before checking if a tenant has been created."); continue; } // Retrieve all tenant settings that are already loaded. var allSettings = _shellHost.GetAllSettings().ToList(); // Check if at least one tenant has been created. if (shellCreatedId != null && _shellCreatedId != shellCreatedId) { // Retrieve all new created tenants that are not already loaded. var names = (await _shellSettingsManager.LoadSettingsNamesAsync()) .Except(allSettings.Select(s => s.Name)) .ToArray(); // Load and enlist the settings of all new created tenant. foreach (var name in names) { allSettings.Add(await _shellSettingsManager.LoadSettingsAsync(name)); } } // Init the busy start time. var _busyStartTime = DateTime.UtcNow; var syncingSuccess = true; // Keep in sync all tenants by checking their specific identifiers. foreach (var settings in allSettings) { // Wait for the min idle time after the max busy time. if (!await TryWaitAfterBusyTime(stoppingToken)) { break; } var semaphore = _semaphores.GetOrAdd(settings.Name, name => new SemaphoreSlim(1)); await semaphore.WaitAsync(); try { // Try to retrieve the release identifier of this tenant from the distributed cache. var releaseId = await distributedCache.GetStringAsync(ReleaseIdKey(settings.Name)); if (releaseId != null) { // Check if the release identifier of this tenant has changed. var identifier = _identifiers.GetOrAdd(settings.Name, name => new ShellIdentifier()); if (identifier.ReleaseId != releaseId) { // Upate the local identifier. identifier.ReleaseId = releaseId; // Keep in sync this tenant by releasing it locally. await _shellHost.ReleaseShellContextAsync(settings, eventSource : false); } } // Try to retrieve the reload identifier of this tenant from the distributed cache. var reloadId = await distributedCache.GetStringAsync(ReloadIdKey(settings.Name)); if (reloadId != null) { // Check if the reload identifier of this tenant has changed. var identifier = _identifiers.GetOrAdd(settings.Name, name => new ShellIdentifier()); if (identifier.ReloadId != reloadId) { // Upate the local identifier. identifier.ReloadId = reloadId; // Keep in sync this tenant by reloading it locally. await _shellHost.ReloadShellContextAsync(settings, eventSource : false); } } } catch (Exception ex) when(!ex.IsFatal()) { syncingSuccess = false; _logger.LogError(ex, "Unable to read the distributed cache while syncing the tenant '{TenantName}'.", settings.Name); break; } finally { semaphore.Release(); } } // Keep in sync the tenant global identifiers. if (syncingSuccess) { _shellChangedId = shellChangedId; _shellCreatedId = shellCreatedId; } } catch (Exception ex) when(!ex.IsFatal()) { _logger.LogError(ex, "Error while executing '{ServiceName}'", nameof(DistributedShellHostedService)); } } _terminated = true; _context?.Release(); _defaultContext = null; _context = null; }