diff --git a/AuthPermissions/AuthPermissions.csproj b/AuthPermissions/AuthPermissions.csproj
index cc518724..3a230cf5 100644
--- a/AuthPermissions/AuthPermissions.csproj
+++ b/AuthPermissions/AuthPermissions.csproj
@@ -22,4 +22,8 @@
+
+
+
+
diff --git a/AuthPermissions/AuthPermissionsOptions.cs b/AuthPermissions/AuthPermissionsOptions.cs
index 846f8890..b8b275d8 100644
--- a/AuthPermissions/AuthPermissionsOptions.cs
+++ b/AuthPermissions/AuthPermissionsOptions.cs
@@ -4,37 +4,60 @@
using System;
using System.Collections.Generic;
using AuthPermissions.SetupParts;
+using AuthPermissions.TenantParts;
namespace AuthPermissions
{
public class AuthPermissionsOptions : IAuthPermissionsOptions
{
+ ///
+ /// The different database types that AuthPermissions supports
+ ///
public enum DatabaseTypes { NotSet, InMemory, SqlServer }
+
+ //--------------------------------------------------
+ //Tenant settings
+
+ ///
+ /// This defines whether tenant code is activated, and whether the
+ /// multi-tenant is is a single layer, or many layers (hierarchical)
+ ///
+ public TenantTypes TenantType { get; set; }
+
+ ///
+ /// 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
+ ///
+ public bool UserCanBeInMoreThanOneTenant { get; set; }
+
//-------------------------------------------------
//internal set properties/handles
+ ///
+ /// Internal: holds the type of the Enum Permissions
+ ///
public Type EnumPermissionsType { get; internal set; }
///
- /// This contains the type of database used
+ /// Internal: This contains the type of database used
///
public DatabaseTypes DatabaseType { get; internal set; }
///
- /// 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 method for the format of the lines
///
public string UserTenantSetupText { get; internal set; }
///
- /// 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 method for the format of the lines
///
public string RolesPermissionsSetupText { get; internal set; }
///
- /// 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 class for information you need to provide
///
public List UserRolesSetupData { get; internal set; }
diff --git a/AuthPermissions/AuthSetupData.cs b/AuthPermissions/AuthSetupData.cs
index cb6e2a0a..8c9623e5 100644
--- a/AuthPermissions/AuthSetupData.cs
+++ b/AuthPermissions/AuthSetupData.cs
@@ -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
{
+ ///
+ /// This class carries data through the setup extensions
+ ///
public class AuthSetupData
{
- public AuthSetupData(IServiceCollection services, AuthPermissionsOptions options)
+ internal AuthSetupData(IServiceCollection services, AuthPermissionsOptions options)
{
Services = services;
Options = options;
}
+ ///
+ /// The DI ServiceCollection which AuthPermissions services, constants and policies are registered to
+ ///
public IServiceCollection Services { get; }
+ ///
+ /// This holds the AuthPermissions options
+ ///
public AuthPermissionsOptions Options { get; }
}
}
\ No newline at end of file
diff --git a/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs b/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs
index eb71d0a9..7c82456f 100644
--- a/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs
+++ b/AuthPermissions/DataLayer/Classes/SupportTypes/AuthDbConstants.cs
@@ -13,6 +13,8 @@ public static class AuthDbConstants
public const int TenantNameSize = 100;
+ public const int TenantDataKeySize = 100;
+
public const int DataKeySize = 64;
}
}
\ No newline at end of file
diff --git a/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs b/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs
index a3ca171a..855df947 100644
--- a/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs
+++ b/AuthPermissions/DataLayer/Classes/SupportTypes/TenantBase.cs
@@ -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; }
}
}
\ No newline at end of file
diff --git a/AuthPermissions/DataLayer/Classes/Tenant.cs b/AuthPermissions/DataLayer/Classes/Tenant.cs
new file mode 100644
index 00000000..eedaaa7a
--- /dev/null
+++ b/AuthPermissions/DataLayer/Classes/Tenant.cs
@@ -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
+{
+ ///
+ /// This is used for multi-tenant systems
+ ///
+ [Index(nameof(TenantName), IsUnique = true)]
+ public class Tenant :TenantBase
+ {
+ private HashSet _children;
+ private string _parentDataKey;
+
+ ///
+ /// This defines a tenant in a single tenant multi-tenant system.
+ ///
+ ///
+ 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;
+ }
+
+
+ ///
+ /// 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
+ ///
+ 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);
+ }
+
+ ///
+ /// Easy way to see the tenant and its key
+ ///
+ ///
+ public override string ToString()
+ {
+ return $"{TenantName}: Key = {TenantDataKey}";
+ }
+
+ ///
+ /// This is the name defined for this tenant. This is unique
+ ///
+ [Required(AllowEmptyStrings = false)]
+ [MaxLength(AuthDbConstants.TenantNameSize)]
+ public string TenantName { get; private set; }
+
+ ///
+ /// 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
+ ///
+ [Required(AllowEmptyStrings = false)]
+ [MaxLength(AuthDbConstants.TenantDataKeySize)]
+ public string TenantDataKey => _parentDataKey + $".{TenantId}";
+
+ //---------------------------------------------------------
+ //relationships - only used for hierarchical multi-tenant system
+
+ ///
+ /// Foreign key to parent - can by null
+ ///
+ public int? ParentTenantId { get; private set; }
+
+ ///
+ /// The parent tenant (if it exists)
+ ///
+ [ForeignKey(nameof(ParentTenantId))]
+ public Tenant Parent { get; private set; }
+
+ ///
+ /// The optional children
+ ///
+ public IReadOnlyCollection Children => _children?.ToList();
+
+ }
+}
\ No newline at end of file
diff --git a/AuthPermissions/DataLayer/Classes/TenantDefinition.cs b/AuthPermissions/DataLayer/Classes/TenantDefinition.cs
deleted file mode 100644
index ac27b71c..00000000
--- a/AuthPermissions/DataLayer/Classes/TenantDefinition.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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.ComponentModel.DataAnnotations;
-using AuthPermissions.DataLayer.Classes.SupportTypes;
-
-namespace AuthPermissions.DataLayer.Classes
-{
- public class TenantDefinition :TenantBase
- {
- public TenantDefinition(Guid tenantId, string tenantName)
- : base(tenantId)
- {
-
- }
-
- [Required(AllowEmptyStrings = false)]
- [MaxLength(AuthDbConstants.TenantNameSize)]
- public string TenantName { get; private set; }
- }
-}
\ No newline at end of file
diff --git a/AuthPermissions/DataLayer/Classes/UserDataKey.cs b/AuthPermissions/DataLayer/Classes/UserDataKey.cs
index 8396ce05..41670745 100644
--- a/AuthPermissions/DataLayer/Classes/UserDataKey.cs
+++ b/AuthPermissions/DataLayer/Classes/UserDataKey.cs
@@ -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)]
diff --git a/AuthPermissions/DataLayer/Classes/UserToRole.cs b/AuthPermissions/DataLayer/Classes/UserToRole.cs
index e5d22694..d33b947f 100644
--- a/AuthPermissions/DataLayer/Classes/UserToRole.cs
+++ b/AuthPermissions/DataLayer/Classes/UserToRole.cs
@@ -15,15 +15,14 @@ namespace AuthPermissions.DataLayer.Classes
///
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
@@ -54,7 +53,7 @@ public override string ToString()
public static IStatusGeneric 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));
diff --git a/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs b/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs
index d8c6bda5..b8adc24e 100644
--- a/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs
+++ b/AuthPermissions/DataLayer/EfCode/AuthPermissionsDbContext.cs
@@ -13,13 +13,19 @@ public AuthPermissionsDbContext(DbContextOptions optio
{ }
public DbSet RoleToPermissions { get; set; }
- public DbSet UserTenantKey { get; set; }
+ public DbSet UserDataKey { get; set; }
+ public DbSet Tenants { get; set; }
public DbSet UserToRoles { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("authp");
+ modelBuilder.Entity().HasKey(x => x.TenantId);
+ modelBuilder.Entity()
+ .Property("_parentDataKey")
+ .HasColumnName("ParentDataKey");
+
modelBuilder.Entity()
.HasKey(x => new { x.UserId, x.TenantId, x.RoleName });
diff --git a/AuthPermissions/SetupExtensions.cs b/AuthPermissions/SetupExtensions.cs
index 73657a14..edbbb9ed 100644
--- a/AuthPermissions/SetupExtensions.cs
+++ b/AuthPermissions/SetupExtensions.cs
@@ -76,10 +76,22 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData)
return setupData;
}
- //public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText)
- //{
- // return setupData;
- //}
+ ///
+ /// This allows you to define the name of each tenant by name
+ /// If you are using a hierarchical tenant design, then the whole
+ ///
+ ///
+ /// 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
+ ///
+ ///
+ public static AuthSetupData AddTenantsIfEmpty(this AuthSetupData setupData, string linesOfText)
+ {
+ setupData.Options.UserTenantSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText));
+ return setupData;
+ }
///
/// This allows you to add Roles with their permissions, but only if the auth database contains NO RoleToPermissions
@@ -93,13 +105,13 @@ public static AuthSetupData UsingInMemoryDatabase(this AuthSetupData setupData)
/// AuthSetupData
public static AuthSetupData AddRolesPermissionsIfEmpty(this AuthSetupData setupData, string linesOfText)
{
- setupData.Options.RolesPermissionsSetupText = linesOfText;
+ setupData.Options.RolesPermissionsSetupText = linesOfText ?? throw new ArgumentNullException(nameof(linesOfText));
return setupData;
}
///
/// 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 parameter must contain a list of userId+roles.
+ /// The parameter must contain a list of userId+roles.
///
///
/// A list of containing the information on users and what auth roles they have.
diff --git a/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs b/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs
index 9094b1d3..ad32185f 100644
--- a/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs
+++ b/AuthPermissions/SetupParts/Internal/CommaDelimitedHandler.cs
@@ -8,12 +8,17 @@ namespace AuthPermissions.SetupParts.Internal
{
internal static class CommaDelimitedHandler
{
- public static List DecodeCheckCommaDelimitedString(this string line, int charNum, Action checkValid)
+ public static List DecodeCodeNameWithTrimming(this string line, int charNum, Action checkValid)
{
var trimmedNames = new List();
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]))
diff --git a/AuthPermissions/SetupParts/Internal/SetupRolesService.cs b/AuthPermissions/SetupParts/Internal/SetupRolesService.cs
index 42102458..55f65c04 100644
--- a/AuthPermissions/SetupParts/Internal/SetupRolesService.cs
+++ b/AuthPermissions/SetupParts/Internal/SetupRolesService.cs
@@ -35,14 +35,18 @@ public IStatusGeneric AddRolesToDatabaseIfEmpty(string linesOfText, Type enumPer
return status;
}
- //https://stackoverflow.com/questions/45758587/how-can-i-turn-a-multi-line-string-into-an-array-where-each-element-is-a-line-of
- var lines = linesOfText.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
+ var lines = linesOfText.Split( Environment.NewLine);
for (int i = 0; i < lines.Length; i++)
{
+ if (string.IsNullOrWhiteSpace(lines[i]))
+ continue;
status.CombineStatuses(DecodeLineAndAddToDb(lines[i], i, enumPermissionType));
}
+ if (status.IsValid)
+ _context.SaveChanges();
+
status.Message = $"Added {lines.Length} new RoleToPermissions to the auth database"; //If there is an error this message is removed
return status;
}
@@ -81,7 +85,7 @@ private IStatusGeneric DecodeLineAndAddToDb(string line, int lineNum, Type enumP
charNum = indexColon + 2;
}
- var validPermissionNames = line.DecodeCheckCommaDelimitedString(charNum,
+ var validPermissionNames = line.DecodeCodeNameWithTrimming(charNum,
(name, startOfName) =>
{
if (!enumPermissionType.PermissionsNameIsValid(name))
diff --git a/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs b/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs
new file mode 100644
index 00000000..e7e5ba8d
--- /dev/null
+++ b/AuthPermissions/SetupParts/Internal/SetupTenantsService.cs
@@ -0,0 +1,175 @@
+// 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.Data.Common;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using AuthPermissions.DataLayer.Classes;
+using AuthPermissions.DataLayer.EfCode;
+using AuthPermissions.TenantParts;
+using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
+using Microsoft.EntityFrameworkCore.ValueGeneration;
+using StatusGeneric;
+
+namespace AuthPermissions.SetupParts.Internal
+{
+ internal class SetupTenantsService
+ {
+ private readonly AuthPermissionsDbContext _context;
+
+ public SetupTenantsService(AuthPermissionsDbContext context)
+ {
+ _context = context;
+ }
+
+ public async Task AddTenantsToDatabaseIfEmptyAsync(string linesOfText, AuthPermissionsOptions options)
+ {
+ var status = new StatusGenericHandler();
+
+ //Check the options are set
+ if (options.TenantType == TenantTypes.NotUsingTenants)
+ return status.AddError(
+ $"You must set the options {nameof(AuthPermissionsOptions.TenantType)} to allow tenants to be processed");
+
+ if (_context.Tenants.Any())
+ {
+ status.Message =
+ "There were already Tenants in the auth database, so didn't add these tenants";
+ return status;
+ }
+
+ var lines = linesOfText.Split( Environment.NewLine);
+
+ //Check for duplicate tenant names
+ var dups = lines.GroupBy(line => line).Where(name => name.Count() > 1).ToList();
+ if (dups.Any())
+ return status.AddError("There were tenants with duplicate names, they are: " + string.Join(Environment.NewLine, dups.Select(x => x.Key)));
+
+ if (options.TenantType == TenantTypes.SingleTenant)
+ {
+ foreach (var line in lines)
+ {
+ _context.Add(new Tenant(line.Trim()));
+ }
+
+ await _context.SaveChangesAsync();
+ }
+ else //hierarchical
+ {
+ //This decodes the hierarchical tenants
+ var entries = new List();
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (string.IsNullOrWhiteSpace(lines[i]))
+ continue;
+ entries.Add(new TenantNameDecoded(lines[i], i));
+ }
+
+ var tenantLookup = new Dictionary();
+ //This creates a group with the higher levels first
+ var groupByLayers = entries.GroupBy(x => x.TenantNamesInOrder.Count);
+
+ //This uses a transactions because its going to be calling SaveChanges for each layer
+ using(var transaction = await _context.Database.BeginTransactionAsync())
+ {
+ //This will save a layer, so that the next layer down can be saved
+ foreach (var groupByLayer in groupByLayers)
+ {
+ var tenantsToAddToDb = new List();
+ foreach (var tenantNameDecoded in groupByLayer)
+ {
+ Tenant parent = null;
+ if (tenantNameDecoded.ParentFullName != null)
+ {
+ if (!tenantLookup.TryGetValue(tenantNameDecoded.ParentFullName, out parent))
+ status.AddError(
+ $"The tenant {tenantNameDecoded.TenantFullName} on line {tenantNameDecoded.LineNum} parent {tenantNameDecoded.ParentFullName} was not found");
+ }
+
+ if (tenantLookup.ContainsKey(tenantNameDecoded.TenantFullName))
+ status.AddError(
+ $"The tenant {tenantNameDecoded.TenantFullName} on line {tenantNameDecoded.LineNum} is a duplicate of the same name defined earlier");
+ var newTenant = Tenant.SetupHierarchicalTenant(tenantNameDecoded.TenantFullName, parent);
+ tenantsToAddToDb.Add(newTenant);
+ tenantLookup[tenantNameDecoded.TenantFullName] = newTenant;
+ }
+ if (status.IsValid)
+ {
+ //we add all the tenants in this layer
+ _context.AddRange(tenantsToAddToDb);
+ await _context.SaveChangesAsync();
+ }
+ }
+ if (status.IsValid)
+ transaction.Commit();
+ }
+ }
+
+ return status;
+ }
+
+ //-----------------------------------------------------------
+ //private parts
+
+ private class TenantNameDecoded
+ {
+ public TenantNameDecoded(string line, int lineNum)
+ {
+ DecodeNamesDelimitedBy(line, '|');
+ LineNum = lineNum;
+
+ if (!TenantNamesInOrder.Any())
+ throw new InvalidOperationException($"line {lineNum} produced no tenant names");
+
+ ParentFullName = TenantNamesInOrder.Count > 1
+ ? CombineName(TenantNamesInOrder.Take(TenantNamesInOrder.Count - 1))
+ : (string)null;
+ TenantFullName = CombineName(TenantNamesInOrder);
+ }
+
+ public List TenantNamesInOrder { get; } = new List();
+ public List TenantNameStartCharNum { get; } = new List();
+
+ public int LineNum { get; }
+
+ public string ParentFullName { get; }
+ public string TenantFullName { get; }
+
+ private string CombineName(IEnumerable names)
+ {
+ return string.Join(" | ", names);
+ }
+
+ private void DecodeNamesDelimitedBy(string line, char delimiterChar)
+ {
+ var charNum = 0;
+ while (charNum < line.Length)
+ {
+ if (line[charNum] == ' ')
+ {
+ charNum++;
+ continue;
+ }
+
+ var foundName = "";
+ var startOfName = charNum;
+ while (charNum < line.Length && line[charNum] != delimiterChar)
+ {
+ foundName += line[charNum];
+ charNum++;
+ }
+ if (foundName.Length > 0)
+ {
+ TenantNamesInOrder.Add(foundName.TrimEnd());
+ TenantNameStartCharNum.Add( startOfName);
+ }
+ charNum++;
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/AuthPermissions/SetupParts/Internal/SetupUsersService.cs b/AuthPermissions/SetupParts/Internal/SetupUsersService.cs
index be19e28d..cd73fa9a 100644
--- a/AuthPermissions/SetupParts/Internal/SetupUsersService.cs
+++ b/AuthPermissions/SetupParts/Internal/SetupUsersService.cs
@@ -55,7 +55,7 @@ private async Task CreateUserTenantAndAddToDbAsync(DefineUserWit
var status = new StatusGenericHandler();
var rolesToPermissions = new List();
- userDefine.RoleNamesCommaDelimited.DecodeCheckCommaDelimitedString(0,
+ userDefine.RoleNamesCommaDelimited.DecodeCodeNameWithTrimming(0,
(name, startOfName) =>
{
var roleToPermission = _context.RoleToPermissions.SingleOrDefault(x => x.RoleName == name);
diff --git a/AuthPermissions/TenantParts/TenantTypes.cs b/AuthPermissions/TenantParts/TenantTypes.cs
new file mode 100644
index 00000000..bce2c4c7
--- /dev/null
+++ b/AuthPermissions/TenantParts/TenantTypes.cs
@@ -0,0 +1,26 @@
+// 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.
+
+namespace AuthPermissions.TenantParts
+{
+ ///
+ /// This defines the types of tenant the AuthPermissions can handle
+ ///
+ public enum TenantTypes
+ {
+ ///
+ /// Usage of tenants are turned off
+ ///
+ NotUsingTenants,
+ ///
+ /// Multi-tenant with one level only, e.g. a company has different departments: sales, finance, HR etc.
+ /// A User can only be in one of these levels
+ ///
+ SingleTenant,
+ ///
+ /// Multi-tenant many levels, e.g. Holding company -> USA branch -> East Coast -> New York
+ /// A User at the USA branch has read/write access to the USA branch data, read-only access to the East Coast and all its subsidiaries
+ ///
+ HierarchicalTenant
+ }
+}
\ No newline at end of file
diff --git a/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs b/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
index 053eac89..31c25814 100644
--- a/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
+++ b/Example1.RazorPages.IndividualAccounts/Data/Migrations/00000000000000_CreateIdentitySchema.Designer.cs
@@ -6,7 +6,6 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System;
-using Example1.RazorPages.IndividualAccounts.Data;
namespace Example1.RazorPages.IndividualAccounts.Data.Migrations
{
diff --git a/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs
index 47215a2b..c39e7dc2 100644
--- a/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/Example1.RazorPages.IndividualAccounts/Data/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -5,7 +5,6 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System;
-using Example1.RazorPages.IndividualAccounts.Data;
namespace Example1.RazorPages.IndividualAccounts.Data.Migrations
{
diff --git a/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs b/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs
new file mode 100644
index 00000000..7a73f588
--- /dev/null
+++ b/Test/UnitTests/TestAuthPermissions/TestSetupPartsSetupTenantService.cs
@@ -0,0 +1,171 @@
+// 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.Linq;
+using System.Runtime.InteropServices.ComTypes;
+using System.Threading.Tasks;
+using AuthPermissions;
+using AuthPermissions.DataLayer.EfCode;
+using AuthPermissions.SetupParts.Internal;
+using AuthPermissions.TenantParts;
+using TestSupport.EfHelpers;
+using Xunit;
+using Xunit.Abstractions;
+using Xunit.Extensions.AssertExtensions;
+
+namespace Test.UnitTests.TestAuthPermissions
+{
+ public class TestSetupPartsSetupTenantService
+ {
+ private readonly ITestOutputHelper _output;
+
+ public TestSetupPartsSetupTenantService(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public async Task TestAddTenantsToDatabaseIfEmptyBadOptions()
+ {
+ //SETUP
+ var options = SqliteInMemory.CreateOptions();
+ using var context = new AuthPermissionsDbContext(options);
+ context.Database.EnsureCreated();
+
+ var service = new SetupTenantsService(context);
+
+ //ATTEMPT
+ var status = await service.AddTenantsToDatabaseIfEmptyAsync("", new AuthPermissionsOptions());
+
+ //VERIFY
+ status.IsValid.ShouldBeFalse();
+ status.GetAllErrors()
+ .ShouldEqual(
+ $"You must set the options {nameof(AuthPermissionsOptions.TenantType)} to allow tenants to be processed");
+ }
+
+ [Fact]
+ public async Task TestAddTenantsToDatabaseIfEmptySingleTenant()
+ {
+ //SETUP
+ var options = SqliteInMemory.CreateOptions();
+ using var context = new AuthPermissionsDbContext(options);
+ context.Database.EnsureCreated();
+
+ var service = new SetupTenantsService(context);
+ var authOptions = new AuthPermissionsOptions
+ {
+ TenantType = TenantTypes.SingleTenant
+ };
+ var lines = @"Tenant1
+Tenant2
+Tenant3";
+
+ //ATTEMPT
+ var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions);
+
+ //VERIFY
+ status.IsValid.ShouldBeTrue(status.GetAllErrors());
+ context.ChangeTracker.Clear();
+ context.Tenants.Count().ShouldEqual(3);
+ }
+
+ [Fact]
+ public async Task TestAddTenantsToDatabaseIfEmptySingleTenantDuplicate()
+ {
+ //SETUP
+ var options = SqliteInMemory.CreateOptions();
+ using var context = new AuthPermissionsDbContext(options);
+ context.Database.EnsureCreated();
+
+ var service = new SetupTenantsService(context);
+ var authOptions = new AuthPermissionsOptions
+ {
+ TenantType = TenantTypes.SingleTenant
+ };
+ var lines = @"Tenant1
+Tenant2
+Tenant2
+Tenant3
+Tenant3";
+
+ //ATTEMPT
+ var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions);
+
+ //VERIFY
+ status.IsValid.ShouldBeFalse(status.GetAllErrors());
+ status.GetAllErrors()
+ .ShouldEqual($"There were tenants with duplicate names, they are: Tenant2{Environment.NewLine}Tenant3");
+ }
+
+ [Fact]
+ public async Task TestAddTenantsToDatabaseIfEmptyHierarchicalTenant()
+ {
+ //SETUP
+ var options = SqliteInMemory.CreateOptions();
+ using var context = new AuthPermissionsDbContext(options);
+ context.Database.EnsureCreated();
+
+ var service = new SetupTenantsService(context);
+ var authOptions = new AuthPermissionsOptions
+ {
+ TenantType = TenantTypes.HierarchicalTenant
+ };
+ var lines = @"Company
+Company | West Coast |
+Company | West Coast | SanFran
+Company | West Coast | SanFran | Shop1
+Company | West Coast | SanFran | Shop2
+Company | West Coast | LA
+Company | West Coast | LA | Shop1
+Company | West Coast | LA | Shop2";
+
+ //ATTEMPT
+ var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions);
+
+ //VERIFY
+ status.IsValid.ShouldBeTrue(status.GetAllErrors());
+ context.ChangeTracker.Clear();
+ var tenants = context.Tenants.ToList();
+ foreach (var tenant in tenants)
+ {
+ _output.WriteLine(tenant.ToString());
+ }
+ context.Tenants.Count().ShouldEqual(8);
+ }
+
+ [Fact]
+ public async Task TestAddTenantsToDatabaseIfEmptyHierarchicalTenantBadName()
+ {
+ //SETUP
+ var options = SqliteInMemory.CreateOptions();
+ using var context = new AuthPermissionsDbContext(options);
+ context.Database.EnsureCreated();
+
+ var service = new SetupTenantsService(context);
+ var authOptions = new AuthPermissionsOptions
+ {
+ TenantType = TenantTypes.HierarchicalTenant
+ };
+ var lines = @"Company
+Company | West Coast |
+Company | West Coast | San???
+Company | XX Coast | SanFran | Shop1
+Company | West Coast | SanFran | Shop2
+Company | West Coast | LA
+Company | YY Coast | LA | Shop1
+Company | West Coast | LA | Shop2";
+
+ //ATTEMPT
+ var status = await service.AddTenantsToDatabaseIfEmptyAsync(lines, authOptions);
+
+ //VERIFY
+ status.IsValid.ShouldBeFalse();
+ status.Errors.Count.ShouldEqual(3);
+ status.Errors[0].ToString().ShouldEqual("The tenant Company | XX Coast | SanFran | Shop1 on line 3 parent Company | XX Coast | SanFran was not found");
+ status.Errors[1].ToString().ShouldEqual("The tenant Company | West Coast | SanFran | Shop2 on line 4 parent Company | West Coast | SanFran was not found");
+ status.Errors[2].ToString().ShouldEqual("The tenant Company | YY Coast | LA | Shop1 on line 6 parent Company | YY Coast | LA was not found");
+ }
+ }
+}
\ No newline at end of file