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(); } }
/// <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); }
/// <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); }
/// <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)); }
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)); }
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)); }