diff --git a/src/ServiceControl.Infrastructure/Auth/Permissions.cs b/src/ServiceControl.Infrastructure/Auth/Permissions.cs
index 006fccf0f5..04f6582c14 100644
--- a/src/ServiceControl.Infrastructure/Auth/Permissions.cs
+++ b/src/ServiceControl.Infrastructure/Auth/Permissions.cs
@@ -85,7 +85,7 @@ public static class Permissions
///
public const string ErrorThroughputManage = "error:throughput:manage";
- /// Platform connections area — viewing and managing broker/platform connection settings.
+ /// Platform connections area — viewing and managing platform connection settings.
public const string ErrorConnectionsView = "error:connections:view";
///
public const string ErrorConnectionsManage = "error:connections:manage";
diff --git a/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs b/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs
index bb79f98f02..8b5d64d6d7 100644
--- a/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs
+++ b/src/ServiceControl.Infrastructure/Auth/RolePermissions.cs
@@ -37,15 +37,17 @@ public static class RolePermissions
static readonly Dictionary RolePatterns = new(StringComparer.OrdinalIgnoreCase)
{
[Reader] = ["*:*:view"],
- [Writer] = ["*:*:*"],
+ [Writer] =
+ [
+ "*:*:*",
+ "-error:licensing:*",
+ "-error:notifications:*",
+ "-error:redirects:*",
+ "-error:throughput:*",
+ ],
[Admin] =
[
- "*:*:view",
- "error:licensing:*",
- "error:notifications:*",
- "error:redirects:*",
- "error:throughput:*",
- "error:connections:*",
+ "*:*:*",
],
};
@@ -106,15 +108,23 @@ static FrozenDictionary> Expand()
foreach (var (role, patterns) in RolePatterns)
{
+ var includePatterns = patterns.Where(pattern => pattern[0] != '-');
+ var excludePatterns = patterns.Where(pattern => pattern[0] == '-').Select(pattern => pattern[1..]);
+
expanded[role] = Permissions.All
- .Where(permission => patterns.Any(pattern => Matches(pattern, permission)))
+ .Where(permission => includePatterns.Any(pattern => Matches(pattern, permission))
+ && !excludePatterns.Any(pattern => Matches(pattern, permission)))
.ToFrozenSet(StringComparer.Ordinal);
}
return expanded.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}
- /// Matches a colon-delimited permission against a pattern where * is a segment wildcard.
+ ///
+ /// Matches a colon-delimited permission against a pattern where * is a segment wildcard.
+ /// A leading - on the pattern (stripped by before calling this method)
+ /// marks the pattern as an exclusion rather than a grant.
+ ///
static bool Matches(string pattern, string permission)
{
var patternSegments = pattern.Split(':');