Exemple #1
0
        public async Task <IActionResult> Create(CreateApiViewModel model)
        {
            if (!_currentShellSettings.IsDefaultShell())
            {
                return(Forbid());
            }

            if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants))
            {
                return(this.ChallengeOrForbid("Api"));
            }

            // Creates a default shell settings based on the configuration.
            var shellSettings = _shellSettingsManager.CreateDefaultSettings();

            shellSettings.Name             = model.Name;
            shellSettings.RequestUrlHost   = model.RequestUrlHost;
            shellSettings.RequestUrlPrefix = model.RequestUrlPrefix;
            shellSettings.State            = TenantState.Uninitialized;

            shellSettings["ConnectionString"] = model.ConnectionString;
            shellSettings["TablePrefix"]      = model.TablePrefix;
            shellSettings["DatabaseProvider"] = model.DatabaseProvider;
            shellSettings["Secret"]           = Guid.NewGuid().ToString();
            shellSettings["RecipeName"]       = model.RecipeName;
            shellSettings["FeatureProfile"]   = model.FeatureProfile;

            model.IsNewTenant = true;

            ModelState.AddModelErrors(await _tenantValidator.ValidateAsync(model));

            if (ModelState.IsValid)
            {
                if (_shellHost.TryGetSettings(model.Name, out var settings))
                {
                    // Site already exists, return 201 for indempotency purpose

                    var token = CreateSetupToken(settings);

                    return(StatusCode(201, GetEncodedUrl(settings, token)));
                }
                else
                {
                    await _shellHost.UpdateShellSettingsAsync(shellSettings);

                    var token = CreateSetupToken(shellSettings);

                    return(Ok(GetEncodedUrl(shellSettings, token)));
                }
            }

            return(BadRequest(ModelState));
        }
        /// <summary>
        /// Called before releasing a tenant to update the related shell identifiers, locally and in the distributed cache.
        /// </summary>
        public async Task ReleasingAsync(string name)
        {
            if (_terminated)
            {
                return;
            }

            // If there is no default tenant or it is not running, nothing to do.
            if (!_shellHost.TryGetSettings(ShellHelper.DefaultShellName, out var settings) ||
                settings.State != TenantState.Running)
            {
                return;
            }

            // Acquire the distributed context or create a new one if not yet built.
            using var context = await AcquireOrCreateDistributedContextAsync(settings);

            // If the required distributed features are not enabled, nothing to do.
            var distributedCache = context.DistributedCache;

            if (distributedCache == null)
            {
                return;
            }

            var semaphore = _semaphores.GetOrAdd(name, name => new SemaphoreSlim(1));
            await semaphore.WaitAsync();

            try
            {
                // Update this tenant in the local collection with a new release identifier.
                var identifier = _identifiers.GetOrAdd(name, name => new ShellIdentifier());
                identifier.ReleaseId = IdGenerator.GenerateId();

                // Update the release identifier of this tenant in the distributed cache.
                await distributedCache.SetStringAsync(ReleaseIdKey(name), identifier.ReleaseId);

                // Also update the global identifier specifying that a tenant has changed.
                await distributedCache.SetStringAsync(ShellChangedIdKey, identifier.ReleaseId);
            }
            catch (Exception ex) when(!ex.IsFatal())
            {
                _logger.LogError(ex, "Unable to update the distributed cache before releasing the tenant '{TenantName}'.", name);
            }
            finally
            {
                semaphore.Release();
            }
        }
Exemple #3
0
        /// <summary>
        /// Tries to creates a standalone service scope that can be used to resolve local services and
        /// replaces <see cref="HttpContext.RequestServices"/> with it.
        /// </summary>
        /// <param name="tenant">The tenant name related to the service scope to get.</param>
        /// <returns>An associated scope if the tenant name is valid, otherwise null.</returns>
        /// <remarks>
        /// Disposing the returned <see cref="IServiceScope"/> instance restores the previous state.
        /// </remarks>
        public static async Task <IServiceScope> TryGetScopeAsync(this IShellHost shellHost, string tenant)
        {
            if (!shellHost.TryGetSettings(tenant, out var settings))
            {
                return(null);
            }

            return((await shellHost.GetScopeAndContextAsync(settings)).Scope);
        }
Exemple #4
0
        /// <summary>
        /// Retrieves the shell settings associated with the specified tenant.
        /// </summary>
        /// <returns>The shell settings associated with the tenant.</returns>
        public static ShellSettings GetSettings(this IShellHost shellHost, string name)
        {
            if (!shellHost.TryGetSettings(name, out ShellSettings settings))
            {
                throw new ArgumentException("The specified tenant name is not valid.", nameof(name));
            }

            return(settings);
        }
Exemple #5
0
        /// <summary>
        /// Tries to creates a standalone service scope that can be used to resolve local services and
        /// replaces <see cref="HttpContext.RequestServices"/> with it.
        /// </summary>
        /// <param name="tenant">The tenant name related to the scope and the shell to get.</param>
        /// <returns>Associated scope and shell if the tenant name is valid, otherwise null values.</returns>
        /// <remarks>
        /// Disposing the returned <see cref="IServiceScope"/> instance restores the previous state.
        /// </remarks>
        public static Task <(IServiceScope Scope, ShellContext ShellContext)> TryGetScopeAndContextAsync(this IShellHost shellHost, string tenant)
        {
            if (!shellHost.TryGetSettings(tenant, out var settings))
            {
                IServiceScope scope = null;
                ShellContext  shell = null;
                return(Task.FromResult((scope, shell)));
            }

            return(shellHost.GetScopeAndContextAsync(settings));
        }
Exemple #6
0
        public async Task <IActionResult> Confirm(string email, string handle, string siteName)
        {
            //  if (!_shellSettingsManager.TryGetSettings(handle, out var shellSettings))
            if (!_shellHost.TryGetSettings(handle, out var shellSettings))
            {
                return(NotFound());
            }

            var recipes = await _setupService.GetSetupRecipesAsync();

            // var recipe = recipes.FirstOrDefault(x => x.Name == shellSettings.RecipeName);
            var recipe = recipes.FirstOrDefault(x => x.Name == shellSettings["RecipeName"]); //changed by giannis

            if (recipe == null)
            {
                return(NotFound());
            }

            var setupContext = new SetupContext
            {
                ShellSettings   = shellSettings,
                SiteName        = siteName,
                EnabledFeatures = null,
                AdminUsername   = "******",
                AdminEmail      = email,
                AdminPassword   = "******",
                Errors          = new Dictionary <string, string>(),
                Recipe          = recipe,
                SiteTimeZone    = _clock.GetSystemTimeZone().TimeZoneId,
                //DatabaseProvider = shellSettings.DatabaseProvider,
                //DatabaseConnectionString = shellSettings.ConnectionString,
                //DatabaseTablePrefix = shellSettings.TablePrefix
                DatabaseProvider         = shellSettings["ConnectionString"], //changed by giannis
                DatabaseConnectionString = shellSettings["DatabaseConnectionString"],
                DatabaseTablePrefix      = shellSettings["DatabaseTablePrefix"]
            };

            var executionId = await _setupService.SetupAsync(setupContext);

            // Check if a component in the Setup failed
            if (setupContext.Errors.Any())
            {
                foreach (var error in setupContext.Errors)
                {
                    ModelState.AddModelError(error.Key, error.Value);
                }

                return(Redirect("Error"));
            }

            return(Redirect("~/" + handle));
        }
Exemple #7
0
        public async Task <IActionResult> SetupAsync(SetupViewModel model)
        {
            /*
             * if (!IsDefaultShell()) {
             *  return Unauthorized();
             * }
             */

            /*
             * if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants)) {
             *  return Unauthorized();
             * }
             */

            if (!ModelState.IsValid)
            {
                return(BadRequest());
            }

            if (!_shellHost.TryGetSettings(model.TenantName, out var shellSettings))
            {
                ModelState.AddModelError(nameof(SetupViewModel.TenantName), string.Format("Tenant not found: '{0}'", model.TenantName));
            }

            if (shellSettings.State == TenantState.Running)
            {
                return(StatusCode(201));
            }

            var setupContext = new SetupContext {
                ShellSettings   = shellSettings,
                EnabledFeatures = null, // default list,
            };

            var executionId = await _setupService.SetupAsync(setupContext);

            // Check if a component in the Setup failed
            if (setupContext.IsFailed)
            {
                foreach (var error in setupContext.Errors)
                {
                    ModelState.AddModelError(error.Key, error.Value);
                }

                return(StatusCode(500, ModelState));
            }

            return(Ok(executionId));
        }
        public async Task <IActionResult> Create(CreateApiViewModel model)
        {
            if (!IsDefaultShell())
            {
                return(Forbid());
            }

            if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants))
            {
                return(this.ChallengeOrForbid("Api"));
            }

            if (!String.IsNullOrEmpty(model.Name) && !Regex.IsMatch(model.Name, @"^\w+$"))
            {
                ModelState.AddModelError(nameof(CreateApiViewModel.Name), S["Invalid tenant name. Must contain characters only and no spaces."]);
            }

            // Creates a default shell settings based on the configuration.
            var shellSettings = _shellSettingsManager.CreateDefaultSettings();

            shellSettings.Name             = model.Name;
            shellSettings.RequestUrlHost   = model.RequestUrlHost;
            shellSettings.RequestUrlPrefix = model.RequestUrlPrefix;
            shellSettings.State            = TenantState.Uninitialized;

            shellSettings["ConnectionString"] = model.ConnectionString;
            shellSettings["TablePrefix"]      = model.TablePrefix;
            shellSettings["DatabaseProvider"] = model.DatabaseProvider;
            shellSettings["Secret"]           = Guid.NewGuid().ToString();
            shellSettings["RecipeName"]       = model.RecipeName;
            shellSettings["FeatureProfile"]   = model.FeatureProfile;

            if (!String.IsNullOrWhiteSpace(model.FeatureProfile))
            {
                var featureProfiles = await _featureProfilesService.GetFeatureProfilesAsync();

                if (!featureProfiles.ContainsKey(model.FeatureProfile))
                {
                    ModelState.AddModelError(nameof(CreateApiViewModel.FeatureProfile), S["The feature profile does not exist.", model.FeatureProfile]);
                }
            }
            if (String.IsNullOrWhiteSpace(shellSettings.RequestUrlHost) && String.IsNullOrWhiteSpace(shellSettings.RequestUrlPrefix))
            {
                ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["Host and url prefix can not be empty at the same time."]);
            }

            if (!String.IsNullOrWhiteSpace(shellSettings.RequestUrlPrefix))
            {
                if (shellSettings.RequestUrlPrefix.Contains('/'))
                {
                    ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["The url prefix can not contain more than one segment."]);
                }
            }

            if (ModelState.IsValid)
            {
                if (_shellHost.TryGetSettings(model.Name, out var settings))
                {
                    // Site already exists, return 201 for indempotency purpose

                    var token = CreateSetupToken(settings);

                    return(StatusCode(201, GetEncodedUrl(settings, token)));
                }
                else
                {
                    await _shellHost.UpdateShellSettingsAsync(shellSettings);

                    var token = CreateSetupToken(shellSettings);

                    return(Ok(GetEncodedUrl(shellSettings, token)));
                }
            }

            return(BadRequest(ModelState));
        }
        public async Task <IActionResult> IndexPost(RegisterUserViewModel model)
        {
            if (!model.AcceptTerms)
            {
                ModelState.AddModelError(nameof(RegisterUserViewModel.AcceptTerms), S["Please, accept the terms and conditions."]);
            }

            if (!string.IsNullOrEmpty(model.Handle) && !Regex.IsMatch(model.Handle, @"^\w+$"))
            {
                ModelState.AddModelError(nameof(RegisterUserViewModel.Handle), S["Invalid tenant name. Must contain characters only and no spaces."]);
            }

            if (ModelState.IsValid)
            {
                if (_shellHost.TryGetSettings(model.Handle, out var shellSettings))
                {
                    ModelState.AddModelError(nameof(RegisterUserViewModel.Handle), S["This site name already exists."]);
                }
                else
                {
                    shellSettings = new ShellSettings
                    {
                        Name             = model.Handle,
                        RequestUrlPrefix = model.Handle.ToLower(),
                        RequestUrlHost   = null,
                        State            = TenantState.Uninitialized
                    };
                    shellSettings["RecipeName"]       = model.RecipeName;
                    shellSettings["DatabaseProvider"] = "Sqlite";

                    await _shellSettingsManager.SaveSettingsAsync(shellSettings);

                    var shellContext = await _shellHost.GetOrCreateShellContextAsync(shellSettings);

                    var recipes = await _setupService.GetSetupRecipesAsync();

                    var recipe = recipes.FirstOrDefault(x => x.Name == model.RecipeName);

                    if (recipe == null)
                    {
                        ModelState.AddModelError(nameof(RegisterUserViewModel.RecipeName), S["Invalid recipe name."]);
                    }

                    var adminName     = defaultAdminName;
                    var adminPassword = GenerateRandomPassword();
                    var siteName      = model.SiteName;
                    var siteUrl       = GetTenantUrl(shellSettings);

                    var dataProtector     = _dataProtectionProvider.CreateProtector(dataProtectionPurpose).ToTimeLimitedDataProtector();
                    var encryptedPassword = dataProtector.Protect(adminPassword, _clock.UtcNow.Add(new TimeSpan(24, 0, 0)));
                    var confirmationLink  = Url.Action(nameof(Confirm), "Home", new { email = model.Email, handle = model.Handle, siteName = model.SiteName, ep = encryptedPassword }, Request.Scheme);

                    var message = new MailMessage();
                    if (emailToBcc)
                    {
                        message.Bcc = _smtpSettingsOptions.Value.DefaultSender;
                    }
                    message.To         = model.Email;
                    message.IsBodyHtml = true;
                    message.Subject    = emailSubject;
                    message.Body       = S[$"Hello,<br><br>Your demo site '{siteName}' has been created.<br><br>1) Setup your site by opening <a href=\"{confirmationLink}\">this link</a>.<br><br>2) Log into the <a href=\"{siteUrl}/admin\">admin</a> with these credentials:<br>Username: {adminName}<br>Password: {adminPassword}"];

                    await _smtpService.SendAsync(message);

                    return(RedirectToAction(nameof(Success)));
                }
            }

            return(View(nameof(Index), model));
        }
        public async Task <ImmutableArray <ValidationResult> > ValidateSettingsAsync(OpenIdValidationSettings settings)
        {
            if (settings == null)
            {
                throw new ArgumentNullException(nameof(settings));
            }

            var results = ImmutableArray.CreateBuilder <ValidationResult>();

            if (!(settings.Authority == null ^ string.IsNullOrEmpty(settings.Tenant)))
            {
                results.Add(new ValidationResult(S["Either a tenant or an authority must be registered."], new[]
                {
                    nameof(settings.Authority),
                    nameof(settings.Tenant)
                }));
            }

            if (settings.Authority != null)
            {
                if (!settings.Authority.IsAbsoluteUri || !settings.Authority.IsWellFormedOriginalString())
                {
                    results.Add(new ValidationResult(S["The specified authority is not valid."], new[]
                    {
                        nameof(settings.Authority)
                    }));
                }

                if (!string.IsNullOrEmpty(settings.Authority.Query) || !string.IsNullOrEmpty(settings.Authority.Fragment))
                {
                    results.Add(new ValidationResult(S["The authority cannot contain a query string or a fragment."], new[]
                    {
                        nameof(settings.Authority)
                    }));
                }
            }

            if (!string.IsNullOrEmpty(settings.Tenant) && !string.IsNullOrEmpty(settings.Audience))
            {
                results.Add(new ValidationResult(S["No audience can be set when using another tenant."], new[]
                {
                    nameof(settings.Audience)
                }));
            }

            if (settings.Authority != null && string.IsNullOrEmpty(settings.Audience))
            {
                results.Add(new ValidationResult(S["An audience must be set when configuring the authority."], new[]
                {
                    nameof(settings.Audience)
                }));
            }

            if (!string.IsNullOrEmpty(settings.Audience) &&
                settings.Audience.StartsWith(OpenIdConstants.Prefixes.Tenant, StringComparison.OrdinalIgnoreCase))
            {
                results.Add(new ValidationResult(S["The audience cannot start with the special 'oct:' prefix."], new[]
                {
                    nameof(settings.Audience)
                }));
            }

            // If a tenant was specified, ensure it is valid, that the OpenID server feature
            // was enabled and that at least a scope linked with the current tenant exists.
            if (!string.IsNullOrEmpty(settings.Tenant) &&
                !string.Equals(settings.Tenant, _shellSettings.Name))
            {
                if (!_shellHost.TryGetSettings(settings.Tenant, out var shellSettings))
                {
                    results.Add(new ValidationResult(S["The specified tenant is not valid."]));
                }
                else
                {
                    var shellScope = await _shellHost.GetScopeAsync(shellSettings);

                    await shellScope.UsingAsync(async scope =>
                    {
                        var manager = scope.ServiceProvider.GetService <IOpenIdScopeManager>();
                        if (manager == null)
                        {
                            results.Add(new ValidationResult(S["The specified tenant is not valid."], new[]
                            {
                                nameof(settings.Tenant)
                            }));
                        }
                        else
                        {
                            var resource = OpenIdConstants.Prefixes.Tenant + _shellSettings.Name;
                            var scopes   = await manager.FindByResourceAsync(resource);
                            if (scopes.IsDefaultOrEmpty)
                            {
                                results.Add(new ValidationResult(S["No appropriate scope was found."], new[]
                                {
                                    nameof(settings.Tenant)
                                }));
                            }
                        }
                    });
                }
            }

            return(results.ToImmutable());
        }
        public async Task <IActionResult> Create(CreateApiViewModel model)
        {
            if (!IsDefaultShell())
            {
                return(Unauthorized());
            }

            if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageTenants))
            {
                return(Unauthorized());
            }

            var allShells = await GetShellsAsync();

            if (!string.IsNullOrEmpty(model.Name) && !Regex.IsMatch(model.Name, @"^\w+$"))
            {
                ModelState.AddModelError(nameof(CreateApiViewModel.Name), S["Invalid tenant name. Must contain characters only and no spaces."]);
            }

            if (!IsDefaultShell() && string.IsNullOrWhiteSpace(model.RequestUrlHost) && string.IsNullOrWhiteSpace(model.RequestUrlPrefix))
            {
                ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["Host and url prefix can not be empty at the same time."]);
            }

            if (!string.IsNullOrWhiteSpace(model.RequestUrlPrefix))
            {
                if (model.RequestUrlPrefix.Contains('/'))
                {
                    ModelState.AddModelError(nameof(CreateApiViewModel.RequestUrlPrefix), S["The url prefix can not contains more than one segment."]);
                }
            }

            if (ModelState.IsValid)
            {
                if (_shellHost.TryGetSettings(model.Name, out var shellSettings))
                {
                    // Site already exists, return 200 for indempotency purpose

                    var token = CreateSetupToken(shellSettings);

                    return(StatusCode(201, GetTenantUrl(shellSettings, token)));
                }
                else
                {
                    shellSettings = new ShellSettings
                    {
                        Name             = model.Name,
                        RequestUrlPrefix = model.RequestUrlPrefix?.Trim(),
                        RequestUrlHost   = model.RequestUrlHost,
                        ConnectionString = model.ConnectionString,
                        TablePrefix      = model.TablePrefix,
                        DatabaseProvider = model.DatabaseProvider,
                        State            = TenantState.Uninitialized,
                        Secret           = Guid.NewGuid().ToString(),
                        RecipeName       = model.RecipeName
                    };

                    _shellSettingsManager.SaveSettings(shellSettings);
                    var shellContext = await _shellHost.GetOrCreateShellContextAsync(shellSettings);

                    var token = CreateSetupToken(shellSettings);

                    return(Ok(GetTenantUrl(shellSettings, token)));
                }
            }

            return(BadRequest(ModelState));
        }