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

Show an error message if permission names are not unique #17385

Merged
merged 12 commits into from
Jan 27, 2025
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ nav:
- Mini Profiler: reference/modules/MiniProfiler/README.md
- Modules: reference/modules/Modules/README.md
- OpenId: reference/modules/OpenId/README.md
- Permissions: reference/modules/Security/Permissions.md
- Razor Helpers: reference/modules/Razor/README.md
- Recipes: reference/modules/Recipes/README.md
- Redis: reference/modules/Redis/README.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,17 @@ public async Task<IActionResult> Edit(string id)
IsAdminRole = await _roleService.IsAdminRoleAsync(role.RoleName),
};

var installedPermissions = await GetInstalledPermissionsAsync();
var allPermissions = installedPermissions.SelectMany(x => x.Value);

ViewData["DuplicatedPermissions"] = allPermissions
.GroupBy(p => p.Name.ToUpperInvariant())
.Where(g => g.Count() > 1)
.Select(g => g.First().Name)
.ToArray();
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved

if (!await _roleService.IsAdminRoleAsync(role.RoleName))
{
var installedPermissions = await GetInstalledPermissionsAsync();
var allPermissions = installedPermissions.SelectMany(x => x.Value);

model.EffectivePermissions = await GetEffectivePermissions(role, allPermissions);
model.RoleCategoryPermissions = installedPermissions;
}
Expand Down
14 changes: 14 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Roles/Views/Admin/Edit.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@

<zone Name="Title"><h1>@RenderTitleSegments(T["Edit '{0}' Role", Model.Name])</h1></zone>

@if (ViewData["DuplicatedPermissions"] is string[] duplicatedPermissions && duplicatedPermissions.Length > 0)
{
<div class="alert alert-danger" role="alert">
<strong>@T["Duplicate Permissions Detected"]</strong>
<p>@T["The following permissions are ambiguous and must be uniquely named across all modules:"]</p>
<ul>
@foreach (var permission in duplicatedPermissions)
{
<li>@permission</li>
}
</ul>
</div>
}

<form asp-action="Edit" method="post" class="no-multisubmit">

<div class="mb-3" asp-validation-class-for="RoleDescription">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async ValueTask<IEnumerable<Permission>> GetPermissionsAsync()

private async Task LoadPermissionsAsync()
{
_permissions = [];
_permissions = new Dictionary<string, Permission>(StringComparer.OrdinalIgnoreCase);

foreach (var permissionProvider in _permissionProviders)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,94 @@

namespace OrchardCore.Security.Permissions;

/// <summary>
/// Represents a permission within the system.
/// </summary>
/// <remarks>
/// The permission name must be unique across the system.
/// </remarks>
public class Permission
{
public const string ClaimType = "Permission";

/// <summary>
/// Initializes a new instance of the <see cref="Permission"/> class.
/// </summary>
/// <param name="name">The name of the permission.</param>
/// <exception cref="ArgumentNullException">Thrown when the name is null.</exception>
/// <remarks>
/// The permission name must be unique across the system.
/// </remarks>
public Permission(string name)
{
ArgumentNullException.ThrowIfNull(name);

Name = name;
}

/// <summary>
/// Initializes a new instance of the <see cref="Permission"/> class with a description and security flag.
/// </summary>
/// <param name="name">The name of the permission.</param>
/// <param name="description">The description of the permission.</param>
/// <param name="isSecurityCritical">Indicates whether the permission is security critical.</param>
/// <remarks>
/// The permission name must be unique across the system.
/// </remarks>
public Permission(string name, string description, bool isSecurityCritical = false) : this(name)
{
Description = description;
IsSecurityCritical = isSecurityCritical;
}

/// <summary>
/// Initializes a new instance of the <see cref="Permission"/> class with a description, implying permissions, and security flag.
/// </summary>
/// <param name="name">The name of the permission.</param>
/// <param name="description">The description of the permission.</param>
/// <param name="impliedBy">The permissions implying this permission.</param>
/// <param name="isSecurityCritical">Indicates whether the permission is security critical.</param>
/// <remarks>
/// The permission name must be unique across the system.
/// </remarks>
public Permission(string name, string description, IEnumerable<Permission> impliedBy, bool isSecurityCritical = false) : this(name, description, isSecurityCritical)
{
ImpliedBy = impliedBy;
}

/// <summary>
/// Gets the name of the permission.
/// </summary>
/// <remarks>
/// The permission name must be unique across the system.
/// </remarks>
public string Name { get; }

/// <summary>
/// Gets or sets the description of the permission.
/// </summary>
public string Description { get; set; }

/// <summary>
/// Gets or sets the category of the permission.
/// </summary>
public string Category { get; set; }

/// <summary>
/// Gets the permissions implying this permission.
/// </summary>
public IEnumerable<Permission> ImpliedBy { get; }

/// <summary>
/// Gets a value indicating whether the permission is security critical.
/// </summary>
public bool IsSecurityCritical { get; }

/// <summary>
/// Converts a <see cref="Permission"/> to a <see cref="Claim"/>.
/// </summary>
/// <param name="p">The permission to convert.</param>
/// <returns>A claim representing the permission.</returns>
public static implicit operator Claim(Permission p)
{
return new Claim(ClaimType, p.Name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ public static class PermissionsServiceCollectionExtensions
public static IServiceCollection AddPermissionProvider<TProvider>(this IServiceCollection services)
where TProvider : class, IPermissionProvider
{
return services.AddScoped<IPermissionProvider, TProvider>();
if (!services.Any(s => s.ImplementationType == typeof(TProvider)))
{
services.AddScoped<IPermissionProvider, TProvider>();
}

return services;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using OrchardCore.Security;
using OrchardCore.Security.AuthorizationHandlers;
using OrchardCore.Security.Permissions;
Expand Down Expand Up @@ -30,6 +33,31 @@ public static OrchardCoreBuilder AddSecurity(this OrchardCoreBuilder builder)
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
});

builder.Configure(ValidatePermissionsAsync);

return builder;
}

private static async ValueTask ValidatePermissionsAsync(IApplicationBuilder builder, IEndpointRouteBuilder routeBuilder, IServiceProvider serviceProvider)
{
// Make sure registered permissions are valid, i.e. they must be unique.
var permissionProviders = serviceProvider.GetServices<IPermissionProvider>();
MikeAlhayek marked this conversation as resolved.
Show resolved Hide resolved
var permissionNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
ILogger logger = null;

foreach (var permissionProvider in permissionProviders)
{
var permissions = await permissionProvider.GetPermissionsAsync();

foreach (var permission in permissions)
{
if(!permissionNames.Add(permission.Name))
{
logger ??= serviceProvider.GetRequiredService<ILogger<IPermissionProvider>>();

logger.LogError("The permission '{PermissionName}' created by the permission provider '{PermissionProvider}' is already registered. Each permission must have a unique name across all modules.", permission.Name, permissionProvider.GetType());
}
}
}
}
}
149 changes: 149 additions & 0 deletions src/docs/reference/modules/Security/Permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Orchard Core Permissions

Orchard Core provides a comprehensive set of permissions to manage various aspects of the system. Below is a list of permissions along with their descriptions. Note that security critical permissions allow users to elevate their permissions and should be assigned with caution.


| Permission Group | Permission | Description |
|------------------|------------|-------------|
| Admin (OrchardCore.Admin) | AccessAdminPanel | Access admin panel |
| | ManageAdminSettings | Manage Admin Settings |
| Admin Menu (OrchardCore.AdminMenu) | ManageAdminMenu | Manage the admin menu |
| | ViewAdminMenu_[MenuName] | View Admin Menu - [MenuName] |
| | ViewAdminMenuAll | View Admin Menu - View All |
| Amazon Media Storage (OrchardCore.Media.AmazonS3) | ViewAmazonS3MediaOptions | View Amazon S3 Media Options |
| Audit Trail (OrchardCore.AuditTrail) | ManageAuditTrailSettings | Manage audit trail settings |
| | ViewAuditTrail | View audit trail |
| Autoroute (OrchardCore.Autoroute) | SetHomepage | Set homepage |
| Azure Media ImageSharp Image Cache (OrchardCore.Media.Azure.ImageSharpImageCache) | ViewAzureMediaOptions | View Azure Media Options |
| Background Tasks (OrchardCore.BackgroundTasks) | ManageBackgroundTasks | Manage background tasks |
| Permissions for each Content Type | Clone_[ContentType] | Clone [ContentType] by others |
| | CloneOwn_[ContentType] | Clone own [ContentType] |
| | DeleteOwn_[ContentType] | Delete [ContentType] |
| | Delete_[ContentType] | Delete [ContentType] for others |
| | EditOwn_[ContentType] | Edit [ContentType] |
| | Edit_[ContentType] | Edit [ContentType] for others |
| | EditContentOwner_[ContentType] | Edit the owner of a [ContentType] content item |
| | ListContent_[ContentType] | List [ContentType] content items |
| | Preview_[ContentType] | Preview [ContentType] by others |
| | PreviewOwn_[ContentType] | Preview own [ContentType] |
| | PublishOwn_[ContentType] | Publish or unpublish [ContentType] |
| | Publish_[ContentType] | Publish or unpublish [ContentType] for others |
| | View_[ContentType] | View [ContentType] by others |
| | ViewOwn_[ContentType] | View own [ContentType] |
| Content Localization (OrchardCore.ContentLocalization) | LocalizeContent | Localize content for others |
| | LocalizeOwnContent | Localize own content |
| | ManageContentCulturePicker | Manage ContentCulturePicker settings |
| Content Types (OrchardCore.ContentTypes) | EditContentTypes (security critical) | Edit content types |
| | ViewContentTypes | View content types |
| Contents (OrchardCore.Contents) | AccessContentApi | Access content via the api |
| | ApiViewContent | Access view content endpoints |
| | CloneContent | Clone content |
| | CloneOwnContent | Clone own content |
| | DeleteContent | Delete content for others |
| | DeleteOwnContent | Delete own content |
| | EditContent | Edit content for others |
| | EditOwnContent | Edit own content |
| | EditContentOwner | Edit the owner of a content item |
| | ListContent | List content items |
| | PreviewContent | Preview content |
| | PreviewOwnContent | Preview own content |
| | PublishContent | Publish or unpublish content for others |
| | PublishOwnContent | Publish or unpublish own content |
| | ViewContent | View all content |
| | ViewOwnContent | View own content |
| Custom Settings (OrchardCore.CustomSettings) | ManageCustomSettings_[CustomSettingsType] | Manage Custom Settings |
| Deployment (OrchardCore.Deployment) | Export | Export Data |
| | Import (security critical) | Import Data |
| | ManageDeploymentPlan | Manage deployment plans |
| Elasticsearch (OrchardCore.Search.Elasticsearch) | ManageElasticIndexes | Manage Elasticsearch Indexes |
| | QueryElasticsearchApi | Query Elasticsearch Api |
| Email (OrchardCore.Email) | ManageEmailSettings | Manage Email Settings |
| Features (OrchardCore.Features) | ManageFeatures | Manage Features |
| GitHub Authentication (OrchardCore.GitHub.Authentication) | ManageGitHubAuthentication | Manage GitHub Authentication settings |
| Google Tag Manager (OrchardCore.Google.TagManager) | ManageGoogleAnalytics | Manage Google Analytics settings |
| | ManageGoogleAuthentication | Manage Google Authentication settings |
| | ManageGoogleTagManager | Manage Google Tag Manager settings |
| GraphQL (OrchardCore.Apis.GraphQL) | ExecuteGraphQLMutations | Execute GraphQL Mutations |
| | ExecuteGraphQL | Execute GraphQL |
| HTTPS (OrchardCore.Https) | ManageHttps | Manage HTTPS |
| Layers (OrchardCore.Layers) | ManageLayers | Manage layers |
| Localization (OrchardCore.Localization) | ManageCultures | Manage supported culture |
| Media (OrchardCore.Media) | ManageMediaFolder | Manage All Media Folders |
| | ManageAttachedMediaFieldsFolder | Manage Attached Media Fields Folder |
| | ManageMediaContent | Manage Media |
| | ManageOthersMediaContent | Manage Media For Others |
| | ManageMediaProfiles | Manage Media Profiles |
| | ManageOwnMediaContent | Manage Own Media |
| | ViewMediaOptions | View Media Options |
| Media Cache (OrchardCore.Media.Cache) | ManageAssetCache | Manage Asset Cache Folder |
| Menu (OrchardCore.Menu) | ManageMenu | Manage menus |
| Meta Core Components (OrchardCore.Facebook) | ManageFacebookApp | View and edit the Facebook app |
| Meta Pixel (OrchardCore.Facebook.Pixel) | ManageFacebookPixel | Manage Facebook Pixel settings |
| Microsoft Entra ID (Azure Active Directory) Authentication (OrchardCore.Microsoft.Authentication.AzureAD) | ManageMicrosoftAuthentication | Manage Microsoft Authentication settings |
| Notifications (OrchardCore.Notifications) | ManageNotifications | Manage notifications |
| OpenID Connect Core Services (OrchardCore.OpenId) | ManageClientSettings | View and edit the OpenID Connect client settings |
| | ManageServerSettings | View and edit the OpenID Connect server settings |
| | ManageValidationSettings | View and edit the OpenID Connect validation settings |
| | ManageApplications | View, add, edit and remove the OpenID Connect applications |
| | ManageScopes | View, add, edit and remove the OpenID Connect scopes |
| Placements (OrchardCore.Placements) | ManagePlacements | Manage placements |
| Queries (OrchardCore.Queries) | ExecuteApiAll | Execute Api - All queries |
| | ExecuteApi_RecentBlogPosts | Execute Api - RecentBlogPosts |
| | ManageQueries | Manage queries |
| ReCaptcha (OrchardCore.ReCaptcha) | ManageReCaptchaSettings | Manage ReCaptcha Settings |
| Recipes (OrchardCore.Recipes) | ManageRecipes (security critical) | Manage Recipes |
| Remote Deployment (OrchardCore.Deployment.Remote) | ExportRemoteInstances | Export to remote instances |
| | ManageRemoteClients | Manage remote clients |
| | ManageRemoteInstances | Manage remote instances |
| Reverse Proxy Configuration (OrchardCore.ReverseProxy) | ManageReverseProxySettings | Manage Reverse Proxy Settings |
| Roles (OrchardCore.Roles) | ManageRoles (security critical) | Managing Roles |
| Search (OrchardCore.Search) | ManageSearchSettings | Manage Search Settings |
| | QuerySearchIndex | Query any index |
| Secure Media (OrchardCore.Media.Security) | ViewMediaContent | View media content in all folders |
| | ViewRootMediaContent | View media content in the root folder |
| | ViewOthersMediaContent | View others media content |
| | ViewOwnMediaContent | View own media content |
| | ViewMediaContent_[FolderName] | View media content in folder '[FolderName]' |
| Security (OrchardCore.Security) | ManageSecurityHeadersSettings | Manage Security Headers Settings |
| SEO (OrchardCore.Seo) | ManageSeoSettings | Manage SEO related settings |
| Settings (OrchardCore.Settings) | ManageSettings | Manage settings |
| Shortcode Templates (OrchardCore.Shortcodes.Templates) | ManageShortcodeTemplates (security critical) | Manage shortcode templates |
| Sitemaps (OrchardCore.Sitemaps) | ManageSitemaps | Manage sitemaps |
| SMS (OrchardCore.Sms) | ManageSmsSettings | Manage SMS Settings |
| SQL Queries (OrchardCore.Queries.Sql) | ManageSqlQueries | Manage SQL Queries |
| Taxonomies (OrchardCore.Taxonomies) | ManageTaxonomy | Manage taxonomies |
| Templates (OrchardCore.Templates) | ManageAdminTemplates (security critical) | Manage admin templates |
| | ManageTemplates (security critical) | Manage templates |
| Themes (OrchardCore.Themes) | ApplyTheme | Apply a Theme |
| Two-Factor Authentication Services (OrchardCore.Users.2FA) | DisableTwoFactorAuthenticationForUsers (security critical) | Disable two-factor authentication for any user |
| URL Rewriting (OrchardCore.UrlRewriting) | ManageUrlRewritingRules | Manage URLs rewriting rules |
| Users (OrchardCore.Users) | AssignRoleToUsers (security critical) | Assign any role to users |
| | AssignRoleToUsers_[RoleName] (security critical) | Assign [RoleName] role to users |
| | DeleteUsers (security critical) | Delete any user |
| | DeleteUsersInRole_[RoleName] (security critical) | Delete users in [RoleName] role |
| | EditUsers (security critical) | Edit any user |
| | ManageOwnUserInformation | Edit own user information |
| | EditUsersInRole_[RoleName] (security critical) | Edit users in [RoleName] role |
| | ListUsers | List all users |
| | ListUsersInRole_[RoleName] | List users in [RoleName] role |
| | ManageUsers (security critical) | Manage security settings and all users |
| | ManageUsersInRole_[RoleName] (security critical) | Manage users in [RoleName] role |
| | View Users | View user profiles |
| Workflows (OrchardCore.Workflows) | ExecuteWorkflows (security critical) | Execute workflows |
| | ManageWorkflowSettings | Manage workflow settings |
| | ManageWorkflows (security critical) | Manage workflows |
| X (Twitter) Integration (OrchardCore.Twitter) | ManageTwitterSignin | Manage Sign in with X (Twitter) settings |
| | ManageTwitter | Manage X (Twitter) settings |

### Writing New Permissions

When creating new permissions in Orchard Core, it is crucial to ensure that each permission name is unique across the system. This helps maintain clarity and prevents conflicts that could arise from duplicate permission names.

#### Steps to Create a New Permission:
1. **Define the Permission**: Clearly define the purpose and scope of the new permission.
2. **Check for Uniqueness**: Before finalizing the permission name, check it against the list of existing permissions to ensure it is unique. This can be done by reviewing the current permissions documentation or querying the system.
3. **Implement the Permission**: Once the name is confirmed to be unique, proceed with implementing the permission in the codebase.

**Note**: Always document new permissions thoroughly, including their descriptions and any security implications, especially if they are security critical permissions that allow users to elevate their permissions.

By following these steps, you can help maintain a well-organized and secure permissions system in Orchard Core.
2 changes: 1 addition & 1 deletion src/docs/topics/security/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ Orchard Core provides many security features to give an authenticated access to
- [OpenId](../../reference/modules/OpenId/README.md)
- [Roles](../../reference/modules/Roles/README.md)
- [DataProtection (Azure Storage)](../../reference/modules/DataProtection.Azure/README.md)
- [Permissions](../../reference/modules/Security/Permissions.md)
- TBD Users
- TBD Permissions
- TBD Login, Registration