Skip to content
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

SNOW-20: Added StrictSecurityPart #55

Merged
merged 8 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions Extensions/Security/Driver/StrictSecuritySettingsDisplayDriver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Lombiq.HelpfulExtensions.Extensions.Security.Models;
using Lombiq.HelpfulExtensions.Extensions.Security.ViewModels;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentManagement.Metadata.Settings;
using OrchardCore.ContentTypes.Editors;
using OrchardCore.DisplayManagement.Views;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.Security.Driver
{
public class StrictSecuritySettingsDisplayDriver : ContentTypeDefinitionDisplayDriver
{
public override IDisplayResult Edit(ContentTypeDefinition model) =>
Initialize<StrictSecuritySettingsViewModel>("StrictSecuritySetting_Edit", viewModel =>
{
var settings = model.GetSettings<StrictSecuritySettings>();

viewModel.Enabled = settings?.Enabled == true;
}).Location("Content:5");

public override async Task<IDisplayResult> UpdateAsync(ContentTypeDefinition model, UpdateTypeEditorContext context)
{
var viewModel = new StrictSecuritySettingsViewModel();

if (await context.Updater.TryUpdateModelAsync(viewModel, Prefix))
{
// Securable must be enabled for Strict Securable to make sense. Also checked on the client side too.
if (model.GetSettings<ContentTypeSettings>()?.Securable != true) viewModel.Enabled = false;

context.Builder.MergeSettings<StrictSecuritySettings>(settings => settings.Enabled = viewModel.Enabled);
}

return Edit(model);
}
}
}
7 changes: 7 additions & 0 deletions Extensions/Security/Models/StrictSecuritySettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.Security.Models
{
public class StrictSecuritySettings
{
public bool Enabled { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Lombiq.HelpfulExtensions.Extensions.Security.Models;
using Microsoft.AspNetCore.Authorization;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.Contents.Security;
using OrchardCore.Modules;
using OrchardCore.Security;
using OrchardCore.Security.Permissions;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.Security.Services
{
[RequireFeatures(FeatureIds.Security)]
public class StrictSecurityPermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private static readonly Dictionary<string, IList<string>> _permissionTemplates = ContentTypePermissionsHelper
.PermissionTemplates
.ToDictionary(
pair => pair.Key,
pair => GetPermissionTemplates(pair.Value, new List<string>()));

private readonly IContentDefinitionManager _contentDefinitionManager;

public StrictSecurityPermissionAuthorizationHandler(IContentDefinitionManager contentDefinitionManager) =>
_contentDefinitionManager = contentDefinitionManager;

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
if ((context.Resource as IContent)?.ContentItem is not { } contentItem ||
!_permissionTemplates.TryGetValue(requirement.Permission.Name, out var claims) ||
_contentDefinitionManager.GetTypeDefinition(contentItem.ContentType) is not { } definition ||
definition.GetSettings<StrictSecuritySettings>()?.Enabled != true)
{
return Task.CompletedTask;
}

if (!context.User.Identity.IsAuthenticated)
{
context.Fail();
return Task.CompletedTask;
}

var contentType = contentItem.ContentType;
claims = claims
.Select(template => string.Format(CultureInfo.InvariantCulture, template, contentType))
.ToList();
var permissionNames = context
.User
.Claims
.Where(claim => claim.Type == nameof(Permission))
.Select(claim => claim.Value);

if (!permissionNames.Any(claims.Contains)) context.Fail();
return Task.CompletedTask;
}

private static IList<string> GetPermissionTemplates(Permission permission, IList<string> templates)
{
templates.Add(permission.Name);

if (permission.ImpliedBy is { } impliedBy)
{
foreach (var impliedPermission in impliedBy)
{
if (impliedPermission.Name.Contains("{0}"))
{
GetPermissionTemplates(impliedPermission, templates);
}
}
}

return templates;
}
}
}
21 changes: 21 additions & 0 deletions Extensions/Security/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Lombiq.HelpfulExtensions.Extensions.Security.Driver;
using Lombiq.HelpfulExtensions.Extensions.Security.Services;
using Lombiq.HelpfulLibraries.Libraries.DependencyInjection;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ContentTypes.Editors;
using OrchardCore.Modules;

namespace Lombiq.HelpfulExtensions.Extensions.Security
{
[Feature(FeatureIds.Security)]
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
services.AddLazyInjectionSupport();
services.AddScoped<IAuthorizationHandler, StrictSecurityPermissionAuthorizationHandler>();
services.AddScoped<IContentTypeDefinitionDisplayDriver, StrictSecuritySettingsDisplayDriver>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.Security.ViewModels
{
public class StrictSecuritySettingsViewModel
{
public bool Enabled { get; set; }
}
}
1 change: 1 addition & 0 deletions FeatureIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public static class FeatureIds
public const string Flows = FeatureIdPrefix + nameof(Flows);
public const string ShapeTracing = FeatureIdPrefix + nameof(ShapeTracing);
public const string Widgets = FeatureIdPrefix + nameof(Widgets);
public const string Security = FeatureIdPrefix + nameof(Security);
}
}
7 changes: 7 additions & 0 deletions Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@
Category = "Development",
Description = "Adds a dump of metadata to the output about every shape."
)]

[assembly: Feature(
Id = Security,
Name = "Lombiq Helpful Extensions - Security Helpful Extensions",
Category = "Security",
Description = "Adds a content type definition setting and authorization handler for richer security options."
)]
17 changes: 17 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ It displays an [accordion powered by Bootstrap](https://getbootstrap.com/docs/4.
prop-Children="IEnumerable<BootstrapAccordionItem>"></shape>
```

### Security Extensions


#### Strict Security

When applied to a content type definition, `StrictSecuritySetting` requires the user to have the exact Securable permission for that content type. For example if you apply it to Page, then just having the common ViewContent permission won't be enough and you must explicitly have the View_Page permission too. Don't worry, the normal implications such as ViewOwn beig fulfilled by View still apply within the content type, they just no longer imply their common counterparts.

Make content type use strict security in migration:
```csharp
_contentDefinitionManager.AlterTypeDefinition("Page", type => type
.Securable()
.WithSettings(new StrictSecuritySettings { Enabled = true }));
```

You can also enable it by going to the content type editor on the admin side and checking the _Strict Securable_ checkbox.


## Contributing and support

Bug reports, feature requests, comments, questions, code contributions, and love letters are warmly welcome, please do so via GitHub issues and pull requests. Please adhere to our [open-source guidelines](https://lombiq.com/open-source-guidelines) while doing so.
Expand Down
23 changes: 23 additions & 0 deletions Views/StrictSecuritySetting.Edit.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@model Lombiq.HelpfulExtensions.Extensions.Security.ViewModels.StrictSecuritySettingsViewModel

@* Template is to be kept identically formatted as ContentTypeSettings.Edit.cshtml. *@
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" asp-for="Enabled">
<label class="custom-control-label" asp-for="Enabled">@T["Strict Securable"]</label>
<span class="hint dashed">
@T["Enable along with Securable to ensure the user must have the content type version of the permission."]
</span>
</div>
</div>

<script at="Foot" depends-on="jQuery">
jQuery(function ($) {
$('#ContentTypeDefinition_Securable').change(function () {
var strict = document.getElementById(@Html.IdFor(model => model.Enabled).JsonHtmlContent());

strict.disabled = !this.checked;
if (!this.checked) strict.checked = false;
});
});
</script>