Add NTFS automatic ensure timeout diagnostics

This commit is contained in:
Meik
2026-07-16 12:42:37 +02:00
parent e7fb041ff5
commit 0b1dbd7925
3 changed files with 201 additions and 12 deletions

View File

@@ -1146,7 +1146,17 @@ namespace C4IT.LIAM
bool ensureTraverseGroups = false, bool ensureTraverseGroups = false,
bool whatIf = false) bool whatIf = false)
{ {
var stopwatch = Stopwatch.StartNew();
LogEntry(
$"NTFS permission ensure provider start. Stage=Start Path='{folderPath}', AllowSharePathEnsure={allowSharePathEnsure}, EnsureTraverseGroups={ensureTraverseGroups}, WhatIf={whatIf}",
LogLevels.Debug);
LogEntry($"NTFS permission ensure provider stage. Stage=Classify Path='{folderPath}'", LogLevels.Debug);
var classification = ClassifyPath(folderPath); var classification = ClassifyPath(folderPath);
LogEntry(
$"NTFS permission ensure provider stage finished. Stage=Classify Path='{folderPath}', PathKind='{classification?.Kind}', NormalizedPath='{classification?.NormalizedPath}', Elapsed='{stopwatch.Elapsed}'",
LogLevels.Debug);
var allowShareKinds = allowSharePathEnsure; var allowShareKinds = allowSharePathEnsure;
if (!IsSupportedPermissionManagedPathKind( if (!IsSupportedPermissionManagedPathKind(
classification, classification,
@@ -1165,6 +1175,7 @@ namespace C4IT.LIAM
string matchingConfigurationKey; string matchingConfigurationKey;
string matchingRule; string matchingRule;
LogEntry($"NTFS permission ensure provider stage. Stage=Blacklist Path='{folderPath}'", LogLevels.Debug);
if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule)) if (IsPathBlacklisted(classification, out matchingConfigurationKey, out matchingRule))
{ {
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
@@ -1174,6 +1185,7 @@ namespace C4IT.LIAM
}); });
} }
LogEntry($"NTFS permission ensure provider stage. Stage=Whitelist Path='{folderPath}'", LogLevels.Debug);
if (!IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule)) if (!IsPathWhitelisted(classification, false, out matchingConfigurationKey, out matchingRule))
{ {
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
@@ -1184,6 +1196,7 @@ namespace C4IT.LIAM
} }
string levelSkipReason; string levelSkipReason;
LogEntry($"NTFS permission ensure provider stage. Stage=LevelRange Path='{folderPath}'", LogLevels.Debug);
if (!IsPermissionLevelManagedPath(classification, out levelSkipReason)) if (!IsPermissionLevelManagedPath(classification, out levelSkipReason))
{ {
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()) return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
@@ -1193,7 +1206,13 @@ namespace C4IT.LIAM
}); });
} }
LogEntry($"NTFS permission ensure provider stage. Stage=ParentPath Path='{folderPath}'", LogLevels.Debug);
var parentPath = Directory.GetParent(folderPath)?.FullName; var parentPath = Directory.GetParent(folderPath)?.FullName;
LogEntry(
$"NTFS permission ensure provider stage finished. Stage=ParentPath Path='{folderPath}', ParentPath='{parentPath}', Elapsed='{stopwatch.Elapsed}'",
LogLevels.Debug);
LogEntry($"NTFS permission ensure provider stage. Stage=CreateFilesystemEngine Path='{folderPath}'", LogLevels.Debug);
var engine = CreateFilesystemEngine( var engine = CreateFilesystemEngine(
folderPath, folderPath,
parentPath, parentPath,
@@ -1202,6 +1221,9 @@ namespace C4IT.LIAM
readerSids, readerSids,
writerSids); writerSids);
engine.WhatIf = whatIf; engine.WhatIf = whatIf;
LogEntry(
$"NTFS permission ensure provider stage finished. Stage=CreateFilesystemEngine Path='{folderPath}', Elapsed='{stopwatch.Elapsed}'",
LogLevels.Debug);
var allowTraverseGroups = ensureTraverseGroups var allowTraverseGroups = ensureTraverseGroups
&& IsSupportedPermissionManagedPathKind( && IsSupportedPermissionManagedPathKind(
@@ -1209,7 +1231,13 @@ namespace C4IT.LIAM
eNtfsPathKind.Folder, eNtfsPathKind.Folder,
eNtfsPathKind.ClassicShare, eNtfsPathKind.ClassicShare,
eNtfsPathKind.DfsLink); eNtfsPathKind.DfsLink);
LogEntry(
$"NTFS permission ensure provider stage. Stage=EngineEnsureDataAreaPermissions Path='{folderPath}', AllowTraverseGroups={allowTraverseGroups}",
LogLevels.Debug);
var resultToken = engine.ensureDataAreaPermissions(allowTraverseGroups); var resultToken = engine.ensureDataAreaPermissions(allowTraverseGroups);
LogEntry(
$"NTFS permission ensure provider finished. Stage=EngineEnsureDataAreaPermissions Path='{folderPath}', ResultErrorId={resultToken?.resultErrorId}, ResultMessage='{resultToken?.resultMessage ?? string.Empty}', Elapsed='{stopwatch.Elapsed}'",
LogLevels.Debug);
if (!allowTraverseGroups && ensureTraverseGroups) if (!allowTraverseGroups && ensureTraverseGroups)
resultToken.warnings.Add($"Traverse groups are currently only ensured for folder and share paths. Traverse processing was skipped for '{folderPath}'."); resultToken.warnings.Add($"Traverse groups are currently only ensured for folder and share paths. Traverse processing was skipped for '{folderPath}'.");

View File

@@ -352,43 +352,90 @@ namespace C4IT_IAM_SET
public ResultToken ensureDataAreaPermissions(bool ensureTraverseGroups = false) public ResultToken ensureDataAreaPermissions(bool ensureTraverseGroups = false)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
var stopwatch = Stopwatch.StartNew();
try try
{ {
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions start. Stage=Start Path='{newFolderPath}', BaseFolder='{baseFolder}', EnsureTraverseGroups={ensureTraverseGroups}, WhatIf={WhatIf}");
var resultToken = checkRequiredVariablesForEnsure(); var resultToken = checkRequiredVariablesForEnsure();
if (resultToken.resultErrorId != 0) if (resultToken.resultErrorId != 0)
return resultToken; return resultToken;
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=NetworkConnection Path='{newFolderPath}', BaseFolder='{baseFolder}'");
if (Connection != null) if (Connection != null)
Connection.Dispose(); Connection.Dispose();
using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password)) using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password))
{ {
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=NetworkConnection Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=DirectoryExists Path='{newFolderPath}'");
if (!Directory.Exists(newFolderPath)) if (!Directory.Exists(newFolderPath))
{ {
resultToken.resultErrorId = 30203; resultToken.resultErrorId = 30203;
resultToken.resultMessage = "Verzeichnis existiert nicht"; resultToken.resultMessage = "Verzeichnis existiert nicht";
return resultToken; return resultToken;
} }
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=DirectoryExists Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=InitializeFolderContext Path='{newFolderPath}'");
var parentDirectory = Directory.GetParent(newFolderPath); var parentDirectory = Directory.GetParent(newFolderPath);
if (string.IsNullOrWhiteSpace(newFolderParent)) if (string.IsNullOrWhiteSpace(newFolderParent))
newFolderParent = parentDirectory?.FullName; newFolderParent = parentDirectory?.FullName;
InitializeFolderContext(); InitializeFolderContext();
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=InitializeFolderContext Path='{newFolderPath}', ParentPath='{newFolderParent}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=ValidateTraverseBoundary Path='{newFolderPath}'");
var traverseBoundaryResult = ValidateTraverseBoundaryForCurrentFolder(); var traverseBoundaryResult = ValidateTraverseBoundaryForCurrentFolder();
if (traverseBoundaryResult.resultErrorId != 0) if (traverseBoundaryResult.resultErrorId != 0)
return traverseBoundaryResult; return traverseBoundaryResult;
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=ValidateTraverseBoundary Path='{newFolderPath}', Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=EnsureADGroups Path='{newFolderPath}'");
ensureADGroups(resultToken); ensureADGroups(resultToken);
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=EnsureADGroups Path='{newFolderPath}', CreatedGroups={resultToken.createdGroups.Count}, ReusedGroups={resultToken.reusedGroups.Count}, Warnings={resultToken.warnings.Count}, Elapsed='{stopwatch.Elapsed}'");
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=EnsureFolderPermissions Path='{newFolderPath}'");
resultToken = ensureFolderPermissions(resultToken); resultToken = ensureFolderPermissions(resultToken);
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=EnsureFolderPermissions Path='{newFolderPath}', ResultErrorId={resultToken.resultErrorId}, AddedAcls={resultToken.addedAclEntries.Count}, SkippedAcls={resultToken.skippedAclEntries.Count}, Elapsed='{stopwatch.Elapsed}'");
if (resultToken.resultErrorId != 0) if (resultToken.resultErrorId != 0)
return resultToken; return resultToken;
if (ensureTraverseGroups) if (ensureTraverseGroups)
{ {
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage. Stage=SetTraversePermissions Path='{newFolderPath}'");
var traverseResult = SetTraversePermissions(); var traverseResult = SetTraversePermissions();
if (traverseResult != null) if (traverseResult != null)
{ {
@@ -405,11 +452,17 @@ namespace C4IT_IAM_SET
return resultToken; return resultToken;
} }
} }
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions stage finished. Stage=SetTraversePermissions Path='{newFolderPath}', TraverseGroups={resultToken.ensuredTraverseGroups.Count}, AddedAcls={resultToken.addedAclEntries.Count}, Elapsed='{stopwatch.Elapsed}'");
} }
resultToken.resultMessage = WhatIf resultToken.resultMessage = WhatIf
? "Gruppen- und ACL-Vorschau erfolgreich erstellt" ? "Gruppen- und ACL-Vorschau erfolgreich erstellt"
: "Gruppen und ACLs erfolgreich sichergestellt"; : "Gruppen und ACLs erfolgreich sichergestellt";
DefaultLogger.LogEntry(
LogLevels.Debug,
$"NTFS engine ensure permissions finished. Stage=Finished Path='{newFolderPath}', ResultErrorId={resultToken.resultErrorId}, ResultMessage='{resultToken.resultMessage}', Elapsed='{stopwatch.Elapsed}'");
return resultToken; return resultToken;
} }
} }

View File

@@ -5,6 +5,7 @@ using C4IT_IAM_Engine;
using LiamAD; using LiamAD;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -77,6 +78,9 @@ namespace LiamWorkflowActivities
public static class LiamWorkflowRuntime public static class LiamWorkflowRuntime
{ {
private const string AutomaticNtfsEnsureTimeoutSecondsKey = "NtfsAutomaticEnsureTimeoutSeconds";
private const int DefaultAutomaticNtfsEnsureTimeoutSeconds = 300;
public static async Task<GetDataAreasOperationResult> GetDataAreasFromProviderAsync(cLiamProviderBase provider, string configurationId = null, bool? simulateConfiguredNtfsPermissionEnsure = null) public static async Task<GetDataAreasOperationResult> GetDataAreasFromProviderAsync(cLiamProviderBase provider, string configurationId = null, bool? simulateConfiguredNtfsPermissionEnsure = null)
{ {
var result = new GetDataAreasOperationResult(); var result = new GetDataAreasOperationResult();
@@ -422,11 +426,19 @@ namespace LiamWorkflowActivities
var allowFolderEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups"); var allowFolderEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups");
var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroupsForShares"); var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroupsForShares");
var allowTraverseEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsTraverseGroups"); var allowTraverseEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsTraverseGroups");
var ensureTimeoutSeconds = GetAdditionalConfigurationInt32(
provider,
AutomaticNtfsEnsureTimeoutSecondsKey,
DefaultAutomaticNtfsEnsureTimeoutSeconds,
0);
var ensureTimeout = ensureTimeoutSeconds > 0
? TimeSpan.FromSeconds(ensureTimeoutSeconds)
: TimeSpan.Zero;
if (!allowFolderEnsure && !allowSharePathEnsure && !allowTraverseEnsure) if (!allowFolderEnsure && !allowSharePathEnsure && !allowTraverseEnsure)
return true; return true;
LogEntry( LogEntry(
$"Automatic NTFS ensure configured. PermissionGroups={allowFolderEnsure}, PermissionGroupsForShares={allowSharePathEnsure}, TraverseGroups={allowTraverseEnsure}, DataAreas={dataAreas.Count}, WhatIf={simulateOnly}", $"Automatic NTFS ensure configured. PermissionGroups={allowFolderEnsure}, PermissionGroupsForShares={allowSharePathEnsure}, TraverseGroups={allowTraverseEnsure}, DataAreas={dataAreas.Count}, WhatIf={simulateOnly}, TimeoutSeconds={ensureTimeoutSeconds}",
LogLevels.Debug); LogLevels.Debug);
if (allowFolderEnsure || allowSharePathEnsure) if (allowFolderEnsure || allowSharePathEnsure)
@@ -449,15 +461,19 @@ namespace LiamWorkflowActivities
ResultToken ensureResult; ResultToken ensureResult;
try try
{ {
ensureResult = await ntfsProvider.EnsureMissingPermissionGroupsAsync( ensureResult = await RunAutomaticNtfsEnsureOperationAsync(
folderPath, folderPath,
null, "permission group ensure",
null, () => ntfsProvider.EnsureMissingPermissionGroupsAsync(
null, folderPath,
null, null,
allowSharePathEnsure, null,
ensureTraverseInPermissionPhase, null,
simulateOnly); null,
allowSharePathEnsure,
ensureTraverseInPermissionPhase,
simulateOnly),
ensureTimeout);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -477,6 +493,12 @@ namespace LiamWorkflowActivities
if (ensureResult.resultErrorId != 0) if (ensureResult.resultErrorId != 0)
{ {
if (IsAutomaticNtfsEnsureTimeout(ensureResult))
{
SetAutomaticEnsureTimeoutError(result, folderPath, ensureResult, "permission group ensure");
return false;
}
LogEntry( LogEntry(
$"Automatic NTFS permission group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'", $"Automatic NTFS permission group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Warning); LogLevels.Warning);
@@ -514,10 +536,14 @@ namespace LiamWorkflowActivities
ResultToken ensureResult; ResultToken ensureResult;
try try
{ {
ensureResult = await ntfsProvider.EnsureTraverseGroupsAsync( ensureResult = await RunAutomaticNtfsEnsureOperationAsync(
folderPath, folderPath,
null, "traverse group ensure",
simulateOnly); () => ntfsProvider.EnsureTraverseGroupsAsync(
folderPath,
null,
simulateOnly),
ensureTimeout);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -537,6 +563,12 @@ namespace LiamWorkflowActivities
if (ensureResult.resultErrorId != 0) if (ensureResult.resultErrorId != 0)
{ {
if (IsAutomaticNtfsEnsureTimeout(ensureResult))
{
SetAutomaticEnsureTimeoutError(result, folderPath, ensureResult, "traverse group ensure");
return false;
}
LogEntry( LogEntry(
$"Automatic NTFS traverse group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'", $"Automatic NTFS traverse group ensure skipped for '{folderPath}' after resultErrorId={ensureResult.resultErrorId}. ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Warning); LogLevels.Warning);
@@ -557,6 +589,55 @@ namespace LiamWorkflowActivities
return true; return true;
} }
private static async Task<ResultToken> RunAutomaticNtfsEnsureOperationAsync(
string folderPath,
string operationLabel,
Func<Task<ResultToken>> operation,
TimeSpan timeout)
{
var operationTask = Task.Run(operation);
#pragma warning disable 4014
// Fire-and-forget observer for exceptions that happen after a timeout.
operationTask.ContinueWith(
task => LogException(task.Exception),
TaskContinuationOptions.OnlyOnFaulted);
#pragma warning restore 4014
if (timeout <= TimeSpan.Zero)
return await operationTask;
var completedTask = await Task.WhenAny(operationTask, Task.Delay(timeout));
if (completedTask == operationTask)
return await operationTask;
var message =
$"Automatic NTFS {operationLabel} timed out for '{folderPath}' after {timeout.TotalSeconds:F0}s. " +
"The workflow will stop automatic NTFS ensure processing for this run. " +
"The underlying NTFS/AD operation may still finish in the worker process.";
LogEntry(message, LogLevels.Error);
return new ResultToken("AutomaticNtfsEnsureTimeout")
{
resultErrorId = 30010,
resultMessage = message
};
}
private static bool IsAutomaticNtfsEnsureTimeout(ResultToken ensureResult)
{
return ensureResult?.resultErrorId == 30010;
}
private static void SetAutomaticEnsureTimeoutError(
GetDataAreasOperationResult result,
string folderPath,
ResultToken ensureResult,
string operationLabel)
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_TIMEOUT";
result.ErrorMessage = ensureResult?.resultMessage
?? $"Automatic NTFS {operationLabel} timed out for '{folderPath}'.";
}
private static NtfsAutomaticEnsurePreviewEntry MapAutomaticEnsurePreview(string folderPath, ResultToken ensureResult) private static NtfsAutomaticEnsurePreviewEntry MapAutomaticEnsurePreview(string folderPath, ResultToken ensureResult)
{ {
return new NtfsAutomaticEnsurePreviewEntry return new NtfsAutomaticEnsurePreviewEntry
@@ -662,6 +743,33 @@ namespace LiamWorkflowActivities
|| rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase); || rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase);
} }
private static int GetAdditionalConfigurationInt32(cLiamProviderBase provider, string key, int defaultValue, int minValue)
{
if (provider?.AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key))
return defaultValue;
if (!provider.AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
return defaultValue;
if (!int.TryParse(rawValue.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue))
{
LogEntry(
$"AdditionalConfiguration '{key}' has invalid integer value '{rawValue}'. Defaulting to {defaultValue}.",
LogLevels.Warning);
return defaultValue;
}
if (parsedValue < minValue)
{
LogEntry(
$"AdditionalConfiguration '{key}' value {parsedValue} is below minimum {minValue}. Defaulting to {defaultValue}.",
LogLevels.Warning);
return defaultValue;
}
return parsedValue;
}
private static bool IsWorkflowWhatIfEnabled(cLiamProviderBase provider) private static bool IsWorkflowWhatIfEnabled(cLiamProviderBase provider)
{ {
return IsAdditionalConfigurationEnabled(provider, "WhatIf"); return IsAdditionalConfigurationEnabled(provider, "WhatIf");