-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SaaS Configuration module #207
Open
jptissot
wants to merge
2
commits into
master
Choose a base branch
from
saas_auth
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# SaaS Configuration (`StatCan.OrchardCore.SaaSConfiguration`) | ||
|
||
This module aims at simplifying the configuration of a SaaS application. | ||
|
||
## Features | ||
- Automatic configuration of OpenId Server (Server, Application, Scopes) | ||
- Automatic configuration of OpenId Clients for tenants. | ||
|
||
When a new tenant is created and has the `StatCan.OrchardCore.SaaSConfiguration.Client` feature enabled, this module will automatically sync the Client Id and Secret when the Main tenant modifies the settings. | ||
|
||
## Roadmap | ||
- Automatic configuration of Login / Registration settings for child tenants. | ||
- Role mapping configuration for child tenants | ||
- Mapping of UserProfile properties for tenants (based on the Default tenant's claims) | ||
- Running of recipes in child tenants (maybe multiple at once) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
src/Modules/StatCan.OrchardCore.SaaSConfiguration/AdminMenu.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using System; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Localization; | ||
using OrchardCore.Environment.Shell.Descriptor.Models; | ||
using OrchardCore.Navigation; | ||
|
||
namespace StatCan.OrchardCore.SaaSConfiguration | ||
{ | ||
public class AdminMenu : INavigationProvider | ||
{ | ||
private readonly IStringLocalizer S; | ||
|
||
public AdminMenu( | ||
IStringLocalizer<AdminMenu> localizer) | ||
{ | ||
S = localizer; | ||
} | ||
|
||
public Task BuildNavigationAsync(string name, NavigationBuilder builder) | ||
{ | ||
if (!String.Equals(name, "admin", StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
|
||
builder | ||
.Add(S["Configuration"], configuration => configuration | ||
.Add(S["SaaS"], security => security | ||
|
||
.Add(S["OpenId"], S["OpenId"].PrefixPosition(), openid => | ||
{ | ||
openid.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = Constants.Features.SaaSConfiguration }) | ||
.Permission(Permissions.ManageSaaSConfiguration) | ||
.LocalNav(); | ||
}) | ||
.Add(S["Execute JSON"], S["Execute JSON"].PrefixPosition(), deployment => deployment | ||
.Action("ExecuteRecipe", "Admin", Constants.Features.SaaSConfiguration) | ||
.Permission(Permissions.ManageSaaSConfiguration) | ||
.LocalNav() | ||
) | ||
) | ||
); | ||
|
||
return Task.CompletedTask; | ||
} | ||
} | ||
} |
123 changes: 123 additions & 0 deletions
123
src/Modules/StatCan.OrchardCore.SaaSConfiguration/ClientMigrations.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using OrchardCore.Data.Migration; | ||
using Microsoft.AspNetCore.DataProtection; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.IdentityModel.Protocols.OpenIdConnect; | ||
using OpenIddict.Abstractions; | ||
using OrchardCore.Entities; | ||
using OrchardCore.Environment.Extensions; | ||
using OrchardCore.Environment.Shell; | ||
using OrchardCore.OpenId.Configuration; | ||
using OrchardCore.OpenId.Services; | ||
using OrchardCore.Settings; | ||
using OrchardCore.Setup.Events; | ||
using Microsoft.AspNetCore.Http; | ||
using OrchardCore.DisplayManagement.Notify; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
|
||
namespace StatCan.OrchardCore.SaaSConfiguration | ||
{ | ||
public class ClientMigrations : DataMigration | ||
{ | ||
private readonly IShellHost _shellHost; | ||
private readonly ITenantHelperService _tenantHelper; | ||
private readonly IOpenIdClientService _openIdClientService; | ||
private readonly IDataProtectionProvider _dataProtectionProvider; | ||
private readonly IHttpContextAccessor _httpContextAccessor; | ||
private readonly ShellSettings _shellSettings; | ||
private readonly INotifier _notifier; | ||
private readonly IHtmlLocalizer H; | ||
|
||
public ClientMigrations(IShellHost shellHost, | ||
ITenantHelperService tenantHelper, | ||
IOpenIdClientService openIdClientService, | ||
IDataProtectionProvider dataProtectionProvider, | ||
IHttpContextAccessor httpContextAccessor, | ||
ShellSettings shellSettings, | ||
INotifier notifier, | ||
IHtmlLocalizer<ClientMigrations> htmlLocalizer) | ||
{ | ||
_shellHost = shellHost; | ||
_tenantHelper = tenantHelper; | ||
_openIdClientService = openIdClientService; | ||
_dataProtectionProvider = dataProtectionProvider; | ||
_httpContextAccessor = httpContextAccessor; | ||
_shellSettings = shellSettings; | ||
_notifier = notifier; | ||
H = htmlLocalizer; | ||
} | ||
|
||
public async Task<int> CreateAsync() | ||
{ | ||
var request = _httpContextAccessor.HttpContext.Request; | ||
|
||
// Get the Configuration options from the default tenant | ||
bool moduleEnabled = true; | ||
SaaSConfigurationSettings saasConfigurationSettings = null; | ||
string unprotectedSecret = null; | ||
Uri authority = null; | ||
|
||
var currentUri = new UriBuilder() | ||
{ | ||
Scheme = request.Scheme, | ||
Host = request.Host.Host, | ||
Port = request.Host.Port ?? 0, | ||
Path = _shellSettings.RequestUrlPrefix + "/signin-oidc" | ||
}; | ||
|
||
var shellScope = await _shellHost.GetScopeAsync(ShellHelper.DefaultShellName); | ||
// This code is running on the default tenant to get the configuration settings for this tenant | ||
await shellScope.UsingAsync(async scope => | ||
{ | ||
var extensionManager = scope.ServiceProvider.GetRequiredService<IExtensionManager>(); | ||
var feature = extensionManager.GetFeatures().FirstOrDefault(f => f.Id == Constants.Features.SaaSConfiguration); | ||
// tenant does not have our ConfigurationClient enabled, thus not configured by this module. | ||
if(feature == null) | ||
{ | ||
moduleEnabled = false; | ||
return; | ||
} | ||
|
||
var saasConfigurationService = scope.ServiceProvider.GetRequiredService<ISaaSConfigurationService>(); | ||
|
||
saasConfigurationSettings = await saasConfigurationService.GetSettingsAsync(); | ||
unprotectedSecret = saasConfigurationService.GetUnprotectedClientSecret(saasConfigurationSettings.ClientSecret); | ||
authority = saasConfigurationSettings.Authority; | ||
|
||
await saasConfigurationService.UpdateRedirectUris(currentUri.ToString()); | ||
}); | ||
|
||
if(!moduleEnabled) | ||
{ | ||
await _tenantHelper.DisableFeatureAsync(Constants.Features.SaaSConfigurationClient); | ||
_notifier.Error(H["The Default tenant does not have the {0} feature enabled. The {1} module has been disabled as a result", Constants.Features.SaaSConfiguration, Constants.Features.SaaSConfigurationClient]); | ||
return 0 ; | ||
} | ||
|
||
var settings = await _openIdClientService.GetSettingsAsync(); | ||
|
||
settings.Authority = authority; | ||
settings.ClientId = saasConfigurationSettings.ClientId; | ||
settings.Scopes = new string[]{OpenIddictConstants.Scopes.Email}; | ||
settings.DisplayName = "Login"; //todo i18n? | ||
settings.ResponseType = OpenIdConnectResponseType.Code; | ||
settings.ResponseMode = OpenIdConnectResponseMode.FormPost; | ||
|
||
// protect the client secret using the openid protector. | ||
var clientProtector = _dataProtectionProvider.CreateProtector(nameof(OpenIdClientConfiguration)); | ||
settings.ClientSecret = clientProtector.Protect(unprotectedSecret); | ||
|
||
var results = await _openIdClientService.ValidateSettingsAsync(settings); | ||
|
||
await _openIdClientService.UpdateSettingsAsync(settings); | ||
|
||
// restart the tenant to reload the OpenId connect configuration | ||
await _shellHost.ReleaseShellContextAsync(_shellSettings); | ||
|
||
return 1; | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/Modules/StatCan.OrchardCore.SaaSConfiguration/Constants.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace StatCan.OrchardCore.SaaSConfiguration | ||
{ | ||
public static class Constants | ||
{ | ||
public static class Features | ||
{ | ||
public const string SaaSConfiguration = "StatCan.OrchardCore.SaaSConfiguration"; | ||
public const string SaaSConfigurationClient = "StatCan.OrchardCore.SaaSConfiguration.Client"; | ||
} | ||
} | ||
} |
122 changes: 122 additions & 0 deletions
122
src/Modules/StatCan.OrchardCore.SaaSConfiguration/Controllers/AdminController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Authorization; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
using Microsoft.AspNetCore.Mvc.Rendering; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.FileProviders; | ||
using Microsoft.Extensions.Localization; | ||
using OrchardCore.Deployment.Services; | ||
using OrchardCore.DisplayManagement.Notify; | ||
using OrchardCore.Mvc.Utilities; | ||
using StatCan.OrchardCore.SaaSConfiguration.ViewModels; | ||
|
||
namespace StatCan.OrchardCore.SaaSConfiguration.Controllers | ||
{ | ||
public class AdminController : Controller | ||
{ | ||
private readonly IAuthorizationService _authorizationService; | ||
private readonly INotifier _notifier; | ||
private readonly IHtmlLocalizer H; | ||
private readonly IStringLocalizer S; | ||
private readonly ITenantHelperService _helperService; | ||
|
||
public AdminController( | ||
IAuthorizationService authorizationService, | ||
INotifier notifier, | ||
IHtmlLocalizer<AdminController> htmlLocalizer, | ||
IStringLocalizer<AdminController> stringLocalizer, | ||
ITenantHelperService helperService | ||
) | ||
{ | ||
_authorizationService = authorizationService; | ||
_notifier = notifier; | ||
_helperService = helperService; | ||
|
||
H = htmlLocalizer; | ||
S = stringLocalizer; | ||
} | ||
|
||
public async Task<IActionResult> ExecuteRecipe() | ||
{ | ||
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSaaSConfiguration)) | ||
{ | ||
return Forbid(); | ||
} | ||
|
||
var vm = new ExecuteRecipeViewModel | ||
{ | ||
AllTenants = _helperService.GetTenantsExceptDefault().Select(t => new SelectListItem(t.Name, t.Name)).ToList() | ||
}; | ||
|
||
return View(vm); | ||
} | ||
|
||
[HttpPost] | ||
public async Task<IActionResult> ExecuteRecipe(ExecuteRecipeViewModel model) | ||
{ | ||
if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageSaaSConfiguration)) | ||
{ | ||
return Forbid(); | ||
} | ||
|
||
if (!model.Json.IsJson()) | ||
{ | ||
ModelState.AddModelError(nameof(model.Json), S["The recipe is written in an incorrect json format."]); | ||
} | ||
|
||
if(model.SelectedTenantNames.Count == 0) | ||
{ | ||
ModelState.AddModelError(nameof(model.SelectedTenantNames), S["Please select at least one tenant to execute the recipe"]); | ||
} | ||
|
||
if (ModelState.IsValid) | ||
{ | ||
var tempArchiveFolder = PathExtensions.Combine(Path.GetTempPath(), Path.GetRandomFileName()); | ||
|
||
try | ||
{ | ||
Directory.CreateDirectory(tempArchiveFolder); | ||
System.IO.File.WriteAllText(Path.Combine(tempArchiveFolder, "Recipe.json"), model.Json); | ||
// todo: fix this to only apply for selected tenants | ||
var tenants = _helperService.GetTenantsExceptDefault(); | ||
var log = new StringBuilder(); | ||
|
||
await _helperService.ExecuteForTenants(tenants, async (settings, scope) => { | ||
var deploymentManager = scope.ServiceProvider.GetRequiredService<IDeploymentManager>(); | ||
try | ||
{ | ||
await deploymentManager.ImportDeploymentPackageAsync(new PhysicalFileProvider(tempArchiveFolder)); | ||
} | ||
catch(Exception ex) | ||
{ | ||
log.AppendLine(S["An exception occurred while executing the Json for tenant {0}: {1}", settings.Name, ex.Message]); | ||
} | ||
}); | ||
if(log.Length > 0) | ||
{ | ||
ModelState.AddModelError("summary", log.ToString()); | ||
} | ||
else | ||
{ | ||
_notifier.Success(H["Recipe successfully executed for selected tenants"]); | ||
} | ||
} | ||
finally | ||
{ | ||
if (Directory.Exists(tempArchiveFolder)) | ||
{ | ||
Directory.Delete(tempArchiveFolder, true); | ||
} | ||
} | ||
} | ||
model.AllTenants = _helperService.GetTenantsExceptDefault().Select(t=> new SelectListItem(t.Name, t.Name, model.SelectedTenantNames.Contains(t.Name))).ToList(); | ||
|
||
return View(model); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ActivateShell = false