Permissions Reference¶
This page is the complete permission catalogue for the solution: every permission constant, its
exact string value, the PermissionDefinitionProvider
that registers it, and the role(s) that receive it by default at seed time.
For the concepts — how [GrantInRoles] works, how the role hierarchy rolls grants up, how to
define a new permission, and how to enforce one with [Authorize] / IAuthorizationService — see
Authorization. This page is the lookup table; that page is the
how-to.
Where things live
Permission definitions (the constants and the providers) live in the *.Application.Contracts
projects. The grant logic that maps permissions onto roles lives in src/Cargonerds.Domain
(RolesDataSeedContributor).
Enforcement lives in the *.Application services. Adding a permission constant is not
enough for a role to receive it — see the seeding section.
Permission groups¶
Permissions are defined as const string values in *Permissions classes and registered with ABP
in the matching *PermissionDefinitionProvider. There are exactly three project-owned groups,
one per layer/module:
| Group | Constants class | Provider | Project |
|---|---|---|---|
Cargonerds |
CargonerdsPermissions |
CargonerdsPermissionDefinitionProvider |
src/Cargonerds.Application.Contracts/Permissions/ |
Hub |
HubPermissions |
HubPermissionDefinitionProvider |
modules/hub/src/Hub.Application.Contracts/Permissions/ |
Pricing |
PricingPermissions |
PricingPermissionDefinitionProvider |
modules/pricing/src/Pricing.Application.Contracts/Permissions/ |
The group name itself is a constant on each class (GroupName): "Cargonerds", "Hub",
"Pricing". The Cargonerds provider additionally extends the built-in ABP Identity and
CMS Kit groups (see Cross-module additions) — those permissions live
under ABP's groups in the management UI, not under Cargonerds.
flowchart LR
subgraph Contracts["*.Application.Contracts (definition)"]
HP["HubPermissions<br/>+ [GrantInRoles]"]
CP["CargonerdsPermissions<br/>+ [GrantInRoles]"]
PP["PricingPermissions<br/>+ [GrantInRoles]"]
HPP[HubPermissionDefinitionProvider]
CPP[CargonerdsPermissionDefinitionProvider]
PPP[PricingPermissionDefinitionProvider]
HP --> HPP
CP --> CPP
PP --> PPP
end
subgraph Domain["Cargonerds.Domain (seeding)"]
RDS["RolesDataSeedContributor<br/>reflects over all 3 classes"]
end
HPP & CPP & PPP -->|register tree → mgmt UI| UI[(Permission tree<br/>per group)]
HP & CP & PP -->|read by reflection at seed time| RDS
RDS -->|SetForRoleAsync| Grants[(Role → permission<br/>grants)]
Tree shape and default grants are independent
The parent/child structure built by a provider (AddPermission / AddChild) only drives the
permission-management UI — checking a parent suggests its children. It does not grant
anything. Whether a role actually receives a permission is decided solely by [GrantInRoles] +
RolesDataSeedContributor.
The two can legitimately disagree — e.g. Hub.Shipment.Create is a child of Shipment.View
in the tree but carries no [GrantInRoles], so seeding grants it to no role.
The role hierarchy¶
Roles and their order (lowest → highest privilege) are defined in RoleConsts
(modules/hub/src/Hub.Domain.Shared/Consts/RoleConsts.cs):
public static class RoleConsts
{
public const string Admin = "admin"; // built-in ABP admin role
public const string DefaultCustomer = nameof(DefaultCustomer);
public const string Operator = nameof(Operator);
public const string AccountOwner = nameof(AccountOwner);
public const string LocalRealtimeAdmin = nameof(LocalRealtimeAdmin);
public const string SuperUser = nameof(SuperUser);
public static readonly string[] Hierarchy =
[
DefaultCustomer, Operator, AccountOwner, LocalRealtimeAdmin, SuperUser,
];
}
DefaultCustomer is marked as the default role at seed time (new users get it automatically).
The built-in ABP admin role exists but is not part of Hierarchy — it is not created by
RolesDataSeedContributor and is excluded from the inheritance roll-up.
How default roles are resolved¶
[GrantInRoles] is a custom attribute
(Hub.Attributes.GrantInRolesAttribute, in modules/hub/src/Hub.Domain.Shared/Attributes/).
By default (Inherit = true) a permission granted to a role is also granted to every higher role
in the hierarchy; Inherit = false pins it to exactly the listed role(s). The roll-up lives in
RolesDataSeedContributor.ResolveRoles:
private static string[] ResolveRoles(string[] roles, bool inherit)
{
if (!inherit) return roles;
var result = new HashSet<string>();
foreach (var role in roles)
{
var index = Array.IndexOf(RoleConsts.Hierarchy, role);
if (index < 0) { result.Add(role); continue; } // role outside hierarchy → as-is
for (var i = index; i < RoleConsts.Hierarchy.Length; i++)
result.Add(RoleConsts.Hierarchy[i]); // this role and every higher one
}
return result.ToArray();
}
Throughout the tables below, the "Annotated role" column shows the role written in the attribute; the "Effective roles" column shows the full set after the upward roll-up. With the hierarchy above, the resolution is always one of:
[GrantInRoles(...)] annotation |
Effective roles (after roll-up) |
|---|---|
DefaultCustomer |
DefaultCustomer, Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Operator |
Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
AccountOwner |
AccountOwner, LocalRealtimeAdmin, SuperUser |
LocalRealtimeAdmin |
LocalRealtimeAdmin, SuperUser |
SuperUser |
SuperUser |
DefaultCustomer, Inherit = false |
DefaultCustomer (only) |
(no [GrantInRoles]) |
none — granted to no role by seeding |
Grants apply only at seed time
Adding, removing, or changing a [GrantInRoles] attribute has no effect until the seeder
runs again — re-run the DbMigrator. A constant with no [GrantInRoles] is granted to no role
by seeding (it can still be granted manually via the permission-management UI). Because
inheritance is upward and on by default, annotating a permission for a low role silently grants
it to every higher role too.
Hub group¶
Source: modules/hub/src/Hub.Application.Contracts/Permissions/HubPermissions.cs +
HubPermissionDefinitionProvider.cs. Group name constant HubPermissions.GroupName = "Hub".
Group-header constants are themselves granted
Several area "group" constants (Shipment.ShipmentGroup, Orders.OrdersGroup,
Document.DocumentGroup, Invoice.InvoiceGroup, Organization.OrganizationGroup,
Insights.InsightsGroup, Reports.ReportsGroup, CodeEntities.Default, Notification.Default)
carry their own [GrantInRoles]. They are listed below alongside the actions. Note that the
provider registers Shipment/Orders/Document/Invoice/Organization using the .View
constant as the parent node, while Insights/Reports use the *Group constant as the parent.
| Permission constant | Value | Annotated role | Effective roles |
|---|---|---|---|
Shipment.ShipmentGroup |
Hub.Shipment |
DefaultCustomer | all roles |
Shipment.View |
Hub.Shipment.View |
DefaultCustomer | all roles |
Shipment.Create |
Hub.Shipment.Create |
— | none (seed) |
Shipment.Edit |
Hub.Shipment.Edit |
— | none (seed) |
Shipment.Delete |
Hub.Shipment.Delete |
— | none (seed) |
Orders.OrdersGroup |
Hub.Orders |
DefaultCustomer | all roles |
Orders.View |
Hub.Orders.View |
DefaultCustomer | all roles |
Orders.Create |
Hub.Orders.Create |
— | none (seed) |
Orders.Edit |
Hub.Orders.Edit |
— | none (seed) |
Orders.Delete |
Hub.Orders.Delete |
— | none (seed) |
Document.DocumentGroup |
Hub.Document |
DefaultCustomer | all roles |
Document.View |
Hub.Document.View |
DefaultCustomer | all roles |
Document.Create |
Hub.Document.Create |
— | none (seed) |
Document.Delete |
Hub.Document.Delete |
— | none (seed) |
Invoice.InvoiceGroup |
Hub.Invoice |
DefaultCustomer | all roles |
Invoice.View |
Hub.Invoice.View |
DefaultCustomer | all roles |
Invoice.Create |
Hub.Invoice.Create |
— | none (seed) |
Invoice.Delete |
Hub.Invoice.Delete |
— | none (seed) |
Organization.OrganizationGroup |
Hub.Organization |
DefaultCustomer | all roles |
Organization.View |
Hub.Organization.View |
DefaultCustomer | all roles |
Insights.InsightsGroup |
Hub.Insights |
SuperUser | SuperUser |
Insights.View |
Hub.Insights.View |
SuperUser | SuperUser |
Insights.ViewFinancial |
Hub.Insights.ViewFinancial |
SuperUser | SuperUser |
Insights.MyInsightsView |
Hub.Insights.MyInsights.View |
SuperUser | SuperUser |
Insights.MyInsightsEdit |
Hub.Insights.MyInsights.Edit |
SuperUser | SuperUser |
Reports.ReportsGroup |
Hub.Reports |
SuperUser | SuperUser |
Reports.View |
Hub.Reports.View |
SuperUser | SuperUser |
Reports.Create |
Hub.Reports.Create |
SuperUser | SuperUser |
RealtimeAdmin.Default |
Hub.RealtimeAdmin |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
InternalAdmin.Default |
Hub.InternalAdmin |
— | none (seed) |
Notification.Default |
Hub.Notification |
DefaultCustomer | all roles |
Notification.Internal |
Hub.Notification.Internal |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
CodeEntities.Default |
Hub.CodeEntities |
DefaultCustomer | all roles |
CodeEntities.View |
Hub.CodeEntities.View |
DefaultCustomer | all roles |
CodeEntities.Create |
Hub.CodeEntities.Create |
SuperUser | SuperUser |
CodeEntities.Edit |
Hub.CodeEntities.Edit |
SuperUser | SuperUser |
Hub.RealtimeAdmin is duplicated in the frontend
RealtimeAdmin.Default gates the admin page in the Next.js frontend. A source comment warns
that "the value needs to be updated in frontend manually" — the literal string is referenced in
the frontend and is not imported from the backend constant. Renaming it requires a matching
frontend change. See Realtime Frontend.
Hub.InternalAdmin is surfaced under the Cargonerds group
InternalAdmin.Default is declared in HubPermissions but registered into the Cargonerds
group by CargonerdsPermissionDefinitionProvider (myGroup.AddPermission(HubPermissions.InternalAdmin.Default, ...)).
It carries no [GrantInRoles], so no role receives it by seeding.
Cargonerds group¶
Source: src/Cargonerds.Application.Contracts/Permissions/CargonerdsPermissions.cs +
CargonerdsPermissionDefinitionProvider.cs. Group name constant
CargonerdsPermissions.GroupName = "Cargonerds".
This is the host layer: API-key management, dashboard tiles, and the
custom-script setting. Several constants here alias ABP Identity permission strings purely so a
[GrantInRoles] attribute can be attached for the seeder (see
Cross-module additions).
Own permissions (registered under the Cargonerds group)¶
| Permission constant | Value | Annotated role | Effective roles |
|---|---|---|---|
Settings.CustomScript |
Cargonerds.Settings.CustomScript |
SuperUser | SuperUser |
Dashboard.Host |
Cargonerds.Dashboard.Host |
— | none (seed) — Host side (MultiTenancySides.Host) |
Dashboard.Tenant |
Cargonerds.Dashboard.Tenant |
— | none (seed) — Tenant side (MultiTenancySides.Tenant) |
ApiKeys.Default |
Cargonerds.ApiKeys |
— | none (seed) |
ApiKeys.Create |
Cargonerds.ApiKeys.Create |
— | none (seed) |
ApiKeys.Edit |
Cargonerds.ApiKeys.Edit |
— | none (seed) |
ApiKeys.Delete |
Cargonerds.ApiKeys.Delete |
— | none (seed) |
ApiKeys.ManagePermissions |
Cargonerds.ApiKeys.ManagePermissions |
— | none (seed) |
ApiKeys.ManageAll |
Cargonerds.ApiKeys.ManageAll |
SuperUser | SuperUser — manage any user's keys |
Dashboard.Host / Dashboard.Tenant are multi-tenancy-scoped
They are the only permissions in the catalogue passed a MultiTenancySides argument
(Host / Tenant), so each is only visible/grantable on the matching side of the multi-tenant
boundary.
Cross-module additions¶
CargonerdsPermissionDefinitionProvider does not only build its own group — it augments built-in
ABP groups. These permissions appear under ABP's Identity and CMS Kit groups in the
management UI, not under Cargonerds, but they still carry [GrantInRoles] and are seeded by the
same contributor. The Users.* and OrganizationUnits.* constants alias ABP's own
IdentityPermissions.* strings.
| Permission constant | Value | Annotated role | Effective roles |
|---|---|---|---|
Users.Default |
AbpIdentity.Users |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.ViewProfile |
AbpIdentity.Users.ViewProfile |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.SendPasswordMail |
AbpIdentity.Users.SendPasswordMail |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.SendWelcomeMail |
AbpIdentity.Users.SendWelcomeMail |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.EditEmail |
AbpIdentity.Users.EditEmail |
LocalRealtimeAdmin | LocalRealtimeAdmin, SuperUser |
Users.Create |
AbpIdentity.Users.Create |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.Update |
AbpIdentity.Users.Update |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Users.Delete |
AbpIdentity.Users.Delete |
SuperUser | SuperUser |
Users.ManagePermissions |
AbpIdentity.Users.ManagePermissions |
SuperUser | SuperUser |
Users.ManageRoles |
AbpIdentity.Users.ManageRoles |
SuperUser | SuperUser |
OrganizationUnits.Default |
AbpIdentity.OrganizationUnits |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
OrganizationUnits.ManageUsers |
AbpIdentity.OrganizationUnits.ManageUsers |
LocalRealtimeAdmin | LocalRealtimeAdmin, SuperUser |
OrganizationUnits.ManageOU |
AbpIdentity.OrganizationUnits.ManageOU |
LocalRealtimeAdmin | LocalRealtimeAdmin, SuperUser |
OrganizationUnits.ManageRoles |
AbpIdentity.OrganizationUnits.ManageRoles |
LocalRealtimeAdmin | LocalRealtimeAdmin, SuperUser |
OrganizationUnits.Export |
AbpIdentity.OrganizationUnits.Export |
SuperUser | SuperUser |
OrganizationUnits.AdvancedManagement |
AbpIdentity.OrganizationUnits.AdvancedManagement |
— | none — [GrantInRoles] commented out (feature unfinished) |
Comments.UpdateStatus |
CmsKit.UpdateStatus |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Unguarded CMS Kit group lookup
The Users / OrganizationUnits augmentation guards with GetPermissionOrNull(...) != null, but
the CMS Kit comment group is fetched with context.GetGroup(CmsKitPermissions.GroupName)
directly. If the CMS Kit module were ever removed from the dependency graph, that call would
throw at application startup.
Commented-out permissions
CargonerdsPermissions also contains a fully commented-out InternalUsers block and a commented
Users.Impersonation alias — "the feature doesn't exist, will be enabled once the feature is
added." They are not registered and not seeded; do not rely on them.
Pricing group¶
Source: modules/pricing/src/Pricing.Application.Contracts/Permissions/PricingPermissions.cs +
PricingPermissionDefinitionProvider.cs. Group name constant
PricingPermissions.GroupName = "Pricing".
| Permission constant | Value | Annotated role | Effective roles |
|---|---|---|---|
Quotation.QuotationGroup |
Pricing.Quotation |
DefaultCustomer | all roles |
Quotation.View |
Pricing.Quotation.View |
DefaultCustomer | all roles |
Quotation.Add |
Pricing.Quotation.Add |
DefaultCustomer | all roles |
Quotation.Edit |
Pricing.Quotation.Edit |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Quotation.Delete |
Pricing.Quotation.Delete |
AccountOwner | AccountOwner, LocalRealtimeAdmin, SuperUser |
Quotation.Admin |
Pricing.Quotation.Admin |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Quotation.QuoteRequest |
Pricing.Quotation.QuoteRequest |
DefaultCustomer (Inherit = false) |
DefaultCustomer only |
Offer.OfferGroup |
Pricing.Offer |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Offer.Add |
Pricing.Offer.Add |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Offer.Edit |
Pricing.Offer.Edit |
Operator | Operator, AccountOwner, LocalRealtimeAdmin, SuperUser |
Offer.Delete |
Pricing.Offer.Delete |
— | none (seed) |
Offer is nested under Quotation in the tree
In the provider, the Offer group is added as a child of the Quotation permission
(var offer = quotation.AddChild(PricingPermissions.Offer.OfferGroup, ...)), not directly under
the Pricing group. This only affects the management-UI tree shape — the seeded grants are still
determined by each constant's [GrantInRoles].
QuoteRequest is the solution's only Inherit = false
It is pinned to exactly DefaultCustomer and is deliberately not rolled up to higher roles —
the customer-facing "request a quote" action that internal/admin roles intentionally do not get
via inheritance.
Defining a permission¶
A permission is just a string constant, organised into nested static classes so a child's value
encodes its parent. Add the constant (with a [GrantInRoles] attribute if a role should receive it
by default), then register it in the matching provider with a localized display name. Example, the
Hub shipment area:
// modules/hub/src/Hub.Application.Contracts/Permissions/HubPermissions.cs
public static class Shipment
{
[GrantInRoles(RoleConsts.DefaultCustomer)]
public const string ShipmentGroup = GroupName + ".Shipment";
[GrantInRoles(RoleConsts.DefaultCustomer)]
public const string View = ShipmentGroup + ".View";
public const string Create = ShipmentGroup + ".Create"; // no role by seeding
public const string Edit = ShipmentGroup + ".Edit";
public const string Delete = ShipmentGroup + ".Delete";
}
// modules/hub/src/Hub.Application.Contracts/Permissions/HubPermissionDefinitionProvider.cs
public override void Define(IPermissionDefinitionContext context)
{
var hubGroup = context.AddGroup(HubPermissions.GroupName, L("Permission:Hub"));
var shipment = hubGroup.AddPermission(HubPermissions.Shipment.View, L("Permission:Shipment:View"));
shipment.AddChild(HubPermissions.Shipment.Create, L("Permission:Shipment:Create"));
shipment.AddChild(HubPermissions.Shipment.Edit, L("Permission:Shipment:Edit"));
shipment.AddChild(HubPermissions.Shipment.Delete, L("Permission:Shipment:Delete"));
}
private static LocalizableString L(string name) => LocalizableString.Create<HubResource>(name);
Each *Permissions class (Hub and Pricing) exposes a reflection-based GetAll() that enumerates
every permission string in the group via
ReflectionHelper.GetPublicConstantsRecursively:
public static string[] GetAll() =>
ReflectionHelper.GetPublicConstantsRecursively(typeof(HubPermissions));
Localization keys
Display names are localized through each module's resource (L(...) →
LocalizableString.Create<HubResource> / <CargonerdsResource> / <PricingResource>). The keys
follow the Permission:Area:Action convention (e.g. Permission:Shipment:View). See
Localization.
Checking a permission in code¶
ABP registers every defined permission name as an ASP.NET Core authorization policy, so
[Authorize("Perm")] and [Authorize(Policy = "Perm")] are equivalent (both appear in the
codebase). Always use the constants rather than string literals:
// Declarative — hard gate on a method
[Authorize(HubPermissions.Shipment.View)]
public async Task<ShipmentDto> GetAsync(Guid id) { /* ... */ }
// Imperative — conditional shaping (returns bool, does NOT throw)
var canSeeInvoices = await AuthorizationService.IsGrantedAnyAsync(HubPermissions.Invoice.View);
The custom read/list base HubEntityBaseService enforces reads through a ReadPermission override
(e.g. ShipmentAppService.ReadPermission => HubPermissions.Shipment.View) rather than [Authorize].
The full mechanics of declarative vs. imperative enforcement, the ReadPermission hook, and
permission-gated query includes are documented in Authorization.
API keys: permissions capped by the owner¶
The custom API-key authentication subsystem participates in the same permission system rather
than bypassing it. ApiKeyPermissionValueProvider (provider name "AK") only activates for
api-key principals and grants a permission only if (a) some other provider — i.e. the owner's
roles/grants — also grants it, and (b) the key itself carries the "AK" grant. The guarantee: a
key can never exceed its owner's permissions. Key management is itself gated by
CargonerdsPermissions.ApiKeys.* (with ManageAll for admin scope). Full details:
API Authentication.
Gotchas¶
Common pitfalls
- Tree shape ≠ grants. The parent/child structure in a
PermissionDefinitionProviderdrives the management UI only. Actual role grants come from[GrantInRoles]+ the seeder. - Grants are seed-time only. Changing a
[GrantInRoles]attribute requires re-running the DbMigrator to take effect. - Inheritance is upward and on by default.
[GrantInRoles(lowRole)]grants to that role and every higher role. UseInherit = falseto pin (as Pricing'sQuoteRequestdoes). - The
adminrole is not inHierarchy. It is not created or roll-up-targeted byRolesDataSeedContributor; only the five hierarchy roles are. - Many actions are intentionally ungranted by seeding (e.g. all
*.Create/Edit/Deleteon Shipment/Orders/Document/Invoice, allApiKeys.*exceptManageAll,Dashboard.*,InternalAdmin,Offer.Delete). They have no[GrantInRoles]and must be granted manually through the management UI, or are reachable via roles assigned out of band. OrganizationUnits.AdvancedManagement, userImpersonation, and the wholeInternalUsersblock are deliberately ungranted — their[GrantInRoles]is commented out because the backing feature/logic doesn't exist yet.Hub.RealtimeAdmin's value is duplicated in the frontend (per the source comment); changing the string requires a matching frontend update.Hub.InternalAdminlives under theCargonerdsgroup, notHub, despite being declared inHubPermissions.- Unguarded CMS Kit group lookup in
CargonerdsPermissionDefinitionProviderwould throw at startup if the CMS Kit module were removed. [Authorize(x)]and[Authorize(Policy = x)]are identical in ABP (permission name = policy name). Don't read meaning into the difference.
See also¶
- Authorization — concepts, the
[GrantInRoles]convention, seeding flow, and declarative/imperative enforcement ([Authorize],ReadPermission,IsGrantedAnyAsync). - Application Services — service base classes and the auto-API surface that the policies guard.
- Service Patterns — the
HubEntityBaseServiceread/list pipeline that hostsReadPermission. - API Authentication — JWT bearer, OpenIddict, and the owner-capped API-key scheme.
- Configuration Reference — the .NET configuration-loading pipeline (Spark/Azure layers and token replacement); a separate concern from permissions.
- Pricing module — where the Pricing permissions are consumed.
- ABP docs: Authorization & permission management, Identity, Features.