Skip to content

Commit

Permalink
Declarative Authorization, step 8. #20
Browse files Browse the repository at this point in the history
  • Loading branch information
jezzsantos committed Jan 28, 2024
1 parent 3d0fab8 commit 35a4f78
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 199 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class TenantFeaturesSpec
[Fact]
public void WhenAssignableFeatureSets_ThenReturnsSome()
{
var result = TenantFeatures.MemberAssignableFeatures;
var result = TenantFeatures.TenantAssignableFeatures;

result.Count.Should().BeGreaterThan(0);
result.Should().Contain(TenantFeatures.TestingOnly);
Expand Down
6 changes: 3 additions & 3 deletions src/Domain.Interfaces/Authorization/TenantFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static class TenantFeatures
public static readonly FeatureLevel Paid3 = PlatformFeatures.Paid3; // a.k.a Enterprise plan features
public static readonly FeatureLevel PaidTrial = PlatformFeatures.PaidTrial; // a.k.a Standard plan features
public static readonly FeatureLevel TestingOnly = PlatformFeatures.TestingOnly;
public static readonly IReadOnlyList<FeatureLevel> MemberAssignableFeatures = new List<FeatureLevel>
public static readonly IReadOnlyList<FeatureLevel> TenantAssignableFeatures = new List<FeatureLevel>
{
// EXTEND: Add new features that can be assigned/unassigned to EndUsers
Basic,
Expand Down Expand Up @@ -54,9 +54,9 @@ public static class TenantFeatures
/// <summary>
/// Whether the <see cref="feature" /> is assignable
/// </summary>
public static bool IsMemberAssignableFeature(string feature)
public static bool IsTenantAssignableFeature(string feature)
{
return MemberAssignableFeatures
return TenantAssignableFeatures
.Select(feat => feat.Name)
.ContainsIgnoreCase(feature);
}
Expand Down
2 changes: 1 addition & 1 deletion src/EndUsersDomain/EndUserRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public Result<Error> AssignMembershipFeatures(EndUserRoot assigner, Identifier o
{
foreach (var feature in tenantFeatures.Items)
{
if (!TenantFeatures.IsMemberAssignableFeature(feature.Identifier))
if (!TenantFeatures.IsTenantAssignableFeature(feature.Identifier))
{
return Error.RuleViolation(
Resources.EndUserRoot_UnassignableTenantFeature.Format(feature.Identifier));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ public void WhenConstructedWithMultipleRolesAndNoFeatures_ThenHasAtLeastBasicPla
{
var result = new AuthorizeAttribute(Roles.Platform_Standard | Roles.Platform_Operations);

result.Roles.All.Should().ContainInOrder(PlatformRoles.Standard, PlatformRoles.Operations);
result.Roles.Platform.Should().ContainInOrder(PlatformRoles.Standard, PlatformRoles.Operations);
result.Roles.All.Should().ContainInOrder(PlatformRoles.Operations, PlatformRoles.Standard);
result.Roles.Platform.Should().ContainInOrder(PlatformRoles.Operations, PlatformRoles.Standard);
result.Roles.Tenant.Should().BeEmpty();
result.Features.All.Should().ContainSingle(level => level == PlatformFeatures.Basic);
result.Features.Platform.Should().ContainSingle(level => level == PlatformFeatures.Basic);
Expand Down Expand Up @@ -117,9 +117,9 @@ public void WhenConstructedWithARoleAndMultipleFeatures_ThenHasThatAccess()
result.Roles.Platform.Should().ContainSingle(rol => rol == PlatformRoles.Operations);
result.Roles.Tenant.Should().BeEmpty();
result.Features.All.Should()
.ContainInOrder(PlatformFeatures.PaidTrial, PlatformFeatures.Paid2);
.ContainInOrder(PlatformFeatures.Paid2, PlatformFeatures.PaidTrial);
result.Features.Platform.Should()
.ContainInOrder(PlatformFeatures.PaidTrial, PlatformFeatures.Paid2);
.ContainInOrder(PlatformFeatures.Paid2, PlatformFeatures.PaidTrial);
result.Features.Tenant.Should().BeEmpty();
}

Expand All @@ -129,8 +129,8 @@ public void WhenConstructedWithAFeatureAndMultipleRoles_ThenHasThatAccess()
var result = new AuthorizeAttribute(Features.Platform_PaidTrial, Roles.Platform_Standard |
Roles.Platform_Operations);

result.Roles.All.Should().ContainInOrder(PlatformRoles.Standard, PlatformRoles.Operations);
result.Roles.Platform.Should().ContainInOrder(PlatformRoles.Standard, PlatformRoles.Operations);
result.Roles.All.Should().ContainInOrder(PlatformRoles.Operations, PlatformRoles.Standard);
result.Roles.Platform.Should().ContainInOrder(PlatformRoles.Operations, PlatformRoles.Standard);
result.Roles.Tenant.Should().BeEmpty();
result.Features.All.Should().ContainSingle(feat => feat == PlatformFeatures.PaidTrial);
result.Features.Platform.Should().ContainSingle(feat => feat == PlatformFeatures.PaidTrial);
Expand Down Expand Up @@ -188,10 +188,10 @@ public void WhenConstructedWithMultipleFeatures_ThenHasAtLeastBasicPlatformRoleA
result.Roles.All.Should().ContainSingle(rol => rol == PlatformRoles.Standard);
result.Roles.Platform.Should().ContainSingle(rol => rol == PlatformRoles.Standard);
result.Roles.Tenant.Should().BeEmpty();
result.Features.All.Should().ContainInOrder(TenantFeatures.PaidTrial, TenantFeatures.Paid2);
result.Features.All.Should().ContainInOrder(TenantFeatures.Paid2, TenantFeatures.PaidTrial);
result.Features.Platform.Should().BeEmpty();
result.Features.Tenant.Should()
.ContainInOrder(TenantFeatures.PaidTrial, TenantFeatures.Paid2);
.ContainInOrder(TenantFeatures.Paid2, TenantFeatures.PaidTrial);
}

[Fact]
Expand All @@ -204,10 +204,10 @@ public void WhenConstructedWithARoleAndMultipleFeatures_ThenHasThatAccess()
result.Roles.Platform.Should().BeEmpty();
result.Roles.Tenant.Should().ContainSingle(rol => rol == TenantRoles.Member);
result.Features.All.Should()
.ContainInOrder(TenantFeatures.PaidTrial, TenantFeatures.Paid2);
.ContainInOrder(TenantFeatures.Paid2, TenantFeatures.PaidTrial);
result.Features.Platform.Should().BeEmpty();
result.Features.Tenant.Should()
.ContainInOrder(TenantFeatures.PaidTrial, TenantFeatures.Paid2);
.ContainInOrder(TenantFeatures.Paid2, TenantFeatures.PaidTrial);
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ private static string CreateJwtToken(IConfigurationSettings settings, ITokensSer
.IssueTokensAsync(new EndUserWithMemberships
{
Id = "auserid",
Roles = new List<string> { PlatformRoles.Standard.Name }
Roles = new List<string> { PlatformRoles.Standard.Name },
Features = new List<string> { PlatformFeatures.Basic.Name }
}).GetAwaiter().GetResult().Value.AccessToken;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@

namespace Infrastructure.Web.Api.Interfaces;

/// <summary>
/// Provides scopes for both Platform and Tenant
/// </summary>
public enum RoleAndFeatureScope
{
Platform = 0,
Tenant = 1
}

/// <summary>
/// Provides all roles for both Platform and Tenant
/// </summary>
[Flags]
public enum Roles
{
Platform_Standard = 1 << 0,
Platform_ExternalWebhookService = 1 << 0,
Platform_Operations = 1 << 1,
Platform_ServiceAccount = 1 << 2,
Tenant_Member = 1 << 3,
Tenant_Owner = 1 << 4,
Tenant_BillingAdmin = 1 << 5
Platform_Standard = 1 << 3,
Platform_TestingOnly = 1 << 4,
Platform_TestingOnlySuperUser = 1 << 5,
Tenant_BillingAdmin = 1 << 6,
Tenant_Member = 1 << 7,
Tenant_Owner = 1 << 8,
Tenant_TestingOnly = 1 << 9
}

/// <summary>
Expand All @@ -24,28 +37,22 @@ public enum Roles
public enum Features
{
Platform_Basic = 1 << 0,
Platform_PaidTrial = 1 << 1,
Platform_Paid2 = 1 << 2,
Platform_Paid3 = 1 << 3,
Tenant_Basic = 1 << 4,
Tenant_PaidTrial = 1 << 5,
Tenant_Paid2 = 1 << 6,
Tenant_Paid3 = 1 << 7
}

/// <summary>
/// Provides scopes for both Platform and Tenant
/// </summary>
public enum RoleAndFeatureScope
{
Platform = 0,
Tenant = 1
Platform_Paid2 = 1 << 1,
Platform_Paid3 = 1 << 2,
Platform_PaidTrial = 1 << 3,
Platform_TestingOnly = 1 << 4,
Platform_TestingOnlySuperUser = 1 << 5,
Tenant_Basic = 1 << 6,
Tenant_Paid2 = 1 << 7,
Tenant_Paid3 = 1 << 8,
Tenant_PaidTrial = 1 << 9,
Tenant_TestingOnly = 1 << 10
}

/// <summary>
/// Provides conversions for both Platform and Tenant
/// </summary>
public static class AuthorizationAttributeExtensions
internal static class AuthorizationAttributeExtensions
{
/// <summary>
/// Converts the <see cref="name" /> in the specified <see cref="scope" /> to the appropriate
Expand All @@ -59,21 +66,26 @@ public static FeatureLevel ToFeatureByName(this string name, RoleAndFeatureScope
{
return PlatformFeatures.Basic;
}

if (name == PlatformFeatures.PaidTrial.Name)
{
return PlatformFeatures.PaidTrial;
}

if (name == PlatformFeatures.Paid2.Name)
{
return PlatformFeatures.Paid2;
}

if (name == PlatformFeatures.Paid3.Name)
{
return PlatformFeatures.Paid3;
}
if (name == PlatformFeatures.PaidTrial.Name)
{
return PlatformFeatures.PaidTrial;
}
if (name == PlatformFeatures.TestingOnly.Name)
{
return PlatformFeatures.TestingOnly;
}
if (name == PlatformFeatures.TestingOnlySuperUser.Name)
{
return PlatformFeatures.TestingOnlySuperUser;
}

throw new ArgumentOutOfRangeException(nameof(name), name, null);
}
Expand All @@ -84,21 +96,22 @@ public static FeatureLevel ToFeatureByName(this string name, RoleAndFeatureScope
{
return TenantFeatures.Basic;
}

if (name == TenantFeatures.PaidTrial.Name)
{
return TenantFeatures.PaidTrial;
}

if (name == TenantFeatures.Paid2.Name)
{
return TenantFeatures.Paid2;
}

if (name == TenantFeatures.Paid3.Name)
{
return TenantFeatures.Paid3;
}
if (name == TenantFeatures.PaidTrial.Name)
{
return TenantFeatures.PaidTrial;
}
if (name == TenantFeatures.TestingOnly.Name)
{
return TenantFeatures.TestingOnly;
}

throw new ArgumentOutOfRangeException(nameof(name), name, null);
}
Expand All @@ -107,21 +120,24 @@ public static FeatureLevel ToFeatureByName(this string name, RoleAndFeatureScope
}

/// <summary>
/// Converts an individual <see cref="features" /> flag to it respective <see cref="FeatureLevel" />
/// Converts an individual <see cref="feature" /> flag to its respective <see cref="FeatureLevel" />
/// </summary>
public static FeatureLevel ToFeatureLevel(this Features features)
public static FeatureLevel ToFeatureLevel(this Features feature)
{
return features switch
return feature switch
{
Features.Platform_Basic => PlatformFeatures.Basic,
Features.Platform_PaidTrial => PlatformFeatures.PaidTrial,
Features.Platform_Paid2 => PlatformFeatures.Paid2,
Features.Platform_Paid3 => PlatformFeatures.Paid3,
Features.Platform_PaidTrial => PlatformFeatures.PaidTrial,
Features.Platform_TestingOnly => PlatformFeatures.TestingOnly,
Features.Platform_TestingOnlySuperUser => PlatformFeatures.TestingOnlySuperUser,
Features.Tenant_Basic => TenantFeatures.Basic,
Features.Tenant_PaidTrial => TenantFeatures.PaidTrial,
Features.Tenant_Paid2 => TenantFeatures.Paid2,
Features.Tenant_Paid3 => TenantFeatures.Paid3,
_ => throw new ArgumentOutOfRangeException(nameof(features), features, null)
Features.Tenant_PaidTrial => TenantFeatures.PaidTrial,
Features.Tenant_TestingOnly => TenantFeatures.TestingOnly,
_ => throw new ArgumentOutOfRangeException(nameof(feature), feature, null)
};
}

Expand All @@ -132,39 +148,51 @@ public static RoleLevel ToRoleByName(this string name, RoleAndFeatureScope scope
{
if (scope == RoleAndFeatureScope.Platform)
{
if (name == PlatformRoles.Standard.Name)
if (name == PlatformRoles.ExternalWebhookService.Name)
{
return PlatformRoles.Standard;
return PlatformRoles.ExternalWebhookService;
}

if (name == PlatformRoles.Operations.Name)
{
return PlatformRoles.Operations;
}

if (name == PlatformRoles.ServiceAccount.Name)
{
return PlatformRoles.ServiceAccount;
}
if (name == PlatformRoles.Standard.Name)
{
return PlatformRoles.Standard;
}
if (name == PlatformRoles.TestingOnly.Name)
{
return PlatformRoles.TestingOnly;
}
if (name == PlatformRoles.TestingOnlySuperUser.Name)
{
return PlatformRoles.TestingOnlySuperUser;
}

throw new ArgumentOutOfRangeException(nameof(name), name, null);
}

if (scope == RoleAndFeatureScope.Tenant)
{
if (name == TenantRoles.BillingAdmin.Name)
{
return TenantRoles.BillingAdmin;
}
if (name == TenantRoles.Member.Name)
{
return TenantRoles.Member;
}

if (name == TenantRoles.Owner.Name)
{
return TenantRoles.Owner;
}

if (name == TenantRoles.BillingAdmin.Name)
if (name == TenantRoles.TestingOnly.Name)
{
return TenantRoles.BillingAdmin;
return TenantRoles.TestingOnly;
}

throw new ArgumentOutOfRangeException(nameof(name), name, null);
Expand All @@ -174,19 +202,23 @@ public static RoleLevel ToRoleByName(this string name, RoleAndFeatureScope scope
}

/// <summary>
/// Converts an individual <see cref="roles" /> flag to it respective <see cref="RoleLevel" />
/// Converts an individual <see cref="role" /> flag to its respective <see cref="RoleLevel" />
/// </summary>
public static RoleLevel ToRoleLevel(this Roles roles)
public static RoleLevel ToRoleLevel(this Roles role)
{
return roles switch
return role switch
{
Roles.Platform_Standard => PlatformRoles.Standard,
Roles.Platform_ExternalWebhookService => PlatformRoles.ExternalWebhookService,
Roles.Platform_Operations => PlatformRoles.Operations,
Roles.Platform_ServiceAccount => PlatformRoles.ServiceAccount,
Roles.Platform_Standard => PlatformRoles.Standard,
Roles.Platform_TestingOnly => PlatformRoles.TestingOnly,
Roles.Platform_TestingOnlySuperUser => PlatformRoles.TestingOnlySuperUser,
Roles.Tenant_BillingAdmin => TenantRoles.BillingAdmin,
Roles.Tenant_Member => TenantRoles.Member,
Roles.Tenant_Owner => TenantRoles.Owner,
Roles.Tenant_BillingAdmin => TenantRoles.BillingAdmin,
_ => throw new ArgumentOutOfRangeException(nameof(roles), roles, null)
Roles.Tenant_TestingOnly => TenantRoles.TestingOnly,
_ => throw new ArgumentOutOfRangeException(nameof(role), role, null)
};
}
}
9 changes: 9 additions & 0 deletions src/SaaStack.sln
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndUsersInfrastructure.Unit
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EndUsersInfrastructure.IntegrationTests", "EndUsersInfrastructure.IntegrationTests\EndUsersInfrastructure.IntegrationTests.csproj", "{306F13C6-CC51-4956-BB88-54355BD05A42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tools.Generators.Web.Api.Authorization.UnitTests", "Tools.Generators.Web.Api.Authorization.UnitTests\Tools.Generators.Web.Api.Authorization.UnitTests.csproj", "{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -919,6 +921,12 @@ Global
{306F13C6-CC51-4956-BB88-54355BD05A42}.Release|Any CPU.Build.0 = Release|Any CPU
{306F13C6-CC51-4956-BB88-54355BD05A42}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU
{306F13C6-CC51-4956-BB88-54355BD05A42}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.Release|Any CPU.Build.0 = Release|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.ReleaseForDeploy|Any CPU.ActiveCfg = ReleaseForDeploy|Any CPU
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7}.ReleaseForDeploy|Any CPU.Build.0 = ReleaseForDeploy|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F5C77A86-38AF-40E4-82FC-617E624B2754} = {508E7DA4-4DF2-4201-955D-CCF70C41AD05}
Expand Down Expand Up @@ -1060,5 +1068,6 @@ Global
{DBFD1050-E4F2-4BA9-88B4-E0450A8C77A1} = {BAE0D6F2-6920-4B02-9F30-D71B04B7170D}
{064B025A-7951-4706-B6A4-86BF6475239C} = {F2F759A4-6B5D-4E11-AFCC-679BF0E72AE6}
{306F13C6-CC51-4956-BB88-54355BD05A42} = {F2F759A4-6B5D-4E11-AFCC-679BF0E72AE6}
{E081B52F-A4AC-47A0-B03C-F23BF34CE1E7} = {A25A3BA8-5602-4825-9595-2CF96B166920}
EndGlobalSection
EndGlobal
Loading

0 comments on commit 35a4f78

Please sign in to comment.