Compare commits

..

13 Commits

Author SHA1 Message Date
Meik
f3af7b74f0 Separate AD group description shortening 2026-03-29 23:46:20 +02:00
Meik
45009dfacc Add integer data area type id 2026-03-29 23:26:16 +02:00
Meik
cd133c67e1 Separate NTFS ensure config flags 2026-03-29 23:23:46 +02:00
Meik
17bcf9d4fb Gate NTFS share ensure behind config 2026-03-29 23:16:56 +02:00
Meik
ae65f8e758 Support NTFS permission ensure for shares 2026-03-29 23:13:17 +02:00
Meik
54be771569 Restore MsTeams legacy permission compatibility 2026-03-29 22:58:29 +02:00
Meik
d95a9c0ea9 Add NTFS path policy examples 2026-03-29 22:51:36 +02:00
Meik
8e6bbe4bec Update NTFS path policy documentation 2026-03-29 22:49:35 +02:00
Meik
b055e29f9a Generalize NTFS path policy 2026-03-29 22:47:03 +02:00
Meik
804eee20fd Add NTFS folder whitelist support 2026-03-29 22:39:35 +02:00
Meik
246af92f5d Add NTFS folder blacklist support 2026-03-29 22:35:00 +02:00
Meik
ac747028f6 Add workflow signing assets 2026-03-29 22:33:02 +02:00
Meik
f9daba1bb6 Add NTFS blacklist whitelist concept 2026-03-29 22:27:00 +02:00
12 changed files with 1366 additions and 575 deletions

View File

@@ -373,6 +373,7 @@ namespace C4IT.LIAM
public string Description { get; set; } public string Description { get; set; }
public string UniqueId { get; set; } public string UniqueId { get; set; }
public string DataAreaType { get; set; } public string DataAreaType { get; set; }
public int DataAreaTypeId { get; set; }
} }
public class LiamApiVersionInfo public class LiamApiVersionInfo
{ {

View File

@@ -557,238 +557,238 @@ where ";
return RetVal; return RetVal;
} }
private bool TryLoadProviderFragments(Guid providerConfigClassID, out Guid objectId, out DataTable tblMain, out DataTable tblBase, out DataTable tblAdditionalAttr, out DataTable tblNamingConvention, out DataTable tblCustomTags) private bool TryLoadProviderFragments(Guid providerConfigClassID, out Guid objectId, out DataTable tblMain, out DataTable tblBase, out DataTable tblAdditionalAttr, out DataTable tblNamingConvention, out DataTable tblCustomTags)
{ {
objectId = Guid.Empty; objectId = Guid.Empty;
tblMain = null; tblMain = null;
tblBase = null; tblBase = null;
tblAdditionalAttr = null; tblAdditionalAttr = null;
tblNamingConvention = null; tblNamingConvention = null;
tblCustomTags = null; tblCustomTags = null;
try try
{ {
var classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderMain); var classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderMain);
LogEntry($"Config provider class ID: {classID}", LogLevels.Debug); LogEntry($"Config provider class ID: {classID}", LogLevels.Debug);
var fragment = FragmentRequest.GetSPSFragment(classID, providerConfigClassID); var fragment = FragmentRequest.GetSPSFragment(classID, providerConfigClassID);
if (fragment == null) if (fragment == null)
{ {
LogEntry($"Provider config fragment not found: ClassID={classID}, FragmentId={providerConfigClassID}", LogLevels.Debug); LogEntry($"Provider config fragment not found: ClassID={classID}, FragmentId={providerConfigClassID}", LogLevels.Debug);
return false; return false;
} }
objectId = fragment.ObjectID; objectId = fragment.ObjectID;
if (objectId == Guid.Empty) if (objectId == Guid.Empty)
{ {
LogEntry($"Provider config fragment not found: ClassID={classID}, FragmentId={providerConfigClassID}", LogLevels.Debug); LogEntry($"Provider config fragment not found: ClassID={classID}, FragmentId={providerConfigClassID}", LogLevels.Debug);
return false; return false;
} }
tblMain = fragment.FragmentTable; tblMain = fragment.FragmentTable;
LogEntry($"Config provider object ID: {objectId}", LogLevels.Debug); LogEntry($"Config provider object ID: {objectId}", LogLevels.Debug);
classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderBase); classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderBase);
LogEntry($"Config provider base class ID: {classID}", LogLevels.Debug); LogEntry($"Config provider base class ID: {classID}", LogLevels.Debug);
Guid[] ids = { objectId }; Guid[] ids = { objectId };
tblBase = FragmentRequestBase.SimpleLoad(classID, tblBase = FragmentRequestBase.SimpleLoad(classID,
"Account, Password", "Account, Password",
AsqlHelper.BuildInCondition("[Expression-ObjectID]", ids)); AsqlHelper.BuildInCondition("[Expression-ObjectID]", ids));
if (tblBase?.Rows == null || tblBase.Rows.Count <= 0) if (tblBase?.Rows == null || tblBase.Rows.Count <= 0)
{ {
LogEntry($"Provider config base fragment not found: ClassId={classID}, ObjectId={objectId}", LogLevels.Debug); LogEntry($"Provider config base fragment not found: ClassId={classID}, ObjectId={objectId}", LogLevels.Debug);
return false; return false;
} }
classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderAdditionalAttributes); classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigProviderAdditionalAttributes);
LogEntry($"Config provider additional config class ID: {classID}", LogLevels.Debug); LogEntry($"Config provider additional config class ID: {classID}", LogLevels.Debug);
tblAdditionalAttr = FragmentRequestBase.SimpleLoad(classID, tblAdditionalAttr = FragmentRequestBase.SimpleLoad(classID,
"Name, Value", "Name, Value",
AsqlHelper.BuildInCondition("[Expression-ObjectID]", ids)); AsqlHelper.BuildInCondition("[Expression-ObjectID]", ids));
if (tblAdditionalAttr == null) if (tblAdditionalAttr == null)
{ {
LogEntry($"Provider additional config class fragment not found: ClassId={classID}, ObjectId={objectId}", LogLevels.Debug); LogEntry($"Provider additional config class fragment not found: ClassId={classID}, ObjectId={objectId}", LogLevels.Debug);
return false; return false;
} }
classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameCustomTagBase); classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameCustomTagBase);
LogEntry($"Custom Tag class ID: {classID}", LogLevels.Debug); LogEntry($"Custom Tag class ID: {classID}", LogLevels.Debug);
tblCustomTags = FragmentRequestBase.SimpleLoad(classID, "Key, Name", $"[Expression-ObjectID]='{objectId}'") ?? new DataTable(); tblCustomTags = FragmentRequestBase.SimpleLoad(classID, "Key, Name", $"[Expression-ObjectID]='{objectId}'") ?? new DataTable();
classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigNamingConvention); classID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameConfigNamingConvention);
LogEntry($"Naming convention config class ID: {classID}", LogLevels.Debug); LogEntry($"Naming convention config class ID: {classID}", LogLevels.Debug);
tblNamingConvention = FragmentRequestBase.SimpleLoad(classID, tblNamingConvention = FragmentRequestBase.SimpleLoad(classID,
"ID, Usage, NamingConvention.Description as Description, NamingConvention.DescriptionTemplate as DescriptionTemplate, " + "ID, Usage, NamingConvention.Description as Description, NamingConvention.DescriptionTemplate as DescriptionTemplate, " +
"NamingConvention.Name as Name, NamingConvention.NamingTemplate as NamingTemplate, NamingConvention.Wildcard as Wildcard", "NamingConvention.Name as Name, NamingConvention.NamingTemplate as NamingTemplate, NamingConvention.Wildcard as Wildcard",
$"[Expression-ObjectID]='{objectId}'") ?? new DataTable(); $"[Expression-ObjectID]='{objectId}'") ?? new DataTable();
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
LogException(e); LogException(e);
return false; return false;
} }
} }
private cLiamProviderData buildProviderData(DataTable dtMain, DataTable dtBase, DataTable dtAdditional, DataTable dtNamingConvention, DataTable dtCustomTag, bool includeSecret, out string sanitizedJson) private cLiamProviderData buildProviderData(DataTable dtMain, DataTable dtBase, DataTable dtAdditional, DataTable dtNamingConvention, DataTable dtCustomTag, bool includeSecret, out string sanitizedJson)
{ {
sanitizedJson = null; sanitizedJson = null;
if (dtMain?.Rows == null || dtMain.Rows.Count <= 0) if (dtMain?.Rows == null || dtMain.Rows.Count <= 0)
return null; return null;
if (dtBase?.Rows == null || dtBase.Rows.Count <= 0) if (dtBase?.Rows == null || dtBase.Rows.Count <= 0)
return null; return null;
if (dtAdditional == null) if (dtAdditional == null)
return null; return null;
var dataMain = dtMain.Rows[0]; var dataMain = dtMain.Rows[0];
var dataBase = dtBase.Rows[0]; var dataBase = dtBase.Rows[0];
var encPW = cLIAMHelper.getStringFromObject(dataBase["Password"]); var encPW = cLIAMHelper.getStringFromObject(dataBase["Password"]);
if (!CryptoManager.Instance.TryDecryptDBText(encPW, out string password)) if (!CryptoManager.Instance.TryDecryptDBText(encPW, out string password))
password = encPW; password = encPW;
var providerData = new cLiamProviderData() var providerData = new cLiamProviderData()
{ {
Domain = cLIAMHelper.getStringFromObject(dataMain["GCCDomain"]), Domain = cLIAMHelper.getStringFromObject(dataMain["GCCDomain"]),
Credential = new cLiamCredential() Credential = new cLiamCredential()
{ {
Domain = cLIAMHelper.getStringFromObject(dataMain["GCCDomain"]), Domain = cLIAMHelper.getStringFromObject(dataMain["GCCDomain"]),
Identification = cLIAMHelper.getStringFromObject(dataBase["Account"]), Identification = cLIAMHelper.getStringFromObject(dataBase["Account"]),
Secret = "***" Secret = "***"
}, },
RootPath = cLIAMHelper.getStringFromObject(dataMain["GCCTarget"]), RootPath = cLIAMHelper.getStringFromObject(dataMain["GCCTarget"]),
MaxDepth = cLIAMHelper.getIntFromObject(dataMain["GCCMaxDepth"]), MaxDepth = cLIAMHelper.getIntFromObject(dataMain["GCCMaxDepth"]),
GroupFilter = cLIAMHelper.getStringFromObject(dataMain["GCCgroupLDAPFilter"]), GroupFilter = cLIAMHelper.getStringFromObject(dataMain["GCCgroupLDAPFilter"]),
GroupPath = cLIAMHelper.getStringFromObject(dataMain["GCCgroupOUPath"]), GroupPath = cLIAMHelper.getStringFromObject(dataMain["GCCgroupOUPath"]),
GroupStrategy = (eLiamGroupStrategies)cLIAMHelper.getIntFromObject(dataMain["GCCPermissionGroupStrategy"]), GroupStrategy = (eLiamGroupStrategies)cLIAMHelper.getIntFromObject(dataMain["GCCPermissionGroupStrategy"]),
ProviderType = (eLiamProviderTypes)cLIAMHelper.getIntFromObject(dataMain["GCCtargetType"]) ProviderType = (eLiamProviderTypes)cLIAMHelper.getIntFromObject(dataMain["GCCtargetType"])
}; };
if (dtAdditional?.Rows != null) if (dtAdditional?.Rows != null)
{ {
foreach (DataRow row in dtAdditional.Rows) foreach (DataRow row in dtAdditional.Rows)
{ {
var name = cLIAMHelper.getStringFromObject(row["Name"]); var name = cLIAMHelper.getStringFromObject(row["Name"]);
var value = cLIAMHelper.getStringFromObject(row["Value"]); var value = cLIAMHelper.getStringFromObject(row["Value"]);
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
providerData.AdditionalConfiguration[name] = value; providerData.AdditionalConfiguration[name] = value;
} }
} }
if (dtCustomTag?.Rows != null) if (dtCustomTag?.Rows != null)
{ {
foreach (DataRow row in dtCustomTag.Rows) foreach (DataRow row in dtCustomTag.Rows)
{ {
var name = cLIAMHelper.getStringFromObject(row["Key"]); var name = cLIAMHelper.getStringFromObject(row["Key"]);
var value = cLIAMHelper.getStringFromObject(row["Name"]); var value = cLIAMHelper.getStringFromObject(row["Name"]);
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
providerData.CustomTags[name] = value; providerData.CustomTags[name] = value;
} }
} }
if (dtNamingConvention?.Rows != null) if (dtNamingConvention?.Rows != null)
{ {
foreach (DataRow row in dtNamingConvention.Rows) foreach (DataRow row in dtNamingConvention.Rows)
{ {
var usage = cLIAMHelper.getIntFromObject(row["Usage"]); var usage = cLIAMHelper.getIntFromObject(row["Usage"]);
var accessRole = eLiamAccessRoles.Read; var accessRole = eLiamAccessRoles.Read;
var scope = eLiamAccessRoleScopes.Unknown; var scope = eLiamAccessRoleScopes.Unknown;
switch (usage) switch (usage)
{ {
case -10: case -10:
accessRole = eLiamAccessRoles.Traverse; accessRole = eLiamAccessRoles.Traverse;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 10: case 10:
accessRole = eLiamAccessRoles.Read; accessRole = eLiamAccessRoles.Read;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 20: case 20:
accessRole = eLiamAccessRoles.Write; accessRole = eLiamAccessRoles.Write;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 30: case 30:
accessRole = eLiamAccessRoles.Owner; accessRole = eLiamAccessRoles.Owner;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 40: case 40:
accessRole = eLiamAccessRoles.Read; accessRole = eLiamAccessRoles.Read;
scope = eLiamAccessRoleScopes.DomainLocal; scope = eLiamAccessRoleScopes.DomainLocal;
break; break;
case 50: case 50:
accessRole = eLiamAccessRoles.Write; accessRole = eLiamAccessRoles.Write;
scope = eLiamAccessRoleScopes.DomainLocal; scope = eLiamAccessRoleScopes.DomainLocal;
break; break;
case 60: case 60:
accessRole = eLiamAccessRoles.Owner; accessRole = eLiamAccessRoles.Owner;
scope = eLiamAccessRoleScopes.DomainLocal; scope = eLiamAccessRoleScopes.DomainLocal;
break; break;
case 100: case 100:
accessRole = eLiamAccessRoles.ADOwner; accessRole = eLiamAccessRoles.ADOwner;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 110: case 110:
accessRole = eLiamAccessRoles.ADMember; accessRole = eLiamAccessRoles.ADMember;
scope = eLiamAccessRoleScopes.Global; scope = eLiamAccessRoleScopes.Global;
break; break;
case 200: case 200:
accessRole = eLiamAccessRoles.ExchangeMLMember; accessRole = eLiamAccessRoles.ExchangeMLMember;
scope = eLiamAccessRoleScopes.Universal; scope = eLiamAccessRoleScopes.Universal;
break; break;
case 210: case 210:
accessRole = eLiamAccessRoles.ExchangeMLOwner; accessRole = eLiamAccessRoles.ExchangeMLOwner;
scope = eLiamAccessRoleScopes.Universal; scope = eLiamAccessRoleScopes.Universal;
break; break;
case 250: case 250:
accessRole = eLiamAccessRoles.ExchangeSMBFullAccess; accessRole = eLiamAccessRoles.ExchangeSMBFullAccess;
scope = eLiamAccessRoleScopes.Universal; scope = eLiamAccessRoleScopes.Universal;
break; break;
case 260: case 260:
accessRole = eLiamAccessRoles.ExchangeSMBSendAs; accessRole = eLiamAccessRoles.ExchangeSMBSendAs;
scope = eLiamAccessRoleScopes.Universal; scope = eLiamAccessRoleScopes.Universal;
break; break;
case 270: case 270:
accessRole = eLiamAccessRoles.ExchangeSMBOwner; accessRole = eLiamAccessRoles.ExchangeSMBOwner;
scope = eLiamAccessRoleScopes.Universal; scope = eLiamAccessRoleScopes.Universal;
break; break;
} }
providerData.NamingConventions.Add(new cLiamNamingConvention() providerData.NamingConventions.Add(new cLiamNamingConvention()
{ {
AccessRole = accessRole, AccessRole = accessRole,
Scope = scope, Scope = scope,
Description = cLIAMHelper.getStringFromObject(row["Description"]), Description = cLIAMHelper.getStringFromObject(row["Description"]),
DescriptionTemplate = cLIAMHelper.getStringFromObject(row["DescriptionTemplate"]), DescriptionTemplate = cLIAMHelper.getStringFromObject(row["DescriptionTemplate"]),
Name = cLIAMHelper.getStringFromObject(row["Name"]), Name = cLIAMHelper.getStringFromObject(row["Name"]),
NamingTemplate = cLIAMHelper.getStringFromObject(row["NamingTemplate"]), NamingTemplate = cLIAMHelper.getStringFromObject(row["NamingTemplate"]),
Wildcard = cLIAMHelper.getStringFromObject(row["Wildcard"]) Wildcard = cLIAMHelper.getStringFromObject(row["Wildcard"])
}); });
} }
} }
sanitizedJson = JsonConvert.SerializeObject(providerData, Newtonsoft.Json.Formatting.Indented); sanitizedJson = JsonConvert.SerializeObject(providerData, Newtonsoft.Json.Formatting.Indented);
if (includeSecret && providerData.Credential != null) if (includeSecret && providerData.Credential != null)
providerData.Credential.Secret = password; providerData.Credential.Secret = password;
return providerData; return providerData;
} }
private cLiamProviderBase createProvider(DataTable dtMain, DataTable dtBase, DataTable dtAdditional, DataTable dtNamingConvention = null, DataTable dtCustomTag = null) private cLiamProviderBase createProvider(DataTable dtMain, DataTable dtBase, DataTable dtAdditional, DataTable dtNamingConvention = null, DataTable dtCustomTag = null)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
var DataProviderData = buildProviderData(dtMain, dtBase, dtAdditional, dtNamingConvention, dtCustomTag, includeSecret: true, out var sanitizedJson); var DataProviderData = buildProviderData(dtMain, dtBase, dtAdditional, dtNamingConvention, dtCustomTag, includeSecret: true, out var sanitizedJson);
if (DataProviderData == null) if (DataProviderData == null)
return null; return null;
LogEntry("Provider configuration (sanitized JSON, copy for diagnostics tool):", LogLevels.Info); LogEntry("Provider configuration (sanitized JSON, copy for diagnostics tool):", LogLevels.Info);
LogEntry(sanitizedJson, LogLevels.Info); LogEntry(sanitizedJson, LogLevels.Info);
var DataProvider = CreateProviderInstance(new cLiamConfiguration(), DataProviderData); var DataProvider = CreateProviderInstance(new cLiamConfiguration(), DataProviderData);
return DataProvider; return DataProvider;
} }
@@ -804,11 +804,11 @@ where ";
} }
[Route("initializeDataProvider"), HttpGet] [Route("initializeDataProvider"), HttpGet]
public async Task<bool> initializeDataProvider(Guid ProviderConfigClassID, bool force = false) public async Task<bool> initializeDataProvider(Guid ProviderConfigClassID, bool force = false)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
if (cC4ITLicenseM42ESM.Instance == null) if (cC4ITLicenseM42ESM.Instance == null)
LoadLicensingInformation(); LoadLicensingInformation();
@@ -829,61 +829,61 @@ where ";
finally finally
{ {
LogMethodEnd(CM); LogMethodEnd(CM);
} }
} }
[Route("exportProviderConfiguration"), HttpGet] [Route("exportProviderConfiguration"), HttpGet]
public IHttpActionResult exportProviderConfiguration(Guid ProviderConfigClassID) public IHttpActionResult exportProviderConfiguration(Guid ProviderConfigClassID)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
if (cC4ITLicenseM42ESM.Instance == null) if (cC4ITLicenseM42ESM.Instance == null)
LoadLicensingInformation(); LoadLicensingInformation();
if (!cC4ITLicenseM42ESM.Instance.IsValid) if (!cC4ITLicenseM42ESM.Instance.IsValid)
{ {
LogEntry($"Error: License not valid", LogLevels.Error); LogEntry($"Error: License not valid", LogLevels.Error);
return Content(HttpStatusCode.Forbidden, "License not valid"); return Content(HttpStatusCode.Forbidden, "License not valid");
} }
if (!TryLoadProviderFragments(ProviderConfigClassID, out var objectId, out var tblMain, out var tblBase, out var tblAdditionalAttr, out var tblNamingConvention, out var tblCustomTags)) if (!TryLoadProviderFragments(ProviderConfigClassID, out var objectId, out var tblMain, out var tblBase, out var tblAdditionalAttr, out var tblNamingConvention, out var tblCustomTags))
{ {
return Content(HttpStatusCode.NotFound, $"Provider configuration '{ProviderConfigClassID}' not found."); return Content(HttpStatusCode.NotFound, $"Provider configuration '{ProviderConfigClassID}' not found.");
} }
var providerData = buildProviderData(tblMain, tblBase, tblAdditionalAttr, tblNamingConvention, tblCustomTags, includeSecret: false, out var sanitizedJson); var providerData = buildProviderData(tblMain, tblBase, tblAdditionalAttr, tblNamingConvention, tblCustomTags, includeSecret: false, out var sanitizedJson);
if (providerData == null) if (providerData == null)
return Content(HttpStatusCode.InternalServerError, "Provider configuration could not be loaded."); return Content(HttpStatusCode.InternalServerError, "Provider configuration could not be loaded.");
var export = new ProviderConfigurationExport var export = new ProviderConfigurationExport
{ {
ProviderConfigClassID = ProviderConfigClassID, ProviderConfigClassID = ProviderConfigClassID,
ProviderConfigObjectID = objectId, ProviderConfigObjectID = objectId,
SanitizedJson = sanitizedJson, SanitizedJson = sanitizedJson,
Configuration = providerData, Configuration = providerData,
GeneratedAtUtc = DateTime.UtcNow GeneratedAtUtc = DateTime.UtcNow
}; };
return Ok(export); return Ok(export);
} }
catch (Exception E) catch (Exception E)
{ {
LogException(E); LogException(E);
return InternalServerError(E); return InternalServerError(E);
} }
finally finally
{ {
LogMethodEnd(CM); LogMethodEnd(CM);
} }
} }
private async Task<ProviderCacheEntry> getDataProvider(Guid ProviderConfigClassID, bool force = false) private async Task<ProviderCacheEntry> getDataProvider(Guid ProviderConfigClassID, bool force = false)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
if (!force) if (!force)
{ {
@@ -894,17 +894,17 @@ where ";
return Provider; return Provider;
} }
} }
if (!TryLoadProviderFragments(ProviderConfigClassID, out var ObjectID, out var tblMain, out var tblBase, out var tblAdditionalAttr, out var tblNamingConvention, out var tblCustomTags)) if (!TryLoadProviderFragments(ProviderConfigClassID, out var ObjectID, out var tblMain, out var tblBase, out var tblAdditionalAttr, out var tblNamingConvention, out var tblCustomTags))
return null; return null;
var DataProvider = createProvider(tblMain, tblBase, tblAdditionalAttr, tblNamingConvention, tblCustomTags); var DataProvider = createProvider(tblMain, tblBase, tblAdditionalAttr, tblNamingConvention, tblCustomTags);
if (DataProvider == null) if (DataProvider == null)
{ {
LogEntry($"Provider configuration '{ProviderConfigClassID}' could not be materialized.", LogLevels.Warning); LogEntry($"Provider configuration '{ProviderConfigClassID}' could not be materialized.", LogLevels.Warning);
return null; return null;
} }
var validLogon = await DataProvider.LogonAsync(); var validLogon = await DataProvider.LogonAsync();
if (!validLogon) if (!validLogon)
return null; return null;
@@ -1178,6 +1178,7 @@ where ";
Level = DataArea.Level.ToString(), Level = DataArea.Level.ToString(),
ConfigurationId = ProviderEntry.ObjectID.ToString(), ConfigurationId = ProviderEntry.ObjectID.ToString(),
DataAreaType = DataArea.DataType.ToString(), DataAreaType = DataArea.DataType.ToString(),
DataAreaTypeId = (int)DataArea.DataType,
Owner = owner, Owner = owner,
Write = write, Write = write,
Read = DataAreaNtfsFolder?.ReadGroupIdentifier ?? string.Empty, Read = DataAreaNtfsFolder?.ReadGroupIdentifier ?? string.Empty,
@@ -1857,34 +1858,34 @@ where ";
return accountEoid; return accountEoid;
} }
private string GetUserPrincipalNameFromEOID(Guid accountEoid) private string GetUserPrincipalNameFromEOID(Guid accountEoid)
{ {
var ClassID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameAccountAd); var ClassID = SPSDataEngineSchemaReader.ClassGetIDFromName(constFragmentNameAccountAd);
LogEntry($"Data area class ID: {ClassID}", LogLevels.Debug); LogEntry($"Data area class ID: {ClassID}", LogLevels.Debug);
var tbl = FragmentRequestBase.SimpleLoad(ClassID, "UserPrincipalName", $"[Expression-ObjectId]='{accountEoid}'"); var tbl = FragmentRequestBase.SimpleLoad(ClassID, "UserPrincipalName", $"[Expression-ObjectId]='{accountEoid}'");
if (tbl?.Rows == null || tbl.Rows.Count == 0) if (tbl?.Rows == null || tbl.Rows.Count == 0)
{ {
LogEntry($"No AD account entry list found with eoid='{accountEoid}'", LogLevels.Warning); LogEntry($"No AD account entry list found with eoid='{accountEoid}'", LogLevels.Warning);
return null; return null;
} }
var UPN = cLIAMHelper.getStringFromObject(tbl.Rows[0]["UserPrincipalName"]); var UPN = cLIAMHelper.getStringFromObject(tbl.Rows[0]["UserPrincipalName"]);
if (string.IsNullOrEmpty(UPN)) if (string.IsNullOrEmpty(UPN))
{ {
LogEntry("No UserPrincipalName found for AccountAd entry", LogLevels.Warning); LogEntry("No UserPrincipalName found for AccountAd entry", LogLevels.Warning);
return null; return null;
} }
return UPN; return UPN;
} }
public class ProviderConfigurationExport public class ProviderConfigurationExport
{ {
public Guid ProviderConfigClassID { get; set; } public Guid ProviderConfigClassID { get; set; }
public Guid ProviderConfigObjectID { get; set; } public Guid ProviderConfigObjectID { get; set; }
public string SanitizedJson { get; set; } public string SanitizedJson { get; set; }
public cLiamProviderData Configuration { get; set; } public cLiamProviderData Configuration { get; set; }
public DateTime GeneratedAtUtc { get; set; } public DateTime GeneratedAtUtc { get; set; }
} }
} }
} }

View File

@@ -7,13 +7,13 @@ using System.Threading.Tasks;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using C4IT.Logging; using C4IT.Logging;
using C4IT.MsGraph; using C4IT.MsGraph;
using static C4IT.Logging.cLogManager; using static C4IT.Logging.cLogManager;
using C4IT.Matrix42.ServerInfo; using C4IT.Matrix42.ServerInfo;
using static C4IT.MsGraph.cMsGraphSharepoint; using static C4IT.MsGraph.cMsGraphSharepoint;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace C4IT.LIAM namespace C4IT.LIAM
{ {
@@ -28,37 +28,93 @@ namespace C4IT.LIAM
public class cLiamProviderMsTeams : cLiamProviderBase public class cLiamProviderMsTeams : cLiamProviderBase
{ {
public readonly cMsGraphSharepoint MsSharepoint = new cMsGraphSharepoint(new cMsGraphBase()); public readonly cMsGraphSharepoint MsSharepoint = new cMsGraphSharepoint(new cMsGraphBase());
public const string allowedMailNickNameCharacter = @"[^A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]"; public const string allowedMailNickNameCharacter = @"[^A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]";
public readonly bool WithoutPrivateChannels = true; public readonly bool WithoutPrivateChannels = true;
private string lastErrorMessage = null; private string lastErrorMessage = null;
private static readonly string[] RequiredGraphRoles = new[] private sealed class GraphPermissionRequirement
{ {
"Application.Read.All", public string Description { get; private set; }
"Channel.ReadBasic.All",
"Directory.Read.All", public string[] AcceptedPermissions { get; private set; }
"Files.ReadWrite.All",
"Group.ReadWrite.All", public GraphPermissionRequirement(string description, params string[] acceptedPermissions)
"GroupMember.Read.All", {
"GroupMember.ReadWrite.All", Description = description;
"Team.Create", AcceptedPermissions = acceptedPermissions ?? new string[0];
"Team.ReadBasic.All", }
"TeamSettings.Read.All", }
"User.Read.All",
}; private static readonly GraphPermissionRequirement[] RequiredGraphPermissions = new[]
{
private void SetLastError(string message) new GraphPermissionRequirement(
{ "Team lesen",
lastErrorMessage = string.IsNullOrWhiteSpace(message) ? null : message; "Team.ReadBasic.All",
} "Group.Read.All",
"Group.ReadWrite.All",
public cLiamProviderMsTeams(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) : "Directory.Read.All",
base(LiamConfiguration, ProviderData) "Directory.ReadWrite.All"),
{ new GraphPermissionRequirement(
WithoutPrivateChannels = AdditionalConfiguration.ContainsKey("WithoutPrivateChannels") ? AdditionalConfiguration["WithoutPrivateChannels"].ToLower() == "true" || AdditionalConfiguration["WithoutPrivateChannels"] == "1" : false; "Channels lesen",
"Channel.ReadBasic.All",
"ChannelSettings.Read.All",
"ChannelSettings.ReadWrite.All",
"Group.Read.All",
"Group.ReadWrite.All",
"Directory.Read.All",
"Directory.ReadWrite.All"),
new GraphPermissionRequirement(
"Dateien lesen und schreiben",
"Files.ReadWrite.All",
"Sites.ReadWrite.All",
"Sites.FullControl.All"),
new GraphPermissionRequirement(
"Benutzer lesen",
"User.Read.All",
"User.ReadWrite.All",
"Directory.Read.All",
"Directory.ReadWrite.All"),
};
private static readonly GraphPermissionRequirement[] CloneBaseGraphPermissions = new[]
{
new GraphPermissionRequirement("Teams klonen", "Team.Create"),
};
private static readonly GraphPermissionRequirement[] CloneAppsGraphPermissions = new[]
{
new GraphPermissionRequirement("Apps mitklonen", "Application.Read.All", "Application.ReadWrite.All"),
};
private static readonly GraphPermissionRequirement[] CloneSettingsGraphPermissions = new[]
{
new GraphPermissionRequirement("Team-Einstellungen mitklonen", "TeamSettings.Read.All", "TeamSettings.ReadWrite.All"),
};
private static readonly GraphPermissionRequirement[] CloneMemberGraphPermissions = new[]
{
new GraphPermissionRequirement(
"Mitglieder mitklonen",
"GroupMember.Read.All",
"GroupMember.ReadWrite.All",
"Group.Read.All",
"Group.ReadWrite.All",
"Directory.Read.All",
"Directory.ReadWrite.All"),
};
private void SetLastError(string message)
{
lastErrorMessage = string.IsNullOrWhiteSpace(message) ? null : message;
}
public cLiamProviderMsTeams(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) :
base(LiamConfiguration, ProviderData)
{
WithoutPrivateChannels = AdditionalConfiguration.ContainsKey("WithoutPrivateChannels") ? AdditionalConfiguration["WithoutPrivateChannels"].ToLower() == "true" || AdditionalConfiguration["WithoutPrivateChannels"] == "1" : false;
} }
public override async Task<bool> LogonAsync() public override async Task<bool> LogonAsync()
@@ -68,90 +124,90 @@ namespace C4IT.LIAM
public async Task<bool> LogonAsync(bool force = false) public async Task<bool> LogonAsync(bool force = false)
{ {
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId)) if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId))
{ {
LogEntry($"Error: License not valid", LogLevels.Error); LogEntry($"Error: License not valid", LogLevels.Error);
SetLastError("License not valid"); SetLastError("License not valid");
return false; return false;
} }
if (!force && this.MsSharepoint.Base.IsOnline) if (!force && this.MsSharepoint.Base.IsOnline)
{ {
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken)) if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
return false; return false;
SetLastError(null); SetLastError(null);
return true; return true;
} }
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
var LI = new cMsGraphLogonInfo() { var LI = new cMsGraphLogonInfo() {
Tenant = this.Domain, Tenant = this.Domain,
ClientID = this.Credential?.Identification, ClientID = this.Credential?.Identification,
ClientSecret = this.Credential?.Secret ClientSecret = this.Credential?.Secret
}; };
var RetVal = await MsSharepoint.Base.LogonAsync(LI); var RetVal = await MsSharepoint.Base.LogonAsync(LI);
if (!RetVal) if (!RetVal)
{ {
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "MsTeams Logon fehlgeschlagen"); SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "MsTeams Logon fehlgeschlagen");
return false; return false;
} }
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken)) if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
return false; return false;
SetLastError(null); SetLastError(null);
return RetVal; return RetVal;
} }
catch (Exception E) catch (Exception E)
{ {
LogException(E); LogException(E);
SetLastError(E.Message); SetLastError(E.Message);
} }
finally finally
{ {
LogMethodEnd(CM); LogMethodEnd(CM);
} }
if (string.IsNullOrWhiteSpace(lastErrorMessage)) if (string.IsNullOrWhiteSpace(lastErrorMessage))
SetLastError("MsTeams Logon fehlgeschlagen"); SetLastError("MsTeams Logon fehlgeschlagen");
return false; return false;
} }
public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1) public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM); LogMethodBegin(CM);
try try
{ {
if (!await LogonAsync()) if (!await LogonAsync())
return null; return null;
var DataAreas = new List<cLiamDataAreaBase>(); var DataAreas = new List<cLiamDataAreaBase>();
var DAL = await MsSharepoint.RequestTeamsListAsync(); var DAL = await MsSharepoint.RequestTeamsListAsync();
if (DAL == null) if (DAL == null)
{ {
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "Konnte Teams-Liste nicht abrufen"); SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "Konnte Teams-Liste nicht abrufen");
return null; return null;
} }
foreach (var Entry in DAL) foreach (var Entry in DAL)
{ {
if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Key, this.DataAreaRegEx).Success) if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Key, this.DataAreaRegEx).Success)
continue; continue;
var MsTeam = await MsSharepoint.RequestGroupInfoAsync(Entry.Value);
if (MsTeam == null)
{
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? $"Konnte Team-Informationen für '{Entry.Key}' nicht abrufen");
continue;
}
var Team = new cLiamMsTeamsTeam(this, MsTeam);
DataAreas.Add(Team);
}
var MsTeam = await MsSharepoint.RequestGroupInfoAsync(Entry.Value);
if (MsTeam == null)
{
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? $"Konnte Team-Informationen für '{Entry.Key}' nicht abrufen");
continue;
}
var Team = new cLiamMsTeamsTeam(this, MsTeam);
DataAreas.Add(Team);
}
if (Depth > 0) if (Depth > 0)
{ {
var allChilds = new List<cLiamDataAreaBase>(); var allChilds = new List<cLiamDataAreaBase>();
@@ -161,97 +217,142 @@ namespace C4IT.LIAM
if (entryChilds != null && entryChilds.Count > 0) if (entryChilds != null && entryChilds.Count > 0)
allChilds.AddRange(entryChilds); allChilds.AddRange(entryChilds);
} }
DataAreas.AddRange(allChilds); DataAreas.AddRange(allChilds);
} }
SetLastError(null); SetLastError(null);
return DataAreas; return DataAreas;
} }
catch (Exception E) catch (Exception E)
{ {
LogException(E); LogException(E);
SetLastError(E.Message); SetLastError(E.Message);
} }
finally finally
{ {
LogMethodEnd(CM); LogMethodEnd(CM);
} }
return null; return null;
} }
private bool EnsureGraphPermissions(string accessToken) private bool EnsureGraphPermissions(string accessToken)
{ {
if (string.IsNullOrWhiteSpace(accessToken)) return EnsureGraphPermissions(accessToken, RequiredGraphPermissions, null);
{ }
SetLastError("Kein Access Token für Berechtigungsprüfung verfügbar");
return false; private bool EnsureClonePermissions(string accessToken, int partsToClone)
} {
var requirements = new List<GraphPermissionRequirement>(CloneBaseGraphPermissions);
try var cloneParts = (CloneTeamRequest.ClonableTeamParts)partsToClone;
{
var parts = accessToken.Split('.'); if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Apps))
if (parts.Length < 2) requirements.AddRange(CloneAppsGraphPermissions);
{ if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Settings))
SetLastError("Ungültiges Access Token"); requirements.AddRange(CloneSettingsGraphPermissions);
return false; if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Members))
} requirements.AddRange(CloneMemberGraphPermissions);
var payload = parts[1].Replace('-', '+').Replace('_', '/'); return EnsureGraphPermissions(accessToken, requirements, "Team-Klonen");
switch (payload.Length % 4) }
{
case 2: payload += "=="; break; private bool EnsureGraphPermissions(string accessToken, IEnumerable<GraphPermissionRequirement> requirements, string operationName)
case 3: payload += "="; break; {
} if (!TryGetGrantedGraphPermissions(accessToken, out var granted, out var errorMessage))
{
var payloadBytes = Convert.FromBase64String(payload); SetLastError(errorMessage);
var payloadJson = Encoding.UTF8.GetString(payloadBytes); return false;
var payloadObj = JsonConvert.DeserializeObject<JObject>(payloadJson); }
if (payloadObj == null)
{ var missing = requirements
SetLastError("Token-Payload konnte nicht gelesen werden"); .Where(requirement => requirement.AcceptedPermissions == null || !requirement.AcceptedPermissions.Any(granted.Contains))
return false; .Select(requirement => $"{requirement.Description} ({string.Join(" / ", requirement.AcceptedPermissions)})")
} .ToList();
var granted = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (missing.Count > 0)
{
if (payloadObj.TryGetValue("roles", out var rolesToken) && rolesToken is JArray roleArray) var prefix = string.IsNullOrWhiteSpace(operationName)
{ ? "Fehlende Graph-Berechtigungen: "
foreach (var role in roleArray.Values<string>()) : $"Fehlende Graph-Berechtigungen für {operationName}: ";
{ SetLastError(prefix + string.Join(", ", missing));
if (!string.IsNullOrWhiteSpace(role)) return false;
granted.Add(role); }
}
} return true;
}
if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken))
{ private bool TryGetGrantedGraphPermissions(string accessToken, out HashSet<string> granted, out string errorMessage)
var scopes = scopeToken.Value<string>() ?? string.Empty; {
foreach (var scope in scopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) granted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
granted.Add(scope); errorMessage = null;
}
if (string.IsNullOrWhiteSpace(accessToken))
var missing = RequiredGraphRoles.Where(required => !granted.Contains(required)).ToList(); {
if (missing.Count > 0) errorMessage = "Kein Access Token für Berechtigungsprüfung verfügbar";
{ return false;
SetLastError("Fehlende Graph-Berechtigungen: " + string.Join(", ", missing)); }
return false;
} try
{
return true; var parts = accessToken.Split('.');
} if (parts.Length < 2)
catch (Exception ex) {
{ errorMessage = "Ungültiges Access Token";
SetLastError("Berechtigungsprüfung fehlgeschlagen: " + ex.Message); return false;
return false; }
}
} var payload = parts[1].Replace('-', '+').Replace('_', '/');
switch (payload.Length % 4)
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID) {
{ case 2: payload += "=="; break;
var CM = MethodBase.GetCurrentMethod(); case 3: payload += "="; break;
LogMethodBegin(CM); }
try
var payloadBytes = Convert.FromBase64String(payload);
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
var payloadObj = JsonConvert.DeserializeObject<JObject>(payloadJson);
if (payloadObj == null)
{
errorMessage = "Token-Payload konnte nicht gelesen werden";
return false;
}
if (payloadObj.TryGetValue("roles", out var rolesToken) && rolesToken is JArray roleArray)
{
foreach (var role in roleArray.Values<string>())
{
if (!string.IsNullOrWhiteSpace(role))
granted.Add(role);
}
}
if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken))
{
var scopes = scopeToken.Value<string>() ?? string.Empty;
foreach (var scope in scopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
granted.Add(scope);
}
if (!granted.Any())
{
errorMessage = "Keine Graph-Berechtigungen im Access Token gefunden";
return false;
}
return true;
}
catch (Exception ex)
{
errorMessage = "Berechtigungsprüfung fehlgeschlagen: " + ex.Message;
return false;
}
}
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{ {
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId)) if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId))
{ {
@@ -340,15 +441,20 @@ namespace C4IT.LIAM
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override string GetLastErrorMessage() public override string GetLastErrorMessage()
{ {
if (!string.IsNullOrWhiteSpace(lastErrorMessage)) if (!string.IsNullOrWhiteSpace(lastErrorMessage))
return lastErrorMessage; return lastErrorMessage;
return MsSharepoint?.Base?.LastErrorMessage ?? string.Empty; return MsSharepoint?.Base?.LastErrorMessage ?? string.Empty;
} }
public async Task<cMsGraphResultBase> cloneTeam(string teamId, string name, string description, int visibility, int partsToClone, string additionalMembers, string additionalOwners) public async Task<cMsGraphResultBase> cloneTeam(string teamId, string name, string description, int visibility, int partsToClone, string additionalMembers, string additionalOwners)
{ {
if (!await LogonAsync())
return null;
if (!EnsureClonePermissions(MsSharepoint.Base?.AccessToken, partsToClone))
return null;
var request = new CloneTeamRequest() var request = new CloneTeamRequest()
{ {
DisplayName = name, DisplayName = name,

View File

@@ -53,6 +53,8 @@ namespace C4IT.LIAM
} }
public static Guid nftsModuleId = new Guid("77e213a1-6517-ea11-4881-000c2980fd94"); public static Guid nftsModuleId = new Guid("77e213a1-6517-ea11-4881-000c2980fd94");
private const string AdditionalConfigurationExcludePathsKey = "NtfsExcludePaths";
private const string AdditionalConfigurationIncludePathsKey = "NtfsIncludePaths";
public readonly cNtfsBase ntfsBase = new cNtfsBase(); public readonly cNtfsBase ntfsBase = new cNtfsBase();
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase(); public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, HashSet<string>> publishedShareCache = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
@@ -208,6 +210,9 @@ namespace C4IT.LIAM
if (!await LogonAsync()) if (!await LogonAsync())
return null; return null;
var classification = ClassifyPath(UID); var classification = ClassifyPath(UID);
if (!PathsEqual(classification?.NormalizedPath, this.RootPath) && !ShouldIncludeDataArea(classification))
return null;
return await BuildDataAreaAsync(classification); return await BuildDataAreaAsync(classification);
} }
catch (Exception E) catch (Exception E)
@@ -371,14 +376,16 @@ namespace C4IT.LIAM
foreach (var childPath in GetServerRootChildPaths(parentClassification.NormalizedPath)) foreach (var childPath in GetServerRootChildPaths(parentClassification.NormalizedPath))
{ {
var childClassification = ClassifyPath(childPath); var childClassification = ClassifyPath(childPath);
if (!ShouldIncludeDataArea(childClassification.DisplayName)) if (!ShouldTraverseDataArea(childClassification))
continue; continue;
var childDataArea = await BuildDataAreaAsync(childClassification); if (ShouldIncludeDataArea(childClassification))
if (childDataArea == null) {
continue; var childDataArea = await BuildDataAreaAsync(childClassification);
if (childDataArea != null)
children.Add(childDataArea);
}
children.Add(childDataArea);
if (depth > 1) if (depth > 1)
children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1)); children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1));
} }
@@ -393,14 +400,16 @@ namespace C4IT.LIAM
foreach (var entry in folderEntries.Values.OfType<cNtfsResultFolder>()) foreach (var entry in folderEntries.Values.OfType<cNtfsResultFolder>())
{ {
var childClassification = ClassifyPath(entry.Path); var childClassification = ClassifyPath(entry.Path);
if (!ShouldIncludeDataArea(childClassification.DisplayName)) if (!ShouldTraverseDataArea(childClassification))
continue; continue;
var childDataArea = await BuildDataAreaAsync(childClassification, entry); if (ShouldIncludeDataArea(childClassification))
if (childDataArea == null) {
continue; var childDataArea = await BuildDataAreaAsync(childClassification, entry);
if (childDataArea != null)
children.Add(childDataArea);
}
children.Add(childDataArea);
if (depth > 1) if (depth > 1)
children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1)); children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1));
} }
@@ -420,7 +429,55 @@ namespace C4IT.LIAM
.Select(shareName => BuildUncPath(new[] { serverName, shareName }, 2)); .Select(shareName => BuildUncPath(new[] { serverName, shareName }, 2));
} }
private bool ShouldIncludeDataArea(string displayName) private bool ShouldIncludeDataArea(cNtfsPathClassification classification)
{
if (classification == null)
return false;
if (!MatchesDataAreaRegEx(classification.DisplayName))
return false;
string matchingConfigurationKey;
string matchingRule;
if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule))
{
LogEntry($"Skip NTFS path '{classification.NormalizedPath}' due to AdditionalConfiguration rule '{matchingConfigurationKey}={matchingRule}'", LogLevels.Debug);
return false;
}
if (!IsPathWhitelisted(classification, true, out matchingConfigurationKey, out matchingRule))
{
LogEntry($"Skip NTFS path '{classification.NormalizedPath}' because no AdditionalConfiguration whitelist matched", LogLevels.Debug);
return false;
}
return true;
}
private bool ShouldTraverseDataArea(cNtfsPathClassification classification)
{
if (classification == null)
return false;
string matchingConfigurationKey;
string matchingRule;
if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule))
{
LogEntry($"Skip NTFS subtree '{classification.NormalizedPath}' due to AdditionalConfiguration rule '{matchingConfigurationKey}={matchingRule}'", LogLevels.Debug);
return false;
}
if (!HasAdditionalConfigurationValues(AdditionalConfigurationIncludePathsKey))
return true;
if (IsPathWhitelisted(classification, true, out matchingConfigurationKey, out matchingRule))
return true;
LogEntry($"Skip NTFS subtree '{classification.NormalizedPath}' because it is outside AdditionalConfiguration whitelist '{AdditionalConfigurationIncludePathsKey}'", LogLevels.Debug);
return false;
}
private bool MatchesDataAreaRegEx(string displayName)
{ {
if (string.IsNullOrEmpty(this.DataAreaRegEx)) if (string.IsNullOrEmpty(this.DataAreaRegEx))
return true; return true;
@@ -428,6 +485,197 @@ namespace C4IT.LIAM
return Regex.Match(displayName ?? string.Empty, this.DataAreaRegEx).Success; return Regex.Match(displayName ?? string.Empty, this.DataAreaRegEx).Success;
} }
private bool IsPathBlacklisted(cNtfsPathClassification classification, out string matchingConfigurationKey, out string matchingRule)
{
return TryMatchPathPolicy(classification, AdditionalConfigurationExcludePathsKey, false, out matchingConfigurationKey, out matchingRule);
}
private bool IsPathWhitelisted(cNtfsPathClassification classification, bool allowPathAncestorMatches, out string matchingConfigurationKey, out string matchingRule)
{
matchingConfigurationKey = null;
matchingRule = null;
if (!HasAdditionalConfigurationValues(AdditionalConfigurationIncludePathsKey))
return true;
return TryMatchPathPolicy(classification, AdditionalConfigurationIncludePathsKey, allowPathAncestorMatches, out matchingConfigurationKey, out matchingRule);
}
private bool TryMatchPathPolicy(cNtfsPathClassification classification, string key, bool allowPathAncestorMatches, out string matchingConfigurationKey, out string matchingRule)
{
matchingConfigurationKey = null;
matchingRule = null;
if (classification == null || string.IsNullOrWhiteSpace(key))
return false;
var patterns = GetAdditionalConfigurationValues(key).ToList();
if (patterns.Count == 0)
return false;
foreach (var pattern in patterns)
{
if (!MatchesPathPolicy(classification, pattern))
continue;
matchingConfigurationKey = key;
matchingRule = pattern;
return true;
}
if (!allowPathAncestorMatches)
return false;
foreach (var pattern in patterns)
{
if (!CanPathLeadToPattern(classification, pattern))
continue;
matchingConfigurationKey = key;
matchingRule = pattern;
return true;
}
return false;
}
private bool HasAdditionalConfigurationValues(string key)
{
return GetAdditionalConfigurationValues(key).Any();
}
private IEnumerable<string> GetAdditionalConfigurationValues(string key)
{
if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key))
return Enumerable.Empty<string>();
string rawValue;
if (!AdditionalConfiguration.TryGetValue(key, out rawValue) || string.IsNullOrWhiteSpace(rawValue))
return Enumerable.Empty<string>();
return rawValue
.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private string GetRelativePathFromRoot(string path)
{
var normalizedRoot = NormalizeUncPath(this.RootPath);
var normalizedPath = NormalizeUncPath(path);
if (string.IsNullOrWhiteSpace(normalizedRoot) || string.IsNullOrWhiteSpace(normalizedPath))
return string.Empty;
if (PathsEqual(normalizedRoot, normalizedPath))
return string.Empty;
var rootWithSeparator = normalizedRoot + "\\";
if (!normalizedPath.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
return normalizedPath;
return normalizedPath.Substring(rootWithSeparator.Length)
.Trim()
.TrimStart('\\')
.Replace('/', '\\');
}
private bool MatchesPathPolicy(cNtfsPathClassification classification, string pattern)
{
if (classification == null || string.IsNullOrWhiteSpace(pattern))
return false;
foreach (var candidate in GetPathPolicyCandidates(classification))
{
if (MatchesAdditionalConfigurationPattern(candidate, pattern))
return true;
}
return false;
}
private IEnumerable<string> GetPathPolicyCandidates(cNtfsPathClassification classification)
{
if (classification == null)
return Enumerable.Empty<string>();
var candidates = new List<string>();
var relativePath = GetRelativePathFromRoot(classification.NormalizedPath);
if (!string.IsNullOrWhiteSpace(relativePath))
candidates.Add(relativePath);
if (!string.IsNullOrWhiteSpace(classification.NormalizedPath))
candidates.Add(classification.NormalizedPath);
return candidates
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private bool MatchesAdditionalConfigurationPattern(string value, string pattern)
{
if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(pattern))
return false;
var normalizedValue = value.Trim().Replace('/', '\\').Trim('\\');
var normalizedPattern = pattern.Trim().Replace('/', '\\').Trim('\\');
if (string.IsNullOrWhiteSpace(normalizedPattern))
return false;
var regexPattern = "^" + Regex.Escape(normalizedPattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(normalizedValue, regexPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private bool CanPathLeadToPattern(cNtfsPathClassification classification, string pattern)
{
if (classification == null || string.IsNullOrWhiteSpace(pattern))
return false;
foreach (var candidate in GetPathPolicyCandidates(classification))
{
if (IsPathAncestorOfPattern(candidate, pattern))
return true;
}
return false;
}
private bool IsPathAncestorOfPattern(string path, string pattern)
{
var normalizedPath = (path ?? string.Empty).Trim().Replace('/', '\\').Trim('\\');
var normalizedPattern = (pattern ?? string.Empty).Trim().Replace('/', '\\').Trim('\\');
if (string.IsNullOrWhiteSpace(normalizedPath) || string.IsNullOrWhiteSpace(normalizedPattern))
return false;
var pathSegments = normalizedPath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
var patternSegments = normalizedPattern.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
if (pathSegments.Length > patternSegments.Length)
return false;
for (var segmentIndex = 0; segmentIndex < pathSegments.Length; segmentIndex++)
{
if (segmentIndex >= patternSegments.Length)
return false;
if (!MatchesPatternSegment(pathSegments[segmentIndex], patternSegments[segmentIndex]))
return false;
}
return true;
}
private bool MatchesPatternSegment(string valueSegment, string patternSegment)
{
if (string.IsNullOrWhiteSpace(valueSegment) || string.IsNullOrWhiteSpace(patternSegment))
return false;
var regexPattern = "^" + Regex.Escape(patternSegment).Replace("\\*", ".*") + "$";
return Regex.IsMatch(valueSegment, regexPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
private List<string> GetDfsObjectPrefixes(string path) private List<string> GetDfsObjectPrefixes(string path)
{ {
var normalizedPath = NormalizeUncPath(path); var normalizedPath = NormalizeUncPath(path);
@@ -645,15 +893,44 @@ namespace C4IT.LIAM
IEnumerable<string> ownerSids, IEnumerable<string> ownerSids,
IEnumerable<string> readerSids, IEnumerable<string> readerSids,
IEnumerable<string> writerSids, IEnumerable<string> writerSids,
bool allowSharePathEnsure = false,
bool ensureTraverseGroups = false, bool ensureTraverseGroups = false,
bool whatIf = false) bool whatIf = false)
{ {
if (!IsPermissionManagedFolderPath(folderPath)) var classification = ClassifyPath(folderPath);
var allowShareKinds = allowSharePathEnsure;
if (!IsSupportedPermissionManagedPathKind(
classification,
allowShareKinds
? new[] { eNtfsPathKind.Folder, eNtfsPathKind.ClassicShare, eNtfsPathKind.DfsLink }
: new[] { eNtfsPathKind.Folder }))
{ {
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
{ {
resultErrorId = 30008, resultErrorId = 30008,
resultMessage = $"NTFS permission ensure is only supported for folder paths. Shares, DFS namespaces and server roots are skipped: {folderPath}" resultMessage = allowShareKinds
? $"NTFS permission ensure is only supported for folder and share paths. DFS namespaces and server roots are skipped: {folderPath}"
: $"NTFS permission ensure is only supported for folder paths unless share support is explicitly enabled. Shares, DFS namespaces and server roots are skipped: {folderPath}"
});
}
string matchingConfigurationKey;
string matchingRule;
if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule))
{
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
{
resultErrorId = 30008,
resultMessage = $"NTFS permission ensure skipped for '{folderPath}' due to AdditionalConfiguration rule '{matchingConfigurationKey}={matchingRule}'."
});
}
if (!IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule))
{
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
{
resultErrorId = 30008,
resultMessage = $"NTFS permission ensure skipped for '{folderPath}' because no AdditionalConfiguration whitelist matched."
}); });
} }
@@ -667,7 +944,12 @@ namespace C4IT.LIAM
writerSids); writerSids);
engine.WhatIf = whatIf; engine.WhatIf = whatIf;
return Task.FromResult(engine.ensureDataAreaPermissions(ensureTraverseGroups)); var allowTraverseGroups = classification.Kind == eNtfsPathKind.Folder && ensureTraverseGroups;
var resultToken = engine.ensureDataAreaPermissions(allowTraverseGroups);
if (!allowTraverseGroups && ensureTraverseGroups)
resultToken.warnings.Add($"Traverse groups are currently only ensured for folder paths. Traverse processing was skipped for '{folderPath}'.");
return Task.FromResult(resultToken);
} }
private DataArea_FileSystem CreateFilesystemEngine( private DataArea_FileSystem CreateFilesystemEngine(
@@ -737,9 +1019,35 @@ namespace C4IT.LIAM
} }
public bool IsPermissionManagedFolderPath(string path) public bool IsPermissionManagedFolderPath(string path)
{
return IsPermissionManagedPath(path, eNtfsPathKind.Folder);
}
public bool IsPermissionManagedSharePath(string path)
{
return IsPermissionManagedPath(path, eNtfsPathKind.ClassicShare, eNtfsPathKind.DfsLink);
}
private bool IsPermissionManagedPath(string path, params eNtfsPathKind[] supportedKinds)
{ {
var classification = ClassifyPath(path); var classification = ClassifyPath(path);
return classification != null && classification.Kind == eNtfsPathKind.Folder; if (!IsSupportedPermissionManagedPathKind(classification, supportedKinds))
return false;
string matchingConfigurationKey;
string matchingRule;
if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule))
return false;
return IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule);
}
private static bool IsSupportedPermissionManagedPathKind(cNtfsPathClassification classification, params eNtfsPathKind[] supportedKinds)
{
if (classification == null || supportedKinds == null || supportedKinds.Length == 0)
return false;
return supportedKinds.Contains(classification.Kind);
} }
private IEnumerable<IAM_SecurityGroupTemplate> BuildSecurityGroupTemplates() private IEnumerable<IAM_SecurityGroupTemplate> BuildSecurityGroupTemplates()

View File

@@ -539,11 +539,24 @@ namespace C4IT_IAM_SET
null, null,
Helper.MaxAdGroupNameLength, Helper.MaxAdGroupNameLength,
$"Traverse fuer '{parent.FullName}'"); $"Traverse fuer '{parent.FullName}'");
var boundedTraverseDescriptionContext = Helper.GetBoundedAdGroupTemplateContext(
traverseGroupTemplate.DescriptionTemplate,
true,
relativePath,
sanitizedSegments,
folderName,
null,
Helper.MaxAdGroupDescriptionLength,
$"Traverse fuer '{parent.FullName}'",
"AD-Gruppenbeschreibung");
var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty<string>(); var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedTraverseRelativePath = adjustedTraverseSegments.Length > 0 ? string.Join("_", adjustedTraverseSegments) : string.Empty; var adjustedTraverseRelativePath = adjustedTraverseSegments.Length > 0 ? string.Join("_", adjustedTraverseSegments) : string.Empty;
var adjustedTraverseFolderName = boundedTraverseContext.FolderName; var adjustedTraverseFolderName = boundedTraverseContext.FolderName;
var adjustedTraverseDescriptionSegments = boundedTraverseDescriptionContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedTraverseDescriptionRelativePath = adjustedTraverseDescriptionSegments.Length > 0 ? string.Join("_", adjustedTraverseDescriptionSegments) : string.Empty;
var adjustedTraverseDescriptionFolderName = boundedTraverseDescriptionContext.FolderName;
var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName); var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName);
var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName); var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseDescriptionRelativePath, adjustedTraverseDescriptionSegments, adjustedTraverseDescriptionFolderName);
string traverseRegex = null; string traverseRegex = null;
try try

View File

@@ -12,6 +12,7 @@ namespace C4IT_IAM_Engine
public static class Helper public static class Helper
{ {
public const int MaxAdGroupNameLength = 64; public const int MaxAdGroupNameLength = 64;
public const int MaxAdGroupDescriptionLength = 1024;
public const int MaxAdGroupLoopDigits = 3; public const int MaxAdGroupLoopDigits = 3;
private const int MinLeadingRelativePathSegmentLength = 3; private const int MinLeadingRelativePathSegmentLength = 3;
private const int MinSingleLeadingRelativePathSegmentLength = 2; private const int MinSingleLeadingRelativePathSegmentLength = 2;
@@ -74,7 +75,8 @@ namespace C4IT_IAM_Engine
string folderName, string folderName,
IDictionary<string, string> replacementTags, IDictionary<string, string> replacementTags,
int maxLength, int maxLength,
string logContext) string logContext,
string valueLabel = "AD-Gruppenname")
{ {
var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray(); var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray();
var effectiveFolderName = folderName ?? string.Empty; var effectiveFolderName = folderName ?? string.Empty;
@@ -132,14 +134,14 @@ namespace C4IT_IAM_Engine
{ {
cLogManager.DefaultLogger.LogEntry( cLogManager.DefaultLogger.LogEntry(
LogLevels.Warning, LogLevels.Warning,
$"AD-Gruppenname gekuerzt ({logContext}): '{result.OriginalValue}' ({GetMeasuredTemplateLength(result.OriginalValue)}) -> '{result.FinalValue}' ({GetMeasuredTemplateLength(result.FinalValue)}), Strategie: {result.Strategy}, Limit: {maxLength}."); $"{valueLabel} gekuerzt ({logContext}): '{result.OriginalValue}' ({GetMeasuredTemplateLength(result.OriginalValue)}) -> '{result.FinalValue}' ({GetMeasuredTemplateLength(result.FinalValue)}), Strategie: {result.Strategy}, Limit: {maxLength}.");
} }
if (measuredValue.Length > maxLength) if (measuredValue.Length > maxLength)
{ {
cLogManager.DefaultLogger.LogEntry( cLogManager.DefaultLogger.LogEntry(
LogLevels.Warning, LogLevels.Warning,
$"AD-Gruppenname ueberschreitet weiterhin das sichere Limit ({logContext}): '{result.FinalValue}' ({measuredValue.Length}), Limit: {maxLength}."); $"{valueLabel} ueberschreitet weiterhin das sichere Limit ({logContext}): '{result.FinalValue}' ({measuredValue.Length}), Limit: {maxLength}.");
} }
return result; return result;

View File

@@ -208,20 +208,34 @@ namespace C4IT_IAM_Engine
Helper.MaxAdGroupNameLength, Helper.MaxAdGroupNameLength,
$"{template.Type}/{template.Scope} fuer '{newFolderPath}'"); $"{template.Type}/{template.Scope} fuer '{newFolderPath}'");
var adjustedSegments = boundedNameContext.SanitizedSegments ?? Array.Empty<string>(); var boundedDescriptionContext = Helper.GetBoundedAdGroupTemplateContext(
var adjustedRelativePath = adjustedSegments.Length > 0 ? string.Join("_", adjustedSegments) : string.Empty; template.DescriptionTemplate,
var adjustedFolderName = boundedNameContext.FolderName; template.Type != SecurityGroupType.Traverse,
relativePath,
sanitizedSegments,
folderName,
replacementTags,
Helper.MaxAdGroupDescriptionLength,
$"{template.Type}/{template.Scope} fuer '{newFolderPath}'",
"AD-Gruppenbeschreibung");
template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName) var adjustedNameSegments = boundedNameContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedNameRelativePath = adjustedNameSegments.Length > 0 ? string.Join("_", adjustedNameSegments) : string.Empty;
var adjustedNameFolderName = boundedNameContext.FolderName;
var adjustedDescriptionSegments = boundedDescriptionContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedDescriptionRelativePath = adjustedDescriptionSegments.Length > 0 ? string.Join("_", adjustedDescriptionSegments) : string.Empty;
var adjustedDescriptionFolderName = boundedDescriptionContext.FolderName;
template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();
template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName) template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedDescriptionRelativePath, adjustedDescriptionSegments, adjustedDescriptionFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();
template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName) template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedNameRelativePath, adjustedNameSegments, adjustedNameFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();

View File

@@ -909,7 +909,7 @@ namespace C4IT.LIAM.Activities
public InArgument<Guid> ConfigID { get; set; } public InArgument<Guid> ConfigID { get; set; }
[Category("Input")] [Category("Input")]
[DisplayName("Folder Path")] [DisplayName("Path")]
[RequiredArgument] [RequiredArgument]
public InArgument<string> FolderPath { get; set; } public InArgument<string> FolderPath { get; set; }

View File

@@ -198,12 +198,14 @@ namespace LiamWorkflowActivities
return result; return result;
} }
var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "AllowManualNtfsPermissionEnsureForShares");
var token = await ntfsProvider.EnsureMissingPermissionGroupsAsync( var token = await ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath, folderPath,
customTags, customTags,
NormalizeIdentifierList(ownerSids), NormalizeIdentifierList(ownerSids),
NormalizeIdentifierList(readerSids), NormalizeIdentifierList(readerSids),
NormalizeIdentifierList(writerSids), NormalizeIdentifierList(writerSids),
allowSharePathEnsure,
ensureTraverseGroups, ensureTraverseGroups,
IsWorkflowWhatIfEnabled(provider)); IsWorkflowWhatIfEnabled(provider));
if (token == null) if (token == null)
@@ -417,10 +419,16 @@ namespace LiamWorkflowActivities
if (!(provider is cLiamProviderNtfs ntfsProvider)) if (!(provider is cLiamProviderNtfs ntfsProvider))
return true; return true;
if (!IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups")) var allowFolderEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups");
var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroupsForShares");
if (!allowFolderEnsure && !allowSharePathEnsure)
return true; return true;
foreach (var ntfsArea in dataAreas.OfType<cLiamNtfsFolder>()) foreach (var ntfsArea in dataAreas
.Where(dataArea =>
allowFolderEnsure && dataArea is cLiamNtfsFolder
|| allowSharePathEnsure && dataArea is cLiamNtfsShare)
.Cast<cLiamNtfsPermissionDataAreaBase>())
{ {
var folderPath = ntfsArea.TechnicalName; var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath)) if (string.IsNullOrWhiteSpace(folderPath))
@@ -438,6 +446,7 @@ namespace LiamWorkflowActivities
null, null,
null, null,
null, null,
allowSharePathEnsure,
false, false,
simulateOnly); simulateOnly);
if (ensureResult == null) if (ensureResult == null)
@@ -660,7 +669,8 @@ namespace LiamWorkflowActivities
ConfigurationId = configurationId ?? string.Empty, ConfigurationId = configurationId ?? string.Empty,
BaseFolder = ntfsFolder?.Share?.TechnicalName ?? dataArea.Provider?.RootPath ?? string.Empty, BaseFolder = ntfsFolder?.Share?.TechnicalName ?? dataArea.Provider?.RootPath ?? string.Empty,
UniqueId = dataArea.UID ?? string.Empty, UniqueId = dataArea.UID ?? string.Empty,
DataAreaType = dataArea.DataType.ToString() DataAreaType = dataArea.DataType.ToString(),
DataAreaTypeId = (int)dataArea.DataType
}; };
} }

View File

@@ -0,0 +1,29 @@
@echo off
setlocal EnableDelayedExpansion
set "ProductName=C4IT Light Identity Access Management"
set "SignTool=..\..\Common Code\Tools\signtool.exe"
set "TimeStamp=http://rfc3161timestamp.globalsign.com/advanced"
set "FileList="
if exist ".\bin\Release\LiamWorkflowDiagnostics.exe" (
set "FileList=!FileList! ".\bin\Release\LiamWorkflowDiagnostics.exe""
)
for %%F in (".\bin\Release\Liam*.dll") do (
if exist "%%~fF" (
set "FileList=!FileList! "%%F""
)
)
if not defined FileList (
echo No matching release binaries found to sign.
pause
exit /b 1
)
echo Signing all matching files at once...
call "%SignTool%" sign /a /tr %TimeStamp% /td SHA256 /fd SHA256 /d "%ProductName%" !FileList!
pause

BIN
Sonstiges/Icon1.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,307 @@
# LIAM NTFS AdditionalConfiguration Blacklist / Whitelist - Technischer Entwurf
## Ziel
Dieses Dokument beschreibt einen kleinen technischen Entwurf, um dem NTFS-Provider ueber `AdditionalConfiguration` eine Blacklist und spaeter optional auch eine Whitelist fuer NTFS-Pfade mitzugeben.
Die Policy soll klassifizierungsunabhaengig arbeiten und damit fuer Shares, DFS-Links, DFS-Namespaces und Folder dieselbe Matching-Logik verwenden.
## Ausgangslage
Der NTFS-Provider verfuegt heute bereits ueber `AdditionalConfiguration`, nutzt diese im NTFS-Code aber im Wesentlichen nur fuer boolesche Feature-Flags.
Die Ermittlung der DataAreas erfolgt aktuell rekursiv ueber:
- Root-Aufbau in `getDataAreasAsync()`
- Kind-Ermittlung in `GetChildDataAreasAsync()`
- Dateisystem-Enumeration je Ebene ueber `ntfsBase.RequestFoldersListAsync(parentPath, 1)`
Der bestehende Filter `ShouldIncludeDataArea()` wirkt nur auf den `DisplayName` und basiert auf `DataAreaRegEx`. Das ist fuer gezielte Ordnerausschluesse fachlich und technisch zu grob.
## Zielbild
Die neue Logik soll eine explizite Pfad-Policy fuer den NTFS-Provider einfuehren.
Diese Policy entscheidet fuer jeden gefundenen Pfad:
- darf als DataArea materialisiert werden
- darf weiter traversiert werden
Ein ausgeschlossener Pfad soll weder als DataArea geliefert noch weiter traversiert werden.
## Vorgeschlagene Konfigurationsschluessel
### Aktueller Stand
- `NtfsExcludePaths`
- `NtfsIncludePaths`
Beispiel:
```text
NtfsExcludePaths=Archiv;*\Temp;Abteilung\Alt;\\server\share\legacy\*
NtfsIncludePaths=Fachbereiche\*;Shares\Produktion\*;\\server\dfs\namespace\link\*
```
## Aktueller Implementierungsstand
Die nachfolgend beschriebene Path-Policy ist im NTFS-Provider inzwischen implementiert.
### 1. Materialisierung und Traversierung sind getrennt
In `GetChildDataAreasAsync()` wird pro gefundenem Pfad heute getrennt entschieden:
- darf der Pfad als DataArea geliefert werden
- darf unterhalb des Pfads weiter traversiert werden
Das ist wichtig fuer Include-Regeln. Ein Zwischenpfad darf fuer die Traversierung erlaubt sein, auch wenn erst ein tiefer liegendes Zielobjekt tatsaechlich auf der Whitelist steht.
### 2. Matching ist klassifizierungsunabhaengig
Die Path-Policy arbeitet fuer:
- `ServerRoot`
- `ClassicShare`
- `DfsNamespaceRoot`
- `DfsLink`
- `Folder`
mit derselben Matching-Logik.
Die Klassifikation bleibt fuer die fachliche Verarbeitung der DataArea relevant, nicht mehr fuer Blacklist-/Whitelist-Matching.
### 3. Relative und absolute Pfade werden parallel ausgewertet
Jeder klassifizierte Pfad wird gegen mehrere Kandidaten gematcht:
- relativer Pfad unterhalb von `RootPath`
- normalisierter absoluter UNC-Pfad
Dadurch koennen Regeln sowohl knapp relativ als auch explizit absolut formuliert werden.
### 4. Include-Regeln duerfen Traversierungs-Vorpfade freischalten
Wenn `NtfsIncludePaths` gesetzt ist, darf ein Pfad auch dann traversiert werden, wenn er selbst noch nicht final matcht, aber zu einem spaeter passenden Zielpfad fuehren kann.
Beispiel:
```text
NtfsIncludePaths=Abteilung\IT\*
```
Dann darf `Abteilung` fuer die Traversierung erhalten bleiben, damit `Abteilung\IT\TeamA` ueberhaupt erreicht werden kann.
### 5. `LoadDataArea()` respektiert die Policy
Direktes Laden eines Pfads ueber `LoadDataArea()` wird ebenfalls durch die Path-Policy eingeschraenkt.
Ausnahme:
- der konfigurierte `RootPath` selbst bleibt ladbar
Damit kann die Filterung nicht einfach durch direktes Laden einer UID umgangen werden.
### 6. Permission-Management bleibt fachlich auf Folder beschraenkt
`IsPermissionManagedFolderPath()` verwendet dieselbe generische Path-Policy, bleibt aber weiterhin nur fuer als `Folder` klassifizierte Pfade zulaessig.
Die Path-Policy ist also klassifizierungsunabhaengig, das Berechtigungs-Handling selbst aber bewusst nicht.
## Matching-Regeln
Empfohlene Semantik:
- Trennzeichen fuer Mehrfachwerte: `;`
- Auswertung case-insensitive
- Leerzeichen an Eintraegen vor dem Match trimmen
- Matching gegen relative Pfade unter `RootPath` und gegen normalisierte absolute UNC-Pfade
- Interne Normalisierung auf konsistente UNC-/Directory-Notation
- Zunaechst nur einfache Wildcards `*` unterstuetzen, keine freien Regex-Ausdruecke
- Include-Regeln duerfen fuer die Traversierung auch uebergeordnete Pfade freischalten, wenn diese zu einem spaeter passenden Zielpfad fuehren
Empfohlene Prioritaet:
1. Wenn keine Include-Regel gesetzt ist, sind alle Pfade grundsaetzlich erlaubt.
2. Wenn Include-Regeln gesetzt sind, sind nur passende Pfade erlaubt.
3. Exclude-Regeln werden danach angewendet und gewinnen bei Kollision.
Das entspricht dem aktuell implementierten Verhalten.
## Beispiele
### 1. Einzelnen Teilbaum ausschliessen
Konfiguration:
```text
NtfsExcludePaths=Abteilung\Alt\*
```
Wirkung:
- `Abteilung\Alt` und alles darunter wird nicht mehr als DataArea geliefert
- unterhalb von `Abteilung\Alt` wird auch nicht weiter traversiert
- andere Teilbaeume bleiben unveraendert sichtbar
### 2. Bestimmte Ordnernamen ueberall ausschliessen
Konfiguration:
```text
NtfsExcludePaths=*\Temp;*\Archiv
```
Wirkung:
- jeder Pfad, dessen letzter oder einer spaeteren Segmente `Temp` oder `Archiv` entspricht, wird ausgeschlossen
- das ist praktisch fuer technische oder historische Unterordner, die in vielen Shares gleich benannt sind
### 3. Nur einen Fachbereich sichtbar lassen
Konfiguration:
```text
NtfsIncludePaths=Fachbereiche\HR\*
```
Wirkung:
- nur Pfade unterhalb von `Fachbereiche\HR` werden als DataAreas geliefert
- notwendige Zwischenpfade wie `Fachbereiche` duerfen fuer die Traversierung erhalten bleiben
- alle anderen Teilbaeume unterhalb von `RootPath` fallen aus der Ergebnismenge
### 4. Whitelist und Blacklist kombinieren
Konfiguration:
```text
NtfsIncludePaths=Fachbereiche\IT\*
NtfsExcludePaths=Fachbereiche\IT\Test;Fachbereiche\IT\Alt\*
```
Wirkung:
- grundsaetzlich ist nur `Fachbereiche\IT` relevant
- innerhalb dieses Bereichs werden `Test` und der komplette Teilbaum `Alt` wieder ausgeschlossen
- Exclude gewinnt also auch innerhalb eines eingeschraenkten Include-Bereichs
### 5. Absoluten UNC-Pfad fuer DFS-Link verwenden
Konfiguration:
```text
NtfsIncludePaths=\\server\dfs\namespace\link\Produktion\*
```
Wirkung:
- die Regel greift auch dann, wenn der Root ueber DFS klassifiziert wird
- benoetigte Vorpfade wie `\\server\dfs`, `\\server\dfs\namespace` und `\\server\dfs\namespace\link` duerfen fuer die Traversierung erhalten bleiben
- dadurch kann ein bestimmter DFS-Zweig sehr gezielt freigegeben werden
### 6. Nur bestimmte Shares unter einem Server-Root zulassen
Konfiguration:
```text
NtfsIncludePaths=ShareA\*;ShareB\*
```
Voraussetzung:
- `RootPath` zeigt auf einen Server-Root wie `\\fileserver`
Wirkung:
- nur Kinder unterhalb von `ShareA` und `ShareB` werden sichtbar
- andere Shares des Servers werden nicht materialisiert und nicht weiter traversiert
### 7. Direktes Laden eines ausgeschlossenen Pfads
Konfiguration:
```text
NtfsExcludePaths=Abteilung\Alt\*
```
Wirkung:
- ein direkter `LoadDataArea()` auf einen Pfad unterhalb von `Abteilung\Alt` liefert kein Objekt mehr
- der konfigurierte `RootPath` selbst bleibt davon ausgenommen und kann weiterhin geladen werden
## Technische Einhaengepunkte
### 1. Provider-seitige Policy-Methoden
Im NTFS-Provider existiert dafuer inzwischen eine kleine Policy-Schicht, insbesondere:
- `GetAdditionalConfigurationValues(string key)`
- `ShouldIncludeDataArea(...)`
- `ShouldTraverseDataArea(...)`
- `MatchesPathPolicy(...)`
- `TryMatchPathPolicy(...)`
- `CanPathLeadToPattern(...)`
Die Methode `IsAdditionalConfigurationEnabled()` bleibt fuer boolesche Flags bestehen und wird durch Listen-/String-Helfer ergaenzt.
### 2. Anwendung in der DataArea-Traversierung
Der erste und wichtigste Einhaengepunkt ist `GetChildDataAreasAsync()`.
Dort wird heute jede gefundene Ebene verarbeitet und vor `BuildDataAreaAsync()` sowie vor dem rekursiven Abstieg durch die Path-Policy geprueft.
Vorteil:
- geringe Eingriffstiefe
- kein Umbau der allgemeinen NTFS-Basis erforderlich
- fachliche Wirkung genau dort, wo DataAreas erzeugt werden
### 3. Wiederverwendung fuer Permission-Management
`IsPermissionManagedFolderPath()` bleibt fachlich auf Folder beschraenkt, verwendet fuer Black-/Whitelist aber dieselbe generische Path-Policy.
Damit wird vermieden, dass ein Ordner zwar nicht mehr als DataArea sichtbar ist, aber weiterhin im Permission-Flow auftaucht.
### 4. Wiederverwendung fuer `LoadDataArea()`
Auch `LoadDataArea()` verwendet die Path-Policy inzwischen, damit gefilterte Pfade nicht per Direktzugriff wieder sichtbar werden.
## Offene Abgrenzung
Bewusst weiterhin nicht umgesetzt:
- Umbau von `cNtfsBase` auf generische Filter-Callbacks
- freie Regex-Konfiguration in `AdditionalConfiguration`
- unterschiedliche Regeln je Klassifikationstyp
Diese Themen koennen spaeter folgen, sind fuer den aktuellen Nutzen aber nicht noetig.
## Logging
Fuer ausgeschlossene Pfade sollte auf `Debug` geloggt werden:
- welcher Pfad verworfen wurde
- welche Regel gegriffen hat
- ob der Pfad nur nicht materialisiert oder auch nicht traversiert wurde
Beispiel:
```text
Skip NTFS path '\\server\share\IT\_disabled' due to AdditionalConfiguration rule 'NtfsExcludePaths=IT\_disabled'
```
Das ist wichtig, damit fehlende DataAreas spaeter im Betrieb nachvollziehbar bleiben.
## Empfohlener Umsetzungsplan
1. Listenparser fuer `AdditionalConfiguration` im NTFS-Provider einfuehren.
2. Pfadnormalisierung und Matching fuer relative sowie absolute Pfade kapseln.
3. `GetChildDataAreasAsync()` um `ShouldTraverseDataArea(...)` erweitern.
4. Debug-Logging fuer Skip-Faelle einfuehren.
5. Dieselbe Policy in `IsPermissionManagedFolderPath()` und `LoadDataArea()` wiederverwenden.
Diese Punkte sind im aktuellen Implementierungsstand umgesetzt.
## Kurzfazit
Die generische Path-Policy im NTFS-Provider ist klein genug fuer eine risikoarme Implementierung, passt in die vorhandene `AdditionalConfiguration`-Architektur und arbeitet ohne Sonderregeln pro Klassifikationstyp.