Skip to content

Improve dynamic permission definition aggregation to support module extensions #25665

Description

@colinin

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

When implementing dynamic permissions, settings, or features that extend existing modules, I noticed that the current implementation of IPermissionDefinitionManager.GetGroupsAsync (and similar managers for settings and features) filters out definitions that share the same name as statically defined ones.

Specifically, when a user extends a module without using plugins or direct references, and adds custom permissions, these custom definitions are silently discarded during the GetGroupsAsync call because the system assumes that any definition with a duplicate name is a duplicate entry and should be skipped. This makes it impossible to extend built-in modules with additional permission, setting, or feature definitions in a clean way when the extension is not integrated through standard module referencing mechanisms.

This behavior is not documented and creates a hidden limitation for modular extension scenarios.

Describe the solution you'd like

I propose that the ABP framework should support aggregating dynamic and static definitions for permissions, settings, and features.

Expected behavior:

When GetGroupsAsync is called, it should return a combined set of definitions from both static registration and dynamic providers.

If the dynamic definition has the same name as the static definition, the framework should merge or override it based on the dynamic option strategy (for example, the dynamic option takes precedence over the static one, or an exception is thrown and clear guidance is provided).

This should be consistent across PermissionDefinitionManager, SettingDefinitionManager, and FeatureDefinitionManager.

Additional context

public virtual async Task<IReadOnlyList<FeatureGroupDefinition>> GetGroupsAsync()
{
var staticGroups = await StaticStore.GetGroupsAsync();
var staticGroupNames = staticGroups
.Select(p => p.Name)
.ToImmutableHashSet();
var dynamicGroups = await DynamicStore.GetGroupsAsync();
/* We prefer static groups over dynamics */
return staticGroups.Concat(
dynamicGroups.Where(d => !staticGroupNames.Contains(d.Name))
).ToImmutableList();
}

Example of Combined Permissions:

public virtual async Task<IReadOnlyList<PermissionGroupDefinition>> GetGroupsAsync()
{
    var staticGroups = await _staticStore.GetGroupsAsync();
    var dynamicGroups = await _dynamicStore.GetGroupsAsync();

    var mergedGroups = new Dictionary<string, PermissionGroupDefinition>();

    foreach (var staticGroup in staticGroups)
    {
        mergedGroups[staticGroup.Name] = staticGroup;
    }

    foreach (var dynamicGroup in dynamicGroups)
    {
        if (mergedGroups.TryGetValue(dynamicGroup.Name, out var existingGroup))
        {
            MergeGroupPermissions(existingGroup, dynamicGroup);
        }
        else
        {
            mergedGroups[dynamicGroup.Name] = dynamicGroup;
        }
    }

    return mergedGroups.Values.ToImmutableList();
}

// Implement MergeGroupPermissions

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions