Example #1
0
 /// <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);
     }
 }
Example #2
0
        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);
        }
Example #4
0
        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));
        }
Example #5
0
        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));
        }
Example #6
0
        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;
        }