Skip to content

Commit

Permalink
Working on tenants and setup
Browse files Browse the repository at this point in the history
  • Loading branch information
JonPSmith committed Jun 2, 2021
1 parent 6410089 commit 4cf5227
Show file tree
Hide file tree
Showing 19 changed files with 571 additions and 58 deletions.
4 changes: 4 additions & 0 deletions AuthPermissions/AuthPermissions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="TenantParts\" />
</ItemGroup>

</Project>
31 changes: 27 additions & 4 deletions AuthPermissions/AuthPermissionsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,60 @@
using System;
using System.Collections.Generic;
using AuthPermissions.SetupParts;
using AuthPermissions.TenantParts;

namespace AuthPermissions
{
public class AuthPermissionsOptions : IAuthPermissionsOptions
{
/// <summary>
/// The different database types that AuthPermissions supports
/// </summary>
public enum DatabaseTypes { NotSet, InMemory, SqlServer }


//--------------------------------------------------
//Tenant settings

/// <summary>
/// This defines whether tenant code is activated, and whether the
/// multi-tenant is is a single layer, or many layers (hierarchical)
/// </summary>
public TenantTypes TenantType { get; set; }

/// <summary>
/// If true, then the login must ask the user to pick which one they want to access
/// Also, the role admin allows you to set up different roles for each tenant the user is in
/// </summary>
public bool UserCanBeInMoreThanOneTenant { get; set; }

//-------------------------------------------------
//internal set properties/handles

/// <summary>
/// Internal: holds the type of the Enum Permissions
/// </summary>
public Type EnumPermissionsType { get; internal set; }

/// <summary>
/// This contains the type of database used
/// Internal: This contains the type of database used
/// </summary>
public DatabaseTypes DatabaseType { get; internal set; }

/// <summary>
/// This holds the a string containing the definition of the tenants
/// Internal: This holds the a string containing the definition of the tenants
/// See the <see cref="SetupExtensions.AddTenantsIfEmpty"/> method for the format of the lines
/// </summary>
public string UserTenantSetupText { get; internal set; }

/// <summary>
/// This holds the a string containing the definition of the RolesToPermission database class
/// Internal: This holds the a string containing the definition of the RolesToPermission database class
/// See the <see cref="SetupExtensions.AddRolesPermissionsIfEmpty"/> method for the format of the lines
/// </summary>
public string RolesPermissionsSetupText { get; internal set; }

/// <summary>
/// This holds the definition for a user, with its various parts
/// Internal: This holds the definition for a user, with its various parts
/// See the <see cref="DefineUserWithRolesTenant"/> class for information you need to provide
/// </summary>
public List<DefineUserWithRolesTenant> UserRolesSetupData { get; internal set; }
Expand Down
14 changes: 10 additions & 4 deletions AuthPermissions/AuthSetupData.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
// Licensed under MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using AuthPermissions.SetupParts;
using Microsoft.Extensions.DependencyInjection;

namespace AuthPermissions
{
/// <summary>
/// This class carries data through the setup extensions
/// </summary>
public class AuthSetupData
{


public AuthSetupData(IServiceCollection services, AuthPermissionsOptions options)
internal AuthSetupData(IServiceCollection services, AuthPermissionsOptions options)
{
Services = services;
Options = options;
}

/// <summary>
/// The DI ServiceCollection which AuthPermissions services, constants and policies are registered to
/// </summary>
public IServiceCollection Services { get; }

/// <summary>
/// This holds the AuthPermissions options
/// </summary>
public AuthPermissionsOptions Options { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public static class AuthDbConstants

public const int TenantNameSize = 100;

public const int TenantDataKeySize = 100;

public const int DataKeySize = 64;
}
}
7 changes: 1 addition & 6 deletions AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ namespace AuthPermissions.DataLayer.Classes.SupportTypes
{
public abstract class TenantBase
{
public TenantBase(Guid tenantId)
{
TenantId = tenantId;
}

public Guid TenantId { get; private set; }
public int TenantId { get; protected set; }
}
}
109 changes: 109 additions & 0 deletions AuthPermissions/DataLayer/Classes/Tenant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
// Licensed under MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using AuthPermissions.DataLayer.Classes.SupportTypes;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

namespace AuthPermissions.DataLayer.Classes
{
/// <summary>
/// This is used for multi-tenant systems
/// </summary>
[Index(nameof(TenantName), IsUnique = true)]
public class Tenant :TenantBase
{
private HashSet<Tenant> _children;
private string _parentDataKey;

/// <summary>
/// This defines a tenant in a single tenant multi-tenant system.
/// </summary>
/// <param name="tenantName"></param>
public Tenant(string tenantName)
{
TenantName = tenantName ?? throw new ArgumentNullException(nameof(tenantName));
}

private Tenant(string tenantName, string parentDataKey, Tenant parent)
{
TenantName = tenantName ?? throw new ArgumentNullException(nameof(tenantName));
_parentDataKey = parentDataKey;
Parent = parent;
}


/// <summary>
/// This defines a tenant in a hierarchical multi-tenant system with a parent/child relationships
/// You MUST a) have every parent layer loaded and b) all parents must have a valid primary key
/// </summary>
public static Tenant SetupHierarchicalTenant(string tenantName, Tenant parent)
{
//We check that the higher layer a) are there and b) have a

var lookParent = parent;
while (lookParent != null)
{
if (lookParent.TenantId == default)
throw new InvalidOperationException(
"The parent in the hierarchical setup doesn't have a valid primary key");
if (lookParent.Parent == null && lookParent.ParentTenantId != null)
throw new InvalidOperationException(
"There is a parent layer that hasn't been read in");
lookParent = lookParent.Parent;
}

return new Tenant(tenantName, parent?.TenantDataKey, parent);
}

/// <summary>
/// Easy way to see the tenant and its key
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"{TenantName}: Key = {TenantDataKey}";
}

/// <summary>
/// This is the name defined for this tenant. This is unique
/// </summary>
[Required(AllowEmptyStrings = false)]
[MaxLength(AuthDbConstants.TenantNameSize)]
public string TenantName { get; private set; }

/// <summary>
/// This contains the data key for this tenant.
/// If it is a single layer multi-tenant it will by the TenantId as a string
/// If it is a hierarchical multi-tenant it will contains a concatenation of the tenantsId
/// </summary>
[Required(AllowEmptyStrings = false)]
[MaxLength(AuthDbConstants.TenantDataKeySize)]
public string TenantDataKey => _parentDataKey + $".{TenantId}";

//---------------------------------------------------------
//relationships - only used for hierarchical multi-tenant system

/// <summary>
/// Foreign key to parent - can by null
/// </summary>
public int? ParentTenantId { get; private set; }

/// <summary>
/// The parent tenant (if it exists)
/// </summary>
[ForeignKey(nameof(ParentTenantId))]
public Tenant Parent { get; private set; }

/// <summary>
/// The optional children
/// </summary>
public IReadOnlyCollection<Tenant> Children => _children?.ToList();

}
}
22 changes: 0 additions & 22 deletions AuthPermissions/DataLayer/Classes/TenantDefinition.cs

This file was deleted.

4 changes: 2 additions & 2 deletions AuthPermissions/DataLayer/Classes/UserDataKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ namespace AuthPermissions.DataLayer.Classes
{
public class UserDataKey : TenantBase
{
public UserDataKey(string userId, string dataKey, Guid tenantId = default)
: base(tenantId)
public UserDataKey(string userId, string dataKey, int tenantId = default)
{
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
DataKey = dataKey ?? throw new ArgumentNullException(nameof(dataKey));
TenantId = tenantId;
}

[Required(AllowEmptyStrings = false)]
Expand Down
9 changes: 4 additions & 5 deletions AuthPermissions/DataLayer/Classes/UserToRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ namespace AuthPermissions.DataLayer.Classes
/// </summary>
public class UserToRole : TenantBase
{
private UserToRole(Guid tenantId) //Needed by EF Core
: base(tenantId) {}
private UserToRole() {} //Needed by EF Core

public UserToRole(string userId, string userName, RoleToPermissions role, Guid tenantId = default)
: base(tenantId)
public UserToRole(string userId, string userName, RoleToPermissions role, int tenantId = default)
{
UserId = userId;
UserName = userName;
Role = role;
TenantId = tenantId;
}

//I use a composite key for this table: combination of UserId, TenantId and RoleName
Expand Down Expand Up @@ -54,7 +53,7 @@ public override string ToString()


public static IStatusGeneric<UserToRole> AddRoleToUser(string userId, string userName, string roleName,
AuthPermissionsDbContext context, Guid tenantId = default)
AuthPermissionsDbContext context, int tenantId = default)
{
if (roleName == null) throw new ArgumentNullException(nameof(roleName));

Expand Down
8 changes: 7 additions & 1 deletion AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ public AuthPermissionsDbContext(DbContextOptions<AuthPermissionsDbContext> optio
{ }

public DbSet<RoleToPermissions> RoleToPermissions { get; set; }
public DbSet<UserDataKey> UserTenantKey { get; set; }
public DbSet<UserDataKey> UserDataKey { get; set; }
public DbSet<Tenant> Tenants { get; set; }
public DbSet<UserToRole> UserToRoles { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("authp");

modelBuilder.Entity<Tenant>().HasKey(x => x.TenantId);
modelBuilder.Entity<Tenant>()
.Property("_parentDataKey")
.HasColumnName("ParentDataKey");

modelBuilder.Entity<UserToRole>()
.HasKey(x => new { x.UserId, x.TenantId, x.RoleName });

Expand Down
24 changes: 18 additions & 6 deletions AuthPermissions/SetupExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,22 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData)
return setupData;
}

//public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText)
//{
// return setupData;
//}
/// <summary>
/// This allows you to define the name of each tenant by name
/// If you are using a hierarchical tenant design, then the whole
/// </summary>
/// <param name="setupData"></param>
/// <param name="linesOfText">If you are using a single layer then each line contains the a tenant name
/// If you are using hierarchical tenant, then each line contains the whole hierarchy with '|' as separator, e.g.
/// Holding company | USA branch | East Coast | New York
/// Holding company | USA branch | East Coast | Washington
/// </param>
/// <returns></returns>
public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText)
{
setupData.Options.UserTenantSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText));
return setupData;
}

/// <summary>
/// This allows you to add Roles with their permissions, but only if the auth database contains NO RoleToPermissions
Expand All @@ -93,13 +105,13 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData)
/// <returns>AuthSetupData</returns>
public static AuthSetupData AddRolesPermissionsIfEmpty(this AuthSetupData setupData, string linesOfText)
{
setupData.Options.RolesPermissionsSetupText = linesOfText;
setupData.Options.RolesPermissionsSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText));
return setupData;
}

/// <summary>
/// This allows you to add what roles a user has, but only if the auth database doesn't have any UserToRoles in the database
/// NOTE: The <see cref="userRolesSetup"/> parameter must contain a list of userId+roles.
/// The <paramref name="userRolesSetup"/> parameter must contain a list of userId+roles.
/// </summary>
/// <param name="setupData"></param>
/// <param name="userRolesSetup">A list of <see cref="DefineUserWithRolesTenant"/> containing the information on users and what auth roles they have.
Expand Down
9 changes: 7 additions & 2 deletions AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ namespace AuthPermissions.SetupParts.Internal
{
internal static class CommaDelimitedHandler
{
public static List<string> DecodeCheckCommaDelimitedString(this string line, int charNum, Action<string, int> checkValid)
public static List<string> DecodeCodeNameWithTrimming(this string line, int charNum, Action<string, int> checkValid)
{
var trimmedNames = new List<string>();
while (charNum < line.Length)
{
if (!char.IsLetterOrDigit(line[charNum])) charNum++;
if (!char.IsLetterOrDigit(line[charNum]))
{
charNum++;
continue;
}

var foundName = "";
var startOfName = charNum;
while (charNum < line.Length && char.IsLetterOrDigit(line[charNum]))
Expand Down
Loading

0 comments on commit 4cf5227

Please sign in to comment.