BusinessMgr
This commit is contained in:
@@ -135,7 +135,7 @@ namespace Sacs.Web.Components
|
||||
|
||||
// Serialise parameters as if in a URL query string - so that they can be deserialised in DecryptParameters() below.
|
||||
// Start with a random length parameter name at start of list to obscure the encryption.
|
||||
string queryString = SacsRandomHelper.getRandomLengthNumber(4) + "&";
|
||||
string queryString = SacsRandomHelper.GetRandomLengthNumber(4) + "&";
|
||||
queryString += String.Join("&", paramKeys.Select(a => a + "=" + HttpUtility.UrlEncode(parametersToEncrypt[a])));
|
||||
|
||||
// Encrypt data.
|
||||
@@ -286,7 +286,7 @@ namespace Sacs.Web.Components
|
||||
ConfirmMessage = new ConfirmMessageData() { Title = title, Text = text, Actions = actions };
|
||||
return this;
|
||||
}
|
||||
public bool HasConfirmMessage { get { return ConfirmMessage != null; } }
|
||||
public bool? HasConfirmMessage { get { return ConfirmMessage != null ? true : null; } }
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.20" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.20" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.8.1" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2622" />
|
||||
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2662" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -43,11 +43,16 @@ namespace Sarand.Net.Standard.Base.Bus
|
||||
//-------------------------------------------------------------------
|
||||
// Properties.
|
||||
|
||||
/// <summary>OK - No error state message.</summary>
|
||||
public bool OK { get { return !HasErrorState(); } }
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// E r r o r S t a t e
|
||||
// Error State.
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
private SacsErrorState errorState = null;
|
||||
public bool HasErrorState ()
|
||||
|
||||
public bool HasErrorState()
|
||||
{
|
||||
return errorState != null;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -111,6 +112,12 @@ namespace Sarand.Net.Standard.Base.Bus
|
||||
return (IsValid(sysId) ? sysId.id.ToString() : invalidValue);
|
||||
}
|
||||
|
||||
// From String.
|
||||
public static SysId ToSysId(String value)
|
||||
{
|
||||
return new SysId(StringUtil.ToLong(value));
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>
|
||||
/// JSON Converter to serialise/deserialise SysId to/from simple integer value.
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Sarand.Net.Standard.Base.Dbas
|
||||
{
|
||||
//=======================================================================
|
||||
public interface IDbDataTable
|
||||
{
|
||||
}
|
||||
|
||||
//=======================================================================
|
||||
}
|
||||
@@ -68,27 +68,39 @@ namespace Sarand.Net.Standard.Base.Dbas
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Database schema.
|
||||
List<String> GetTableNames (bool refresh = false);
|
||||
List<DbDfnTable> GetTableDfns (bool refresh = false);
|
||||
DbDfnTable GetTableDfn (String name, bool refresh = false);
|
||||
bool TableExists (String tableName);
|
||||
bool ColumnExists (String tableName, String columnName);
|
||||
bool IndexExists (String tableName, String indexName);
|
||||
List<String> GetTableNames(bool refresh = false);
|
||||
List<DbDfnTable> GetTableDfns(bool refresh = false);
|
||||
DbDfnTable GetTableDfn(String name, bool refresh = false);
|
||||
bool TableExists(String tableName);
|
||||
bool ColumnExists(String tableName, String columnName);
|
||||
bool IndexExists(String tableName, String indexName);
|
||||
// Database info - schema summary.
|
||||
IDatabaseInfo CreateDbInfo();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
public enum PasswordHashMethod { MsSql }
|
||||
/// <summary>Generate password hash using the specified method.</summary>
|
||||
String EncodePassword_Hex(String password, PasswordHashMethod method = PasswordHashMethod.MsSql);
|
||||
/// <summary>Compare password to hashed value using the specified method.</summary>
|
||||
bool ComparePassword_Hex(String password, String passwordHashHex, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Query records.
|
||||
String GetSelectQueryPrefix ();
|
||||
IDbDataTable ExecuteQuery (String query);
|
||||
int ExecuteNonQuery (String query);
|
||||
int ExecuteInsert (String tableName, List<StringPair> columnNameValues);
|
||||
int ExecuteUpdate (String tableName, List<StringPair> columnNameValues, String filter);
|
||||
int ExecuteDelete (String tableName, String filter);
|
||||
String GetSelectQueryPrefix();
|
||||
/// <summary>
|
||||
/// Execute query directly against database.
|
||||
/// <para>- NB Check for null response and error state.</para>
|
||||
/// </summary>
|
||||
DbResultSet ExecuteQuery(String query);
|
||||
int ExecuteNonQuery(String query);
|
||||
int ExecuteInsert(String tableName, List<StringPair> columnNameValues);
|
||||
int ExecuteUpdate(String tableName, List<StringPair> columnNameValues, String filter);
|
||||
int ExecuteDelete(String tableName, String filter);
|
||||
// Specific queries.
|
||||
long GetRowCount (String tableName, String filter);
|
||||
DataValue GetRowValue (String tableName, String columnName, String filter);
|
||||
DataValue GetRowValue (String tableName, String columnName, String filter, DbGetValueType getValueType);
|
||||
long GetRowCount(String tableName, String filter);
|
||||
DataValue GetRowValue(String tableName, String columnName, String filter);
|
||||
DataValue GetRowValue(String tableName, String columnName, String filter, DbGetValueType getValueType);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Sarand.Net.Standard.Base.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@@ -52,19 +53,27 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
public static String getRandomLengthNumber(ushort maxLength=4)
|
||||
public static String GetRandomLengthNumber(ushort maxLength = 4)
|
||||
{
|
||||
String fn = "getRandomLengthNumber";
|
||||
String value = "";
|
||||
int len = GetInt32(1, maxLength + 1);
|
||||
for (int i = 0; i < len; i++)
|
||||
value += GetInt32(0,10);
|
||||
if (StringUtil.IsEmpty(value))
|
||||
throw new NotSupportedException("getRandomLengthNumber Generated blank value");
|
||||
value += GetInt32(0, 10);
|
||||
if (StringUtil.IsBlank(value))
|
||||
throw new NotSupportedException($"{fn} Generated blank value");
|
||||
if (value.Length > maxLength)
|
||||
throw new NotSupportedException("getRandomLengthNumber Generated excess length value");
|
||||
throw new NotSupportedException($"{fn} Generated excess length value");
|
||||
return value;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>Returns filename in 8.3 format.</summary>
|
||||
public static String GetRandomFileName()
|
||||
{
|
||||
return Path.GetRandomFileName();
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
//=======================================================================
|
||||
|
||||
@@ -21,6 +21,12 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
Group = 3,
|
||||
Right = 4,
|
||||
}
|
||||
// Password storage methods - must be kept up to date with legacy application.
|
||||
public enum PasswordMethod
|
||||
{
|
||||
Legacy = 0,
|
||||
MsSql = 10,
|
||||
}
|
||||
public enum FilterAttrType
|
||||
{
|
||||
Code, // Partial code.
|
||||
@@ -29,14 +35,6 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
Group_Exclude, // Item doesn't belong to group.
|
||||
};
|
||||
}
|
||||
// Password configuration.
|
||||
public class PasswordConfig
|
||||
{
|
||||
/// <summary>Minimum password length. < 1 = no limit.</summary>
|
||||
public int MinimumLength { get; protected set; } = 6;
|
||||
/// <summary>Maximum password length. < 1 = no limit.</summary>
|
||||
public int MaximumLength { get; protected set; } = 6;
|
||||
}
|
||||
|
||||
//=======================================================================
|
||||
public abstract class SaSecurityConsole : SaErrorStateItem
|
||||
@@ -58,8 +56,8 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Data members.
|
||||
protected static String GroupSuperCode = "SUPER";
|
||||
protected static String AdminUserCode = "admin";
|
||||
public static String GroupCode_Super = "SUPER";
|
||||
public static String UserCode_Admin = "admin";
|
||||
private static List<TypeCodeDesc> securityItemTypes = new List<TypeCodeDesc>();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -116,6 +114,12 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
return userIdDefault;
|
||||
}
|
||||
|
||||
protected void ClearRegisteredSecurityRights()
|
||||
{
|
||||
RegisteredRights.Clear();
|
||||
DataItemDfns.Clear();
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// L o g i n
|
||||
//-------------------------------------------------------------------
|
||||
@@ -140,9 +144,28 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
/// <summary>Security Group modified event.</summary>
|
||||
public abstract bool OnSecurityGroupModified(SysId userId);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
//-------------------------------------------------------------------
|
||||
// Password configuration.
|
||||
public PasswordConfig PasswordConfig { get; protected set; } = new PasswordConfig();
|
||||
public class SaPasswordConfig
|
||||
{
|
||||
/// <summary>Minimum password length. < 1 = no limit.</summary>
|
||||
public int MinimumLength { get; protected set; } = 6;
|
||||
/// <summary>Maximum password length. < 1 = no limit.</summary>
|
||||
public int MaximumLength { get; protected set; } = 6;
|
||||
}
|
||||
public SaPasswordConfig PasswordConfig { get; protected set; } = new SaPasswordConfig();
|
||||
|
||||
/// <summary>Store the password against against the user.</summary>
|
||||
public abstract bool SetPassword(SaSecurityItem securityItemUser, String password);
|
||||
|
||||
/// <summary>Does the password match that currently defined against the user.</summary>
|
||||
public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password);
|
||||
|
||||
/// <summary>Is the password currently defined against the user blank.</summary>
|
||||
public abstract bool PasswordIsBlank(SaSecurityItem securityItemUser);
|
||||
|
||||
/// <summary>Show form to change password for currently logged in user.</summary>
|
||||
/// <returns>Flag to indicate password changed</returns>
|
||||
public abstract bool ShowChangePasswordForm(DbUpdateInfo dbUpdateInfo);
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
//=======================================================================
|
||||
public abstract class SaSecurityItem : SacsHierarchyItem
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// Definitions.
|
||||
// Password Salt constants - must be kept up to date with legacy application.
|
||||
private static String PasswordSalt_MsSql = "MsSql";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Columns.
|
||||
public enum Column
|
||||
@@ -67,7 +72,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
// Data members.
|
||||
private readonly DataProperties data = new DataProperties();
|
||||
// Construction.
|
||||
public Data ()
|
||||
public Data()
|
||||
{
|
||||
Status = DefaultStatus;
|
||||
}
|
||||
@@ -161,7 +166,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
set { SetFlag(Flag_AuthenticatedUser, value); }
|
||||
}
|
||||
// Population.
|
||||
internal void PopulateFrom (Data data)
|
||||
internal void PopulateFrom(Data data)
|
||||
{
|
||||
ItemType = data.ItemType;
|
||||
Code = data.Code;
|
||||
@@ -182,7 +187,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
InvalidLoginCount = data.InvalidLoginCount;
|
||||
base.PopulateFrom(data);
|
||||
}
|
||||
public Data Clone ()
|
||||
public Data Clone()
|
||||
{
|
||||
Data data = new Data();
|
||||
data.PopulateFrom(this);
|
||||
@@ -219,7 +224,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Construction.
|
||||
public SaSecurityItem (SysId id, SaSecurity.ItemType securityItemType) :
|
||||
public SaSecurityItem(SysId id, SaSecurity.ItemType securityItemType) :
|
||||
base(id)
|
||||
{
|
||||
base.SetData(data);
|
||||
@@ -238,7 +243,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
public static SaStatus EnabledStatus { get { return SaStatus.Enabled; } }
|
||||
public static SaStatus DisabledStatus { get { return SaStatus.Disabled; } }
|
||||
public static List<SaStatus> ValidStatuses { get { return new List<SaStatus>(securityItemStatuses); } }
|
||||
public static DataValue.TriStateType GetStatusTriState (SaStatus status)
|
||||
public static DataValue.TriStateType GetStatusTriState(SaStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
@@ -254,8 +259,8 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Data properties.
|
||||
public Data GetData () { return data.Clone(); }
|
||||
public void SetData (Data data)
|
||||
public Data GetData() { return data.Clone(); }
|
||||
public void SetData(Data data)
|
||||
{
|
||||
// Properties that cannot be changed if previously specified.
|
||||
if (SysId.IsValid(Id) && SysId.ToInt(Id) != SysId.ToInt(data.Id))
|
||||
@@ -359,68 +364,31 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
private static readonly Encoding Encoding_Ansi1252 = Encoding.GetEncoding(1252);
|
||||
public bool IsPasswordBlank ()
|
||||
{
|
||||
return EncryptPassword("").Equals(PasswordEncrypted);
|
||||
}
|
||||
public bool PasswordMatches (String password)
|
||||
{
|
||||
return PasswordEncrypted.Equals(EncryptPassword(password));
|
||||
}
|
||||
public static String EncryptPassword (String password)
|
||||
{
|
||||
// We know this is not very water-tight, but it at least
|
||||
// hides the contents of the password from prying eyes.
|
||||
String toBeEncrypted = password;
|
||||
// Password length goes in as first character:
|
||||
toBeEncrypted = ByteToString((byte)(128 - password.Length)) + toBeEncrypted;
|
||||
toBeEncrypted = string.Concat(toBeEncrypted.Reverse());
|
||||
// Pad out to fixed length (minimum 16, multiples of 8):
|
||||
int fixedLength = 16;
|
||||
if (password.Length / 8 > 2) fixedLength = 8 * (int)(password.Length / 8);
|
||||
while (toBeEncrypted.Length < fixedLength)
|
||||
{
|
||||
char charToAdd = (char)(password.Length > 0 ? password[(toBeEncrypted.Length % password.Length)] : 'A' + (toBeEncrypted.Length * 13) % 23);
|
||||
if (toBeEncrypted.Length % 2 != 0)
|
||||
toBeEncrypted += charToAdd;
|
||||
else toBeEncrypted = charToAdd + toBeEncrypted;
|
||||
}
|
||||
|
||||
// Now perform a simple algorythm on each character to hide its true value:
|
||||
byte[] encryptedChars = new byte[toBeEncrypted.Length];
|
||||
for (int i = 0; i < toBeEncrypted.Length; i++)
|
||||
{
|
||||
int iData = toBeEncrypted[i];
|
||||
iData += 17 + i;
|
||||
if (iData > 255) iData -= (255 + 32);
|
||||
encryptedChars[i] = (byte)iData;
|
||||
}
|
||||
String result = Encoding_Ansi1252.GetString(encryptedChars);
|
||||
#if DEBUG
|
||||
// Password: mgbgt
|
||||
// To encrypt: tbmggtgbgm{btggm
|
||||
// Encrypted: 133, 116, 128, 123, 124, 138, 126, 122, 128, 135, 150, 126, 145, 133, 134, 141
|
||||
//Logger.Info(fn + "\n\tPassword: " + password + "\n\tTo encrypt: " + toBeEncrypted + "\n\tEncrypted: " + encryptedChars.Select(i => i.ToString()).ToList().Concat(", "));
|
||||
//Logger.Debug(password + ": " + result);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
private byte[] PasswordStringToBytes (String value)
|
||||
/// <summary>Extract password storage method from data.</summary>
|
||||
public SaSecurity.PasswordMethod GetPasswordMethod()
|
||||
{
|
||||
byte[] values = new byte[value.Length];
|
||||
for (int idx = 0; idx < value.Length; idx++)
|
||||
{
|
||||
char c = value[idx];
|
||||
UInt32 ui = Convert.ToUInt32(c);
|
||||
SaLogger.Info(idx + ". " + ui.ToString("x8"));
|
||||
values[idx] = (byte)(c);
|
||||
}
|
||||
return values;
|
||||
return SaSecurity.PasswordMethod.Legacy;
|
||||
}
|
||||
private static String ByteToString (byte value)
|
||||
|
||||
public void SetEncodedPassword(SaSecurity.PasswordMethod method, String encodedPassword)
|
||||
{
|
||||
return Encoding_Ansi1252.GetString(new[] { value });
|
||||
if (method == SaSecurity.PasswordMethod.MsSql)
|
||||
{
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
PasswordEncrypted = "";
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
PasswordSalt = PasswordSalt_MsSql;
|
||||
PasswordHash = encodedPassword;
|
||||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
PasswordEncrypted = encodedPassword;
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
PasswordSalt = "";
|
||||
PasswordHash = "";
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -455,7 +423,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
//=======================================================================
|
||||
public static class SaSecurityItemList
|
||||
{
|
||||
public static SaSecurityItem GetSecItemById (this List<SaSecurityItem> secItems, SysId secItemId)
|
||||
public static SaSecurityItem GetSecItemById(this List<SaSecurityItem> secItems, SysId secItemId)
|
||||
{
|
||||
if (!SysId.IsValid(secItemId)) return null;
|
||||
foreach (SaSecurityItem secItem in secItems)
|
||||
|
||||
@@ -31,10 +31,19 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
// Database properties.
|
||||
public abstract bool DatabaseIsValid { get; }
|
||||
public abstract bool CanUpdateDatabase { get; }
|
||||
public abstract bool ColumnExists (SaSecurityDbDfn.ColumnIdx columnIdx);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Utilities.
|
||||
public abstract bool ColumnExists (SaSecurityDbDfn.ColumnIdx columnIdx);
|
||||
// Password utilities.
|
||||
|
||||
/// <summary>Store the password against against the user.</summary>
|
||||
public abstract bool SetPassword(SaSecurityItem securityItemUser, String password);
|
||||
|
||||
/// <summary>Does the password match that currently defined against the user.</summary>
|
||||
public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password);
|
||||
|
||||
/// <summary>Is the password currently defined against the user blank.</summary>
|
||||
public abstract bool PasswordIsBlank(SaSecurityItem securityItemUser);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
|
||||
@@ -10,40 +10,52 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
public class SaSecurityProfile
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>Security Group definition.</summary>
|
||||
public class GroupData
|
||||
{
|
||||
public readonly List<SaSecurityProfile.RightData> rights = new List<RightData>();
|
||||
public GroupData (SaSecurityItem secGroup, List<SaSecurityProfile.RightData> groupRights)
|
||||
private readonly List<SaSecurityProfile.RightData> rights = new List<RightData>();
|
||||
public GroupData(SaSecurityItem secGroup, List<SaSecurityProfile.RightData> groupRights)
|
||||
{
|
||||
if (secGroup.ItemType != SaSecurity.ItemType.Group) throw new NotSupportedException("SaSecurityProfile.GroupData - Invalid security item type: "+ secGroup.ItemType.ToString());
|
||||
GroupId = secGroup.Id;
|
||||
GroupCode = secGroup.Code;
|
||||
GroupDesc = secGroup.Description;
|
||||
if (secGroup.ItemType != SaSecurity.ItemType.Group) throw new NotSupportedException("SaSecurityProfile.GroupData - Invalid security item type: " + secGroup.ItemType.ToString());
|
||||
Id = secGroup.Id;
|
||||
Code = secGroup.Code;
|
||||
Desc = secGroup.Description;
|
||||
rights.AddRange(groupRights);
|
||||
}
|
||||
protected GroupData () { }
|
||||
public SysId GroupId { get; protected set; }
|
||||
public String GroupCode { get; protected set; }
|
||||
public String GroupDesc { get; protected set; }
|
||||
protected GroupData() { }
|
||||
public SysId Id { get; }
|
||||
public String Code { get; }
|
||||
public String Desc { get; }
|
||||
// Security rights.
|
||||
public List<SaSecurityProfile.RightData> Rights { get { return new List<RightData>(rights); } }
|
||||
/// <summary>
|
||||
/// Retrieve the list of data items for which the user is explicitly granted the
|
||||
/// specified security right, or where the right is explicitly denied if the flag is false.
|
||||
/// </summary>
|
||||
public List<SysId> GetDataItemsForRight(String rightCode, bool granted = true) { return rights.GetDataItemsForRight(rightCode, granted); }
|
||||
/// <summary>Is the Security Right code granted access for the Data item (if specified).</summary>
|
||||
public bool IsSecurityRightGranted(String rightCode, SysId dataItemId = null) { return rights.IsSecurityRightGranted(rightCode, dataItemId); }
|
||||
/// <summary>Is the Security Right code denined access for the Data item (if specified).</summary>
|
||||
public bool IsSecurityRightDenied(String rightCode, SysId dataItemId = null) { return rights.IsSecurityRightDenied(rightCode, dataItemId); }
|
||||
}
|
||||
/// <summary>Security Right definition.</summary>
|
||||
public class RightData
|
||||
{
|
||||
public RightData (SaSecurityItem secRight, SaSecurityLink secRightLink)
|
||||
public RightData(SaSecurityItem secRight, SaSecurityLink secRightLink)
|
||||
{
|
||||
if (secRight.ItemType != SaSecurity.ItemType.Right) throw new NotSupportedException("SaSecurityProfile.RightData - Invalid security item type: " + secRight.ItemType.ToString());
|
||||
RightId = secRight.Id;
|
||||
RightCode = secRight.Code;
|
||||
RightDesc = secRight.Description;
|
||||
Id = secRight.Id;
|
||||
Code = secRight.Code;
|
||||
Desc = secRight.Description;
|
||||
DataItemId = secRightLink.ItemIdContext;
|
||||
Granted = (secRightLink.Status == SaStatus.Granted);
|
||||
}
|
||||
protected RightData () { }
|
||||
public SysId RightId { get; protected set; }
|
||||
public String RightCode { get; protected set; }
|
||||
public String RightDesc { get; protected set; }
|
||||
public SysId DataItemId { get; protected set; }
|
||||
public bool Granted { get; protected set; }
|
||||
protected RightData() { }
|
||||
public SysId Id { get; }
|
||||
public String Code { get; }
|
||||
public String Desc { get; }
|
||||
public SysId DataItemId { get; }
|
||||
public bool Granted { get; }
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -53,7 +65,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Construction.
|
||||
internal SaSecurityProfile (SaSecurityItem secItem, List<SaSecurityProfile.GroupData> profileGroups, List<SaSecurityProfile.RightData> profileRights, bool systemAdmin)
|
||||
internal SaSecurityProfile(SaSecurityItem secItem, List<SaSecurityProfile.GroupData> profileGroups, List<SaSecurityProfile.RightData> profileRights, bool systemAdmin)
|
||||
{
|
||||
Clear();
|
||||
if (secItem != null)
|
||||
@@ -115,9 +127,9 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Utilities.
|
||||
public bool IsMemberOfSecurityGroup (SysId secGroupId)
|
||||
public bool IsMemberOfSecurityGroup(SysId secGroupId)
|
||||
{
|
||||
return securityGroups.Exists(item => item.GroupId.Equals(secGroupId));
|
||||
return securityGroups.Exists(item => item.Id.Equals(secGroupId));
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -125,14 +137,14 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
/// Retrieve the list of data items for which the user is explicitly granted the
|
||||
/// specified security right, or where the right is explicitly denied if the flag is false.
|
||||
/// </summary>
|
||||
public List<SysId> GetDataItemsForRight (String rightCode, bool granted = true)
|
||||
public List<SysId> GetDataItemsForRight(String rightCode, bool granted = true)
|
||||
{
|
||||
return securityGroups.GetDataItemsForRight(rightCode, granted);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>Is the Security Right code granted access for the Data item (if specified).</summary>
|
||||
public bool IsSecurityRightGranted (String rightCode, SysId dataItemId = null)
|
||||
public bool IsSecurityRightGranted(String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
if (SystemAdmin) return true;
|
||||
return (securityGroups.IsSecurityRightGranted(rightCode, dataItemId) || securityRights.IsSecurityRightGranted(rightCode, dataItemId));
|
||||
@@ -140,7 +152,7 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>Is the Security Right code denined access for the Data item (if specified).</summary>
|
||||
public bool IsSecurityRightDenied (String rightCode, SysId dataItemId = null)
|
||||
public bool IsSecurityRightDenied(String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
if (SystemAdmin) return false;
|
||||
return (securityGroups.IsSecurityRightDenied(rightCode, dataItemId) || securityRights.IsSecurityRightDenied(rightCode, dataItemId));
|
||||
@@ -156,22 +168,22 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
/// Retrieve the list of data items for which the user is explicitly granted the
|
||||
/// specified security right, or where the right is explicitly denied if the flag is false.
|
||||
/// </summary>
|
||||
public static List<SysId> GetDataItemsForRight (this List<SaSecurityProfile.GroupData> groups, String rightCode, bool granted = true)
|
||||
public static List<SysId> GetDataItemsForRight(this List<SaSecurityProfile.GroupData> groups, String rightCode, bool granted = true)
|
||||
{
|
||||
List<SysId> dataItemIds = new List<SysId>();
|
||||
HashSet<SysId> dataItemIds = new HashSet<SysId>();
|
||||
foreach (var securityGroup in groups)
|
||||
dataItemIds.AddIds(securityGroup.rights.GetDataItemsForRight(rightCode,granted));
|
||||
return dataItemIds;
|
||||
dataItemIds.AddIds(securityGroup.GetDataItemsForRight(rightCode, granted));
|
||||
return new List<SysId>(dataItemIds);
|
||||
}
|
||||
/// <summary>Is the Security Right code granted access for the Data item (if specified).</summary>
|
||||
public static bool IsSecurityRightGranted (this List<SaSecurityProfile.GroupData> groups, String rightCode, SysId dataItemId = null)
|
||||
public static bool IsSecurityRightGranted(this List<SaSecurityProfile.GroupData> groups, String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
return groups.Exists(item => item.rights.IsSecurityRightGranted(rightCode, dataItemId));
|
||||
return groups.Exists(group => group.IsSecurityRightGranted(rightCode, dataItemId));
|
||||
}
|
||||
/// <summary>Is the Security Right code denined access for the Data item (if specified).</summary>
|
||||
public static bool IsSecurityRightDenied (this List<SaSecurityProfile.GroupData> groups, String rightCode, SysId dataItemId = null)
|
||||
public static bool IsSecurityRightDenied(this List<SaSecurityProfile.GroupData> groups, String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
return groups.Exists(item => item.rights.IsSecurityRightDenied(rightCode, dataItemId));
|
||||
return groups.Exists(group => group.IsSecurityRightDenied(rightCode, dataItemId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,17 +194,17 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
/// Retrieve the list of data items for which the user is explicitly granted the
|
||||
/// specified security right, or where the right is explicitly denied if the flag is false.
|
||||
/// </summary>
|
||||
public static List<SysId> GetDataItemsForRight (this List<SaSecurityProfile.RightData> rights, String rightCode, bool granted = true)
|
||||
public static List<SysId> GetDataItemsForRight(this List<SaSecurityProfile.RightData> rights, String rightCode, bool granted = true)
|
||||
{
|
||||
List<SysId> dataItemIds = new List<SysId>();
|
||||
foreach (var securityRight in rights.FindAll(x => x.RightCode.EqualsCI(rightCode) && x.Granted == granted))
|
||||
HashSet<SysId> dataItemIds = new HashSet<SysId>();
|
||||
foreach (var securityRight in rights.FindAll(x => x.Code.EqualsCI(rightCode) && x.Granted == granted))
|
||||
dataItemIds.AddId(securityRight.DataItemId);
|
||||
return dataItemIds;
|
||||
return new List<SysId>(dataItemIds);
|
||||
}
|
||||
/// <summary>Is the Security Right code granted access for the Data item (if specified).</summary>
|
||||
public static bool IsSecurityRightGranted (this List<SaSecurityProfile.RightData> rights, String rightCode, SysId dataItemId = null)
|
||||
public static bool IsSecurityRightGranted(this List<SaSecurityProfile.RightData> rights, String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
foreach (var userSecurityRight in rights.FindAll(item => item.RightCode.EqualsCI(rightCode)))
|
||||
foreach (var userSecurityRight in rights.FindAll(item => item.Code.EqualsCI(rightCode)))
|
||||
{
|
||||
if (SysId.ToInt(dataItemId) != SysId.ToInt(userSecurityRight.DataItemId)) continue;
|
||||
return userSecurityRight.Granted;
|
||||
@@ -200,9 +212,9 @@ namespace Sarand.Net.Standard.Base.Security
|
||||
return false;
|
||||
}
|
||||
/// <summary>Is the Security Right code denined access for the Data item (if specified).</summary>
|
||||
public static bool IsSecurityRightDenied (this List<SaSecurityProfile.RightData> rights, String rightCode, SysId dataItemId = null)
|
||||
public static bool IsSecurityRightDenied(this List<SaSecurityProfile.RightData> rights, String rightCode, SysId dataItemId = null)
|
||||
{
|
||||
foreach (var userSecurityRight in rights.FindAll(item => item.RightCode.EqualsCI(rightCode)))
|
||||
foreach (var userSecurityRight in rights.FindAll(item => item.Code.EqualsCI(rightCode)))
|
||||
{
|
||||
if (SysId.ToInt(dataItemId) != SysId.ToInt(userSecurityRight.DataItemId)) continue;
|
||||
return !userSecurityRight.Granted;
|
||||
|
||||
@@ -474,14 +474,16 @@ namespace Sarand.Net.Standard.Base.Utils
|
||||
}
|
||||
public static long ToLong(this string value, long defaultValue = 0)
|
||||
{
|
||||
if (String.IsNullOrEmpty(value)) return defaultValue;
|
||||
try
|
||||
if (!String.IsNullOrEmpty(value))
|
||||
{
|
||||
Double returnValue = defaultValue;
|
||||
if (Double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out returnValue))
|
||||
return (Int64)returnValue;
|
||||
try
|
||||
{
|
||||
Double returnValue = defaultValue;
|
||||
if (Double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out returnValue))
|
||||
return (Int64)returnValue;
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
catch (Exception) { }
|
||||
return defaultValue;
|
||||
}
|
||||
public static short ToShort(Object value, short defaultValue = 0)
|
||||
@@ -494,22 +496,17 @@ namespace Sarand.Net.Standard.Base.Utils
|
||||
}
|
||||
public static long ToLong(Object value, long defaultValue = 0)
|
||||
{
|
||||
if (value == null) return defaultValue;
|
||||
try
|
||||
if (value != null)
|
||||
{
|
||||
return ToLong(value.ToString());
|
||||
try
|
||||
{
|
||||
return ToLong(value.ToString(), defaultValue);
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
catch (Exception) { }
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// SysId.
|
||||
public static SysId ToSysId(this String value)
|
||||
{
|
||||
return new SysId(StringUtil.ToLong(value));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
// Hex values to integer.
|
||||
/// <summary>Convert hex character to integer value.</summary>
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="19.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="ZstdNet" Version="1.4.5" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -61,6 +61,83 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
public abstract int GetNextIdFromTable(string objectName, string idColumn = "Id");
|
||||
public abstract bool SetMinNextId(String objectName, SysId id, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id", bool force = false);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>
|
||||
/// Generate password hash using the specified method.
|
||||
/// <para>- Returns hex notation as a string.</para>
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <returns>String of hex digits corresponding to encrypted password.</returns>
|
||||
public String EncodePassword_Hex(String password, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql)
|
||||
{
|
||||
ClearErrorState();
|
||||
if (method == ISacsDatabase.PasswordHashMethod.MsSql)
|
||||
{
|
||||
if (DbmsType == DbmsType.MsSql)
|
||||
{
|
||||
try
|
||||
{
|
||||
String colAlias = "DataValue";
|
||||
String query = $"Select {colAlias}=Convert(VarChar(2000),PwdEncrypt('{DbQuery.FormatForCondition(password)}'),2);";
|
||||
using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query))
|
||||
{
|
||||
if (dbResultSet.NextRecord())
|
||||
return dbResultSet.GetRecordValues().GetValue(colAlias).ToString();
|
||||
else if (dbResultSet.HasErrorState())
|
||||
SetErrorState(dbResultSet.ErrorStateMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "hashing password")}", true);
|
||||
SetErrorState(exc.Message);
|
||||
}
|
||||
}
|
||||
else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password by method 'SQL'. Database is '{DbmsType}'.");
|
||||
}
|
||||
else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password. Unrecognised method: '{method}'.");
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare the password to the encrypted hash using the specified method.
|
||||
/// </summary>
|
||||
/// <param name="password"></param>
|
||||
/// <param name="passwordHashHex"></param>
|
||||
/// <returns>True if the password matches the encrypted hash value.</returns>
|
||||
public bool ComparePassword_Hex(String password, String passwordHashHex, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql)
|
||||
{
|
||||
ClearErrorState();
|
||||
if (method == ISacsDatabase.PasswordHashMethod.MsSql)
|
||||
{
|
||||
if (DbmsType == DbmsType.MsSql)
|
||||
{
|
||||
try
|
||||
{
|
||||
String colAlias = "PwdCompare";
|
||||
String query = $"Select {colAlias}=PwdCompare('{DbQuery.FormatForCondition(password)}', Convert(VarBinary(1000),'{DbQuery.FormatForCondition(passwordHashHex)}',2));";
|
||||
using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query))
|
||||
{
|
||||
if (dbResultSet.NextRecord())
|
||||
return StringUtil.ToBool(dbResultSet.GetRecordValues().GetValue(colAlias).ToString());
|
||||
else if (dbResultSet.HasErrorState())
|
||||
SetErrorState(dbResultSet.ErrorStateMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "comparing password")}", true);
|
||||
SetErrorState(exc.Message);
|
||||
}
|
||||
}
|
||||
else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password by method 'SQL'. Database is '{DbmsType}'.");
|
||||
}
|
||||
else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password. Unrecognised method: '{method}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// O D B C D r i v e r s
|
||||
//-------------------------------------------------------------------
|
||||
@@ -184,7 +261,7 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public abstract IDbDataTable ExecuteQuery(String query);
|
||||
public abstract DbResultSet ExecuteQuery(String query);
|
||||
public abstract int ExecuteNonQuery(String query);
|
||||
public abstract int ExecuteInsert(String tableName, List<StringPair> columnNameValues);
|
||||
public abstract int ExecuteUpdate(String tableName, List<StringPair> columnNameValues, String filter);
|
||||
|
||||
@@ -17,9 +17,9 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
// Database configuration
|
||||
public class OdbcConfig
|
||||
{
|
||||
public DbmsType dbms = DbmsType.General;
|
||||
public String connectionString; // Raw connection string.
|
||||
public String location, user, pword; // Application properties.
|
||||
public DbmsType Dbms = DbmsType.General;
|
||||
public String ConnectionString; // Raw connection string.
|
||||
public String Location, User, Password; // Connection properties.
|
||||
/// <summary>Database name populated when connection string is generated if not done previously.</summary>
|
||||
public String DatabaseName { get; set; }
|
||||
}
|
||||
@@ -42,9 +42,9 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
//-------------------------------------------------------------------
|
||||
public static DbmsType GetDbmsType(OdbcConfig config)
|
||||
{
|
||||
if (config.dbms == DbmsType.MsSql || config.location.Left(6).EqualsCI("MsSql:"))
|
||||
if (config.Dbms == DbmsType.MsSql || config.Location.Left(6).EqualsCI("MsSql:"))
|
||||
return DbmsType.MsSql;
|
||||
return config.dbms;
|
||||
return config.Dbms;
|
||||
}
|
||||
public static String GenerateConnectionString(OdbcConfig config, bool dbmsSpecificConnectionString = false)
|
||||
{
|
||||
@@ -58,7 +58,7 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
// MsSql:Server:Port\Instance.Database
|
||||
dbms = DbmsType.MsSql;
|
||||
String sqlDriver = OdbcUtil.GetOdbcSqlDriverName();
|
||||
String sqlLocation = config.location.Left(6).EqualsCI("MsSql:") ? config.location.Substring(6) : config.location;
|
||||
String sqlLocation = config.Location.Left(6).EqualsCI("MsSql:") ? config.Location.Substring(6) : config.Location;
|
||||
String sqlServer = "."; /* Assume local host */
|
||||
String sqlInstance = "";
|
||||
String sqlPort = "";
|
||||
@@ -99,17 +99,17 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
// WsId=<WorkstationId>;
|
||||
// Server=[protocol:]Server[,port][\instancename];
|
||||
// Database=<Default database>;
|
||||
// Trusted_Connection=<Yes|No>;
|
||||
// Trusted_Connection=<Yes|No>; [or Integrated Security=SSPI;]
|
||||
// UseProcForPrepare=2
|
||||
connectionString = "Driver={" + sqlDriver + "};";
|
||||
connectionString += "Server=" + sqlServer + ";";
|
||||
connectionString += "Database=" + sqlDatabase + ";";
|
||||
if (!config.user.IsEmpty())
|
||||
if (!config.User.IsEmpty())
|
||||
{
|
||||
connectionString += "UID=" + config.user + ";";
|
||||
connectionString += "PWD=" + config.pword + ";";
|
||||
connectionString += "UID=" + config.User + ";";
|
||||
connectionString += "PWD=" + config.Password + ";";
|
||||
}
|
||||
connectionString += "Trusted_Connection=" + (config.user.IsEmpty() ? "Yes" : "No") + ";";
|
||||
connectionString += "Trusted_Connection=" + (config.User.IsEmpty() ? "Yes" : "No") + ";";
|
||||
// Mar24 Added to avoid 'The certificate chain was issued by an authority that is not trusted'.
|
||||
connectionString += "TrustServerCertificate=Yes;";
|
||||
// Multiple Active Result Sets.
|
||||
@@ -125,23 +125,23 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
connectionString = "";
|
||||
connectionString += "Server=" + sqlServer + ";";
|
||||
connectionString += "Database=" + sqlDatabase + ";";
|
||||
if (!config.user.IsEmpty())
|
||||
if (!config.User.IsEmpty())
|
||||
{
|
||||
connectionString += "User Id=" + config.user + ";";
|
||||
connectionString += "Password=" + config.pword + ";";
|
||||
connectionString += "User Id=" + config.User + ";";
|
||||
connectionString += "Password=" + config.Password + ";";
|
||||
}
|
||||
connectionString += "Trusted_Connection=" + (config.user.IsEmpty() ? "Yes" : "No") + ";";
|
||||
connectionString += "Trusted_Connection=" + (config.User.IsEmpty() ? "Yes" : "No") + ";";
|
||||
// Mar24 Added to avoid 'The certificate chain was issued by an authority that is not trusted'.
|
||||
connectionString += "TrustServerCertificate=Yes;";
|
||||
}
|
||||
|
||||
// Populate configuration values if blank.
|
||||
if (config.dbms <= 0)
|
||||
config.dbms = dbms;
|
||||
if (config.Dbms <= 0)
|
||||
config.Dbms = dbms;
|
||||
if (StringUtil.IsEmpty(config.DatabaseName))
|
||||
config.DatabaseName = sqlDatabase;
|
||||
|
||||
SaLogger.Info($"{SysUtil.Fn} Driver: " + sqlDriver + ". Server: " + sqlServer + ". Database: " + sqlDatabase + ". Connection: " + connectionString);
|
||||
//SaLogger.Debug($"{SysUtil.Fn} Driver: " + sqlDriver + ". Server: " + sqlServer + ". Database: " + sqlDatabase + ". Connection: " + connectionString);
|
||||
}
|
||||
return connectionString;
|
||||
}
|
||||
@@ -152,17 +152,17 @@ namespace Sarand.Net.Standard.Dbas.Base
|
||||
public static SacsDatabase OpenOdbcDatabase(OdbcConfig config)
|
||||
{
|
||||
// Populate configuration.
|
||||
config.dbms = GetDbmsType(config);
|
||||
config.Dbms = GetDbmsType(config);
|
||||
|
||||
// Connection strings.
|
||||
String connectionString = config.connectionString;
|
||||
String connectionString = config.ConnectionString;
|
||||
if (StringUtil.IsEmpty(connectionString))
|
||||
connectionString = GenerateConnectionString(config);
|
||||
String sqlConnectionString = GenerateConnectionString(config, true);
|
||||
|
||||
// Open database.
|
||||
DbGlobalDbms.Type = config.dbms;
|
||||
SacsDatabaseOdbc database = new SacsDatabaseOdbc(config.dbms, config.DatabaseName, connectionString);
|
||||
DbGlobalDbms.Type = config.Dbms;
|
||||
SacsDatabaseOdbc database = new SacsDatabaseOdbc(config.Dbms, config.DatabaseName, connectionString);
|
||||
database.SelectLockMode = SelectLockMode.ReadUncommitted;
|
||||
database.SqlConnectionString = sqlConnectionString;
|
||||
return database;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Sarand.Net.Standard.Dbas.CodeType
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using System.Data;
|
||||
using Sarand.Net.Standard.Base.Dbas;
|
||||
|
||||
namespace Sarand.Net.Standard.Dbas.Data
|
||||
{
|
||||
//=======================================================================
|
||||
/// <summary>
|
||||
/// Wrapper for System.Data.DataTable
|
||||
/// </summary>
|
||||
public class DbDataTable : IDbDataTable
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// Data members.
|
||||
private DataTable dataTable;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Construction.
|
||||
public DbDataTable (DataTable dataTable)
|
||||
{
|
||||
this.dataTable = dataTable;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
@@ -171,29 +171,20 @@ namespace Sarand.Net.Standard.Dbas.Data
|
||||
// Q u e r y D a t a b a s e
|
||||
//-------------------------------------------------------------------
|
||||
// Query records.
|
||||
public override IDbDataTable ExecuteQuery(String query)
|
||||
public override DbResultSet ExecuteQuery(String query)
|
||||
{
|
||||
ClearErrorState();
|
||||
DataTable dataTable = null;
|
||||
Log(SysUtil.Fn, "Query: " + query, SaMessageLevel.Debug);
|
||||
try
|
||||
{
|
||||
using (DbConnection dbConnection = CreateConnection())
|
||||
using (var dbCommand = dbConnection.CreateCommand())
|
||||
{
|
||||
dbConnection.Open();
|
||||
dbCommand.CommandType = System.Data.CommandType.Text;
|
||||
dbCommand.CommandText = query;
|
||||
dataTable = new DataTable();
|
||||
dataTable.Load(dbCommand.ExecuteReader());
|
||||
}
|
||||
return DbResultSet_DataReader.ExecuteQuery(this, query);
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc)}\nQuery:\n\t{String.Join("\n\t", query.ReplaceAll("\n\r", "\n").Split("\n"))}", true);
|
||||
SetErrorState(exc.Message);
|
||||
}
|
||||
return new DbDataTable(dataTable);
|
||||
return null;
|
||||
}
|
||||
//-------------------------------------------------------------------
|
||||
/// <summary>Execute a non-query statement and return the row count.</summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Sarand.Net.Standard.Dbas.Interface
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Sarand.Net.Standard.Dbas.ItemData
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Sarand.Net.Standard.Dbas.ItemDocs
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Sarand.Net.Standard.Dbas.ItemOptions
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Sarand.Net.Standard.Dbas.ReferenceData
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -73,6 +73,28 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
/// <summary>Store the password against against the user.</summary>
|
||||
public override bool SetPassword(SaSecurityItem securityItemUser, String password)
|
||||
{
|
||||
return securityMgr.SetPassword(securityItemUser, password);
|
||||
}
|
||||
|
||||
/// <summary>Does the password match that currently defined against the user.</summary>
|
||||
public override bool PasswordMatches(SaSecurityItem securityItemUser, String password)
|
||||
{
|
||||
return securityMgr.PasswordMatches(securityItemUser, password);
|
||||
}
|
||||
|
||||
/// <summary>Is the password currently defined against the user blank.</summary>
|
||||
public override bool PasswordIsBlank(SaSecurityItem securityItemUser)
|
||||
{
|
||||
return securityMgr.PasswordIsBlank(securityItemUser);
|
||||
}
|
||||
|
||||
/// <summary>Change Password form - desktop only.</summary>
|
||||
public override bool ShowChangePasswordForm(DbUpdateInfo dbUpdateInfo)
|
||||
{
|
||||
ClearErrorState();
|
||||
@@ -112,16 +134,16 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
if (!HasErrorState()) PopulateSuperGroup(dbUpdateInfo);
|
||||
// Create the 'admin' user.
|
||||
if (!HasErrorState()) PopulateAdminUser(dbUpdateInfo);
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
private bool PopulateSuperGroup(DbUpdateInfo dbUpdateInfo)
|
||||
{
|
||||
ClearErrorState();
|
||||
SaSecurityItem superGroup = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.Group, GroupSuperCode);
|
||||
SaSecurityItem superGroup = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.Group, GroupCode_Super);
|
||||
if (superGroup == null)
|
||||
{
|
||||
superGroup = securityMgr.CreateSecurityItem(SaSecurity.ItemType.Group);
|
||||
superGroup.Code = GroupSuperCode;
|
||||
superGroup.Code = GroupCode_Super;
|
||||
superGroup.Description = "Superuser";
|
||||
if (!securityMgr.InsertSecurityItem(superGroup, dbUpdateInfo))
|
||||
return SetErrorState(securityMgr.ErrorStateMessage);
|
||||
@@ -143,29 +165,29 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
}
|
||||
}
|
||||
}
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
private bool PopulateAdminUser(DbUpdateInfo dbUpdateInfo)
|
||||
{
|
||||
ClearErrorState();
|
||||
SaSecurityItem user = null;
|
||||
if (!securityMgr.SecurityItemExistsByTypeCode(SaSecurity.ItemType.User, AdminUserCode))
|
||||
if (!securityMgr.SecurityItemExistsByTypeCode(SaSecurity.ItemType.User, UserCode_Admin))
|
||||
{
|
||||
// Create the admin user.
|
||||
user = securityMgr.CreateSecurityItem(SaSecurity.ItemType.User);
|
||||
user.Code = AdminUserCode;
|
||||
user.Code = UserCode_Admin;
|
||||
user.Description = "<Admin>";
|
||||
user.Location = "<System>";
|
||||
user.PasswordEncrypted = SaSecurityItem.EncryptPassword(AdminUserCode);
|
||||
securityMgr.SetPassword(user, UserCode_Admin);
|
||||
if (!securityMgr.InsertSecurityItem(user, dbUpdateInfo))
|
||||
return SetErrorState(securityMgr.ErrorStateMessage);
|
||||
}
|
||||
// Retrieve the user (even if just created).
|
||||
user = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.User, AdminUserCode);
|
||||
user = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.User, UserCode_Admin);
|
||||
if (user != null)
|
||||
{
|
||||
// Give this user superuser rights.
|
||||
SaSecurityItem superGroup = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.Group, GroupSuperCode);
|
||||
SaSecurityItem superGroup = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.Group, GroupCode_Super);
|
||||
if (superGroup != null)
|
||||
{
|
||||
if (!securityMgr.SecurityItemHasLinksWith(user.Id, superGroup.Id))
|
||||
@@ -176,7 +198,7 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
}
|
||||
}
|
||||
}
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -234,7 +256,7 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
}
|
||||
OnLogin(secItem != null ? secItem.Id : null);
|
||||
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -534,19 +556,35 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
if (!securityMgr.InsertSecurityItem(secItem, dbUpdateInfo))
|
||||
return SetErrorState(securityMgr.ErrorStateMessage);
|
||||
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
public override bool AssignUserToGroup(string userCode, string groupCode, DbUpdateInfo dbUpdateInfo)
|
||||
{
|
||||
ClearErrorState();
|
||||
throw new NotImplementedException();
|
||||
// Validate environment.
|
||||
if (securityMgr == null) return SetErrorState("security manager is not available");
|
||||
if (!securityMgr.DatabaseIsValid) return SetErrorState("database has an invalid security profile");
|
||||
// User security item.
|
||||
SaSecurityItem securityItemUser = securityMgr.GetSecurityItemByTypeCode(SaSecurity.ItemType.User, userCode);
|
||||
if (securityItemUser == null) return SetErrorState(("user does not exist"));
|
||||
// Group security item.
|
||||
SysId securityGroupId = GetIdForItemCode(SaSecurity.ItemType.Group, groupCode);
|
||||
if (!SysId.IsValid(securityGroupId)) return SetErrorState(("invalid group code"));
|
||||
if (!ItemsAreLinked(securityItemUser.Id, securityGroupId))
|
||||
{
|
||||
SaSecurityLink securityLinkUserGroup = securityMgr.CreateSecurityLink(securityItemUser.Id, securityGroupId, new SysId());
|
||||
if (securityLinkUserGroup == null) return SetErrorState($"failed to create user-group ({securityMgr.ErrorStateMessage})");
|
||||
if (!securityMgr.InsertSecurityLink(securityLinkUserGroup, dbUpdateInfo))
|
||||
return SetErrorState($"failed to add user to group ({securityMgr.ErrorStateMessage})");
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
public override bool UpdateSecurityItem(SaSecurityItem securityItem, DbUpdateInfo dbUpdateInfo)
|
||||
{
|
||||
ClearErrorState();
|
||||
if (!securityMgr.UpdateSecurityItem(securityItem, dbUpdateInfo))
|
||||
return SetErrorState(securityMgr.ErrorStateMessage);
|
||||
return !HasErrorState();
|
||||
return OK;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -278,7 +278,9 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
dbSecurityItem._Code = securityItem.Code;
|
||||
dbSecurityItem._Description = securityItem.Description;
|
||||
dbSecurityItem._Location = securityItem.Location;
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
dbSecurityItem._EncPword = securityItem.PasswordEncrypted;
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
dbSecurityItem._PwordSalt = securityItem.PasswordSalt;
|
||||
dbSecurityItem._PwordHash = securityItem.PasswordHash;
|
||||
dbSecurityItem._LastLogin = securityItem.LastLogin;
|
||||
@@ -304,7 +306,9 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
securityItem.EmailAddress = dbSecurityItem._EmailAddress;
|
||||
securityItem.Description = dbSecurityItem._Description;
|
||||
securityItem.Location = dbSecurityItem._Location;
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
securityItem.PasswordEncrypted = dbSecurityItem._EncPword;
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
securityItem.PasswordSalt = dbSecurityItem._PwordSalt;
|
||||
securityItem.PasswordHash = dbSecurityItem._PwordHash;
|
||||
securityItem.LastLogin = dbSecurityItem._LastLogin;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.Dbas;
|
||||
using Sarand.Net.Standard.Base.Security;
|
||||
using Sarand.Net.Standard.Dbas.Data;
|
||||
@@ -15,7 +17,7 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Construction.
|
||||
internal SaSecurityMgr_Dbas (SaSecurityConsole_Dbas securityConsole) : base()
|
||||
internal SaSecurityMgr_Dbas(SaSecurityConsole_Dbas securityConsole) : base()
|
||||
{
|
||||
SecurityConsole = securityConsole;
|
||||
DbInfo = new DbDataItem.DatabaseInfo(securityConsole.Database);
|
||||
@@ -31,15 +33,109 @@ namespace Sarand.Net.Standard.Dbas.Security
|
||||
public override bool DatabaseIsValid { get { return (Database != null && Database.GetTableNames().Contains(TableName_SecItem) && Database.GetTableNames().Contains(TableName_SecLink)); } }
|
||||
public override bool CanUpdateDatabase { get { return DatabaseIsValid; } }
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Utilities.
|
||||
public override bool ColumnExists (SaSecurityDbDfn.ColumnIdx columnIdx)
|
||||
// Database utilities.
|
||||
public override bool ColumnExists(SaSecurityDbDfn.ColumnIdx columnIdx)
|
||||
{
|
||||
String tableName = SaSecurityDbDfn.GetTableName(SaSecurityDbDfn.GetTableForColumn(columnIdx), false);
|
||||
String columnName = SaSecurityDbDfn.GetColumnName(columnIdx, false);
|
||||
return Database.ColumnExists(tableName, columnName);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Password utilities.
|
||||
|
||||
public override bool PasswordIsBlank(SaSecurityItem securityItemUser)
|
||||
{
|
||||
if (securityItemUser == null) return false;
|
||||
SaSecurity.PasswordMethod method = securityItemUser.GetPasswordMethod();
|
||||
// MsSql method.
|
||||
if (method == SaSecurity.PasswordMethod.MsSql)
|
||||
{
|
||||
//...
|
||||
}
|
||||
// Legacy.
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
return EncryptPassword_Legacy("").Equals(securityItemUser.PasswordEncrypted);
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
}
|
||||
|
||||
public override bool PasswordMatches(SaSecurityItem securityItemUser, String password)
|
||||
{
|
||||
if (securityItemUser == null) return false;
|
||||
SaSecurity.PasswordMethod method = securityItemUser.GetPasswordMethod();
|
||||
// MsSql method.
|
||||
if (method == SaSecurity.PasswordMethod.MsSql)
|
||||
{
|
||||
//...
|
||||
}
|
||||
// Legacy.
|
||||
#pragma warning disable CS0618 // Deprecated property
|
||||
return (EncryptPassword_Legacy(password).Equals(securityItemUser.PasswordEncrypted));
|
||||
#pragma warning restore CS0618 // Deprecated property
|
||||
}
|
||||
|
||||
/// <summary>Store the password against against the user.</summary>
|
||||
public override bool SetPassword(SaSecurityItem securityItemUser, String password)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static String EncryptPassword_Legacy(String password)
|
||||
{
|
||||
// We know this is not very water-tight, but it at least
|
||||
// hides the contents of the password from prying eyes.
|
||||
String toBeEncrypted = password;
|
||||
// Password length goes in as first character:
|
||||
toBeEncrypted = ByteToString((byte)(128 - password.Length)) + toBeEncrypted;
|
||||
toBeEncrypted = string.Concat(toBeEncrypted.Reverse());
|
||||
// Pad out to fixed length (minimum 16, multiples of 8):
|
||||
int fixedLength = 16;
|
||||
if (password.Length / 8 > 2) fixedLength = 8 * (int)(password.Length / 8);
|
||||
while (toBeEncrypted.Length < fixedLength)
|
||||
{
|
||||
char charToAdd = (char)(password.Length > 0 ? password[(toBeEncrypted.Length % password.Length)] : 'A' + (toBeEncrypted.Length * 13) % 23);
|
||||
if (toBeEncrypted.Length % 2 != 0)
|
||||
toBeEncrypted += charToAdd;
|
||||
else toBeEncrypted = charToAdd + toBeEncrypted;
|
||||
}
|
||||
|
||||
// Now perform a simple algorythm on each character to hide its true value:
|
||||
byte[] encryptedChars = new byte[toBeEncrypted.Length];
|
||||
for (int i = 0; i < toBeEncrypted.Length; i++)
|
||||
{
|
||||
int iData = toBeEncrypted[i];
|
||||
iData += 17 + i;
|
||||
if (iData > 255) iData -= (255 + 32);
|
||||
encryptedChars[i] = (byte)iData;
|
||||
}
|
||||
String result = Encoding_Ansi1252.GetString(encryptedChars);
|
||||
#if DEBUG
|
||||
// Password: mgbgt
|
||||
// To encrypt: tbmggtgbgm{btggm
|
||||
// Encrypted: 133, 116, 128, 123, 124, 138, 126, 122, 128, 135, 150, 126, 145, 133, 134, 141
|
||||
//Logger.Info(fn + "\n\tPassword: " + password + "\n\tTo encrypt: " + toBeEncrypted + "\n\tEncrypted: " + encryptedChars.Select(i => i.ToString()).ToList().Concat(", "));
|
||||
//Logger.Debug(password + ": " + result);
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
private static byte[] PasswordStringToBytes(String value)
|
||||
{
|
||||
byte[] values = new byte[value.Length];
|
||||
for (int idx = 0; idx < value.Length; idx++)
|
||||
{
|
||||
char c = value[idx];
|
||||
UInt32 ui = Convert.ToUInt32(c);
|
||||
SaLogger.Info(idx + ". " + ui.ToString("x8"));
|
||||
values[idx] = (byte)(c);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
private static readonly Encoding Encoding_Ansi1252 = Encoding.GetEncoding(1252);
|
||||
private static String ByteToString(byte value)
|
||||
{
|
||||
return Encoding_Ansi1252.GetString(new[] { value });
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
//=======================================================================
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Sarand.Net.Standard.Dbas.UpdateLog
|
||||
// Open database.
|
||||
// NB Must be ODBC connection - especially to retrieve index columns.
|
||||
SacsDatabaseConsole.OdbcConfig config = new SacsDatabaseConsole.OdbcConfig();
|
||||
config.connectionString = connectionString;
|
||||
config.ConnectionString = connectionString;
|
||||
SacsDatabase database = SacsDatabaseConsole.OpenOdbcDatabase(config);
|
||||
|
||||
// Generate database definition.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.1.1" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
|
||||
<PackageReference Include="NPOI" Version="2.7.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user