Compare commits

..

41 Commits

Author SHA1 Message Date
Meik
d07728b455 Update diagnostics app icon 2026-03-19 13:20:42 +01:00
Meik
ee396e5259 Fix diagnostics window icon URI 2026-03-19 13:19:20 +01:00
Meik
1884469419 Set diagnostics app icon 2026-03-19 13:17:23 +01:00
Meik
06381a4fa4 Fix relative path shortening loop 2026-03-18 16:57:41 +01:00
Meik
6ce1e70426 Refine NTFS group name shortening 2026-03-18 16:46:54 +01:00
Meik
01fc0ba877 Remove BOM from NTFS helper 2026-03-18 16:36:08 +01:00
Meik
a3c741a50a Update AD group name length finding status 2026-03-18 16:34:06 +01:00
Meik
ca15d635d4 Bound NTFS AD group name lengths 2026-03-18 16:32:40 +01:00
Meik
0e95ddf53a Detail AD group name shortening approach 2026-03-18 16:21:52 +01:00
Meik
dd02459851 Document AD group name length finding 2026-03-18 16:11:43 +01:00
Meik
9d9575c9ef Skip NTFS ensure and traverse on share roots 2026-03-18 15:54:42 +01:00
Meik
eb6f23321d Document traverse blocker mitigation 2026-03-18 15:44:28 +01:00
Meik
e087eb4197 Retry traverse membership changes directly 2026-03-18 15:42:54 +01:00
Meik
b636f454cf Replace traverse sleep with bounded retry 2026-03-18 15:32:03 +01:00
Meik
a81c12e9b7 Honor diagnostics WhatIf override for data area fetch 2026-03-18 15:08:54 +01:00
Meik
518087289e Run diagnostics actions off UI thread 2026-03-18 15:03:49 +01:00
Meik
66ce92eadd Fix diagnostics project build targets 2026-03-18 14:46:04 +01:00
Meik
cd3819c9bd Fix diagnostics WhatIf toolbar layout 2026-03-18 14:38:03 +01:00
Meik
61dd57cf0c Add workflow NTFS WhatIf toggle 2026-03-18 14:35:14 +01:00
Meik
3ec73817e8 Preview NTFS auto ensure in diagnostics 2026-03-18 14:17:40 +01:00
Meik
24e10feffc Add diagnostics WhatIf mode 2026-03-18 14:05:51 +01:00
Meik
2bda1010d1 Harden NTFS SMB login retry 2026-03-18 13:48:24 +01:00
Meik
8573698e33 Log NTFS ensure details during data area fetch 2026-03-17 15:08:58 +01:00
Meik
837dd0b9ee Add NTFS stable folder ID demo script 2026-03-17 14:46:54 +01:00
Meik
663373092e Add NTFS share provisioning concept 2026-03-16 15:50:57 +01:00
Meik
c330627f0f Enable WSL solution builds 2026-03-16 14:33:28 +01:00
Meik
7f3415c690 Log and continue on NTFS subtree access errors 2026-03-16 14:23:53 +01:00
Meik
f2d1cbb3d8 Update initial findings with head comparison 2026-03-16 14:04:19 +01:00
Meik
537754f6bc Add initial WF GetDataAreas findings 2026-03-16 13:56:55 +01:00
Meik
ece7fd8e7c Document WF GetDataAreas NTFS findings 2026-03-16 13:47:59 +01:00
Meik
42f57ed7ba Fix NTFS ensure group reuse and parent mapping 2026-03-13 23:37:47 +01:00
Meik
865fa577e3 Add NTFS bulk AD group processing concept 2026-03-13 23:25:59 +01:00
Meik
9cfd266294 Support NTFS server roots 2026-03-13 20:20:16 +01:00
Meik
f14d4ec2e6 Classify NTFS paths via DFS metadata 2026-03-13 20:06:47 +01:00
Meik
e7fc76bf5a Add DFS metadata classification concept 2026-03-13 17:14:57 +01:00
Meik
4850fdb80d Fix orphaned ACL cleanup summary count 2026-03-13 16:51:23 +01:00
Meik
ba7d0fb600 Add orphaned folder ACL cleanup script 2026-03-13 16:45:55 +01:00
Meik
b5981487d7 Prefer folder ACL groups during NTFS ensure 2026-03-13 16:37:36 +01:00
Meik
c12978ff5d Fix runtime Guid parsing for older compiler 2026-03-13 15:19:29 +01:00
Meik
4909c93bef Share workflow runtime with diagnostics tool 2026-03-13 15:14:03 +01:00
Meik
55ff17c4b4 Make strict AD group names optional 2026-03-13 14:45:06 +01:00
27 changed files with 4828 additions and 834 deletions

View File

@@ -8,6 +8,8 @@ The solution `LIAM.sln` covers all Matrix42 integration projects. Runtime code c
Run `nuget restore LIAM.sln` once per clone to hydrate packages. Build locally with `msbuild LIAM.sln /p:Configuration=Debug`; use `Release` for deployable artifacts. For a clean rebuild, execute `msbuild LIAM.sln /t:Clean,Build /p:Configuration=Debug`. Visual Studio can open `LIAM.sln`, with `LiamM42WebApi` as the suggested startup project. When self-hosting the API, deploy it to IIS or IIS Express pointing at the project folder. Run `nuget restore LIAM.sln` once per clone to hydrate packages. Build locally with `msbuild LIAM.sln /p:Configuration=Debug`; use `Release` for deployable artifacts. For a clean rebuild, execute `msbuild LIAM.sln /t:Clean,Build /p:Configuration=Debug`. Visual Studio can open `LIAM.sln`, with `LiamM42WebApi` as the suggested startup project. When self-hosting the API, deploy it to IIS or IIS Express pointing at the project folder.
On WSL/Linux, use the user-local `msbuild` wrapper that delegates to `dotnet msbuild` with the local .NET Framework reference assemblies under `~/.local/share/dotnet-framework-reference-assemblies/`. The expected verification command there is still `msbuild LIAM.sln /p:Configuration=Debug`. `LiamWorkflowDiagnostics` is a WPF diagnostics tool and is intentionally skipped on non-Windows builds; validate that project on Windows with Visual Studio or Windows MSBuild. If you only need to validate the NTFS provider in isolation, `msbuild LiamNtfs/LiamNtfs.csproj /p:Configuration=Debug` is the fastest targeted check.
## Coding Style & Naming Conventions ## Coding Style & Naming Conventions
Follow C# Allman braces with four-space indentation. Maintain `PascalCase` for classes, members, and constants (e.g., `constFragmentNameConfigProviderBase`), and `camelCase` for locals and parameters. Keep `using` directives sorted and trimmed. New projects should link `SharedAssemblyInfo.cs` to align assembly metadata. Format via Visual Studio or `dotnet format` if the SDK is available. Follow C# Allman braces with four-space indentation. Maintain `PascalCase` for classes, members, and constants (e.g., `constFragmentNameConfigProviderBase`), and `camelCase` for locals and parameters. Keep `using` directives sorted and trimmed. New projects should link `SharedAssemblyInfo.cs` to align assembly metadata. Format via Visual Studio or `dotnet format` if the SDK is available.

View File

@@ -30,6 +30,7 @@ namespace C4IT.LIAM
public enum eLiamDataAreaTypes public enum eLiamDataAreaTypes
{ {
Unknown = 0, Unknown = 0,
NtfsServerRoot = 100,
NtfsShare = 101, NtfsShare = 101,
NtfsFolder = 102, NtfsFolder = 102,
DfsNamespaceRoot = 103, DfsNamespaceRoot = 103,

View File

@@ -132,6 +132,6 @@
<!--<Exec Command="xcopy.exe /S ..\LiamMsTeams\bin\Debug\LiamMsTeams.dll bin\Debug" ContinueOnError="false" WorkingDirectory="." />--> <!--<Exec Command="xcopy.exe /S ..\LiamMsTeams\bin\Debug\LiamMsTeams.dll bin\Debug" ContinueOnError="false" WorkingDirectory="." />-->
</Target> </Target>
<PropertyGroup> <PropertyGroup>
<PostBuildEvent>IF $(ConfigurationName) == Debug_and_copy start XCOPY /Y /R "$(ProjectDir)$(OutDir)Liam*.dll" "\\srvwsm001.imagoverum.com\c$\Program Files (x86)\Matrix42\Matrix42 Workplace Management\svc\bin"</PostBuildEvent> <PostBuildEvent Condition=" '$(OS)' == 'Windows_NT' and '$(Configuration)' == 'Debug_and_copy' ">XCOPY /Y /R "$(ProjectDir)$(OutDir)Liam*.dll" "\\srvwsm001.imagoverum.com\c$\Program Files (x86)\Matrix42\Matrix42 Workplace Management\svc\bin"</PostBuildEvent>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -47,6 +47,7 @@ namespace C4IT.LIAM
public eNtfsPathKind Kind { get; set; } = eNtfsPathKind.Unknown; public eNtfsPathKind Kind { get; set; } = eNtfsPathKind.Unknown;
public string BoundaryPath { get; set; } = string.Empty; public string BoundaryPath { get; set; } = string.Empty;
public string ParentBoundaryPath { get; set; } = string.Empty; public string ParentBoundaryPath { get; set; } = string.Empty;
public string ParentPath { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty;
public int Level { get; set; } = -1; public int Level { get; set; } = -1;
} }
@@ -55,6 +56,7 @@ namespace C4IT.LIAM
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);
private readonly Dictionary<string, string> dfsEntryPathCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
//public readonly bool WithoutPrivateFolders = true; //public readonly bool WithoutPrivateFolders = true;
@@ -177,20 +179,7 @@ namespace C4IT.LIAM
if (Depth == 0) if (Depth == 0)
return DataAreas; return DataAreas;
var DAL = await ntfsBase.RequestFoldersListAsync(this.RootPath, Depth); DataAreas.AddRange(await GetChildDataAreasAsync(rootClassification, Depth));
if (DAL == null)
return null;
foreach (var Entry in DAL)
{
if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Value.DisplayName, this.DataAreaRegEx).Success)
continue;
var classification = ClassifyPath(Entry.Value.Path);
var dataArea = await BuildDataAreaAsync(classification, Entry.Value as cNtfsResultFolder);
if (dataArea != null)
DataAreas.Add(dataArea);
}
return DataAreas; return DataAreas;
} }
catch (Exception E) catch (Exception E)
@@ -240,6 +229,10 @@ namespace C4IT.LIAM
switch (classification.Kind) switch (classification.Kind)
{ {
case eNtfsPathKind.ServerRoot:
{
return new cLiamNtfsServerRoot(this, classification.NormalizedPath, classification.Level);
}
case eNtfsPathKind.ClassicShare: case eNtfsPathKind.ClassicShare:
case eNtfsPathKind.DfsLink: case eNtfsPathKind.DfsLink:
{ {
@@ -274,7 +267,13 @@ namespace C4IT.LIAM
? new DirectoryInfo(classification.NormalizedPath).CreationTimeUtc.ToString("s") ? new DirectoryInfo(classification.NormalizedPath).CreationTimeUtc.ToString("s")
: DateTime.MinValue.ToString("s") : DateTime.MinValue.ToString("s")
}; };
var folder = new cLiamNtfsFolder(this, null, null, folderData, classification.ParentBoundaryPath); folderData.Level = classification.Level;
if (folderData.Parent == null && !string.IsNullOrWhiteSpace(classification.ParentPath))
folderData.Parent = new cNtfsResultFolder() { Path = classification.ParentPath };
var parentPath = !string.IsNullOrWhiteSpace(classification.ParentPath)
? classification.ParentPath
: classification.ParentBoundaryPath;
var folder = new cLiamNtfsFolder(this, null, null, folderData, parentPath);
await folder.ResolvePermissionGroupsAsync(folder.TechnicalName); await folder.ResolvePermissionGroupsAsync(folder.TechnicalName);
return folder; return folder;
} }
@@ -303,59 +302,196 @@ namespace C4IT.LIAM
if (segments.Length < 2) if (segments.Length < 2)
return classification; return classification;
var serverName = segments[0]; classification.ParentPath = segments.Length > 2
var publishedShares = GetPublishedShareNames(serverName); ? BuildUncPath(segments, segments.Length - 1)
var firstBoundaryPath = BuildUncPath(segments, 2); : string.Empty;
if (segments.Length == 2) var dfsPrefixes = GetDfsObjectPrefixes(normalizedPath);
if (dfsPrefixes.Count > 0)
{ {
if (publishedShares.Contains(segments[1])) var namespaceRootPath = dfsPrefixes[0];
{ var deepestDfsPath = dfsPrefixes[dfsPrefixes.Count - 1];
classification.Kind = eNtfsPathKind.ClassicShare;
classification.BoundaryPath = normalizedPath;
return classification;
}
if (Directory.Exists(normalizedPath)) if (PathsEqual(normalizedPath, namespaceRootPath))
{ {
classification.Kind = eNtfsPathKind.DfsNamespaceRoot; classification.Kind = eNtfsPathKind.DfsNamespaceRoot;
classification.BoundaryPath = normalizedPath; classification.BoundaryPath = normalizedPath;
return classification;
} }
return classification; if (PathsEqual(normalizedPath, deepestDfsPath))
}
if (publishedShares.Contains(segments[1]))
{
classification.Kind = eNtfsPathKind.Folder;
classification.BoundaryPath = firstBoundaryPath;
classification.ParentBoundaryPath = segments.Length == 3
? firstBoundaryPath
: BuildUncPath(segments, segments.Length - 1);
return classification;
}
if (Directory.Exists(firstBoundaryPath))
{
if (segments.Length == 3)
{ {
classification.Kind = eNtfsPathKind.DfsLink; classification.Kind = eNtfsPathKind.DfsLink;
classification.BoundaryPath = normalizedPath; classification.BoundaryPath = deepestDfsPath;
classification.ParentBoundaryPath = firstBoundaryPath; classification.ParentBoundaryPath = dfsPrefixes.Count > 1
? dfsPrefixes[dfsPrefixes.Count - 2]
: namespaceRootPath;
return classification; return classification;
} }
classification.Kind = eNtfsPathKind.Folder; classification.Kind = eNtfsPathKind.Folder;
classification.BoundaryPath = BuildUncPath(segments, 3); classification.BoundaryPath = deepestDfsPath;
classification.ParentBoundaryPath = segments.Length == 4 classification.ParentBoundaryPath = classification.ParentPath;
? BuildUncPath(segments, 3)
: BuildUncPath(segments, segments.Length - 1);
return classification; return classification;
} }
var shareBoundaryPath = GetPublishedShareBoundaryPath(segments);
if (!string.IsNullOrWhiteSpace(shareBoundaryPath))
{
if (PathsEqual(normalizedPath, shareBoundaryPath))
{
classification.Kind = eNtfsPathKind.ClassicShare;
classification.BoundaryPath = shareBoundaryPath;
return classification;
}
classification.Kind = eNtfsPathKind.Folder;
classification.BoundaryPath = shareBoundaryPath;
classification.ParentBoundaryPath = classification.ParentPath;
return classification;
}
if (Directory.Exists(normalizedPath))
{
classification.Kind = eNtfsPathKind.Folder;
classification.ParentBoundaryPath = classification.ParentPath;
}
return classification; return classification;
} }
private async Task<List<cLiamDataAreaBase>> GetChildDataAreasAsync(cNtfsPathClassification parentClassification, int depth)
{
var children = new List<cLiamDataAreaBase>();
if (parentClassification == null || depth == 0)
return children;
if (parentClassification.Kind == eNtfsPathKind.ServerRoot)
{
foreach (var childPath in GetServerRootChildPaths(parentClassification.NormalizedPath))
{
var childClassification = ClassifyPath(childPath);
if (!ShouldIncludeDataArea(childClassification.DisplayName))
continue;
var childDataArea = await BuildDataAreaAsync(childClassification);
if (childDataArea == null)
continue;
children.Add(childDataArea);
if (depth > 1)
children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1));
}
return children;
}
var folderEntries = await ntfsBase.RequestFoldersListAsync(parentClassification.NormalizedPath, 1);
if (folderEntries == null)
return children;
foreach (var entry in folderEntries.Values.OfType<cNtfsResultFolder>())
{
var childClassification = ClassifyPath(entry.Path);
if (!ShouldIncludeDataArea(childClassification.DisplayName))
continue;
var childDataArea = await BuildDataAreaAsync(childClassification, entry);
if (childDataArea == null)
continue;
children.Add(childDataArea);
if (depth > 1)
children.AddRange(await GetChildDataAreasAsync(childClassification, depth - 1));
}
return children;
}
private IEnumerable<string> GetServerRootChildPaths(string serverRootPath)
{
var segments = GetUncSegments(serverRootPath);
if (segments.Length != 1)
return Enumerable.Empty<string>();
var serverName = segments[0];
return GetPublishedShareNames(serverName)
.OrderBy(i => i, StringComparer.OrdinalIgnoreCase)
.Select(shareName => BuildUncPath(new[] { serverName, shareName }, 2));
}
private bool ShouldIncludeDataArea(string displayName)
{
if (string.IsNullOrEmpty(this.DataAreaRegEx))
return true;
return Regex.Match(displayName ?? string.Empty, this.DataAreaRegEx).Success;
}
private List<string> GetDfsObjectPrefixes(string path)
{
var normalizedPath = NormalizeUncPath(path);
var segments = GetUncSegments(normalizedPath);
var prefixes = new List<string>();
for (var segmentCount = 2; segmentCount <= segments.Length; segmentCount++)
{
var prefix = BuildUncPath(segments, segmentCount);
string entryPath;
if (!TryGetDfsEntryPath(prefix, out entryPath))
continue;
prefixes.Add(!string.IsNullOrWhiteSpace(entryPath)
? NormalizeUncPath(entryPath)
: prefix);
}
return prefixes
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(i => GetUncSegments(i).Length)
.ToList();
}
private bool TryGetDfsEntryPath(string path, out string entryPath)
{
var normalizedPath = NormalizeUncPath(path);
if (dfsEntryPathCache.TryGetValue(normalizedPath, out entryPath))
return !string.IsNullOrWhiteSpace(entryPath);
string resolvedEntryPath;
if (cNetworkConnection.TryGetDfsEntryPath(normalizedPath, out resolvedEntryPath))
{
entryPath = NormalizeUncPath(string.IsNullOrWhiteSpace(resolvedEntryPath) ? normalizedPath : resolvedEntryPath);
dfsEntryPathCache[normalizedPath] = entryPath;
return true;
}
dfsEntryPathCache[normalizedPath] = string.Empty;
entryPath = string.Empty;
return false;
}
private string GetPublishedShareBoundaryPath(string[] segments)
{
if (segments == null || segments.Length < 2)
return string.Empty;
var serverName = segments[0];
var publishedShares = GetPublishedShareNames(serverName);
if (!publishedShares.Contains(segments[1]))
return string.Empty;
return BuildUncPath(segments, 2);
}
private bool PathsEqual(string left, string right)
{
return string.Equals(
NormalizeUncPath(left),
NormalizeUncPath(right),
StringComparison.OrdinalIgnoreCase);
}
private string NormalizeUncPath(string path) private string NormalizeUncPath(string path)
{ {
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
@@ -487,7 +623,8 @@ namespace C4IT.LIAM
IDictionary<string, string> customTags, IDictionary<string, string> customTags,
IEnumerable<string> ownerSids, IEnumerable<string> ownerSids,
IEnumerable<string> readerSids, IEnumerable<string> readerSids,
IEnumerable<string> writerSids IEnumerable<string> writerSids,
bool whatIf = false
) )
{ {
var engine = CreateFilesystemEngine( var engine = CreateFilesystemEngine(
@@ -497,6 +634,7 @@ namespace C4IT.LIAM
ownerSids, ownerSids,
readerSids, readerSids,
writerSids); writerSids);
engine.WhatIf = whatIf;
var result = engine.createDataArea(); var result = engine.createDataArea();
return Task.FromResult(result); return Task.FromResult(result);
} }
@@ -507,8 +645,18 @@ namespace C4IT.LIAM
IEnumerable<string> ownerSids, IEnumerable<string> ownerSids,
IEnumerable<string> readerSids, IEnumerable<string> readerSids,
IEnumerable<string> writerSids, IEnumerable<string> writerSids,
bool ensureTraverseGroups = false) bool ensureTraverseGroups = false,
bool whatIf = false)
{ {
if (!IsPermissionManagedFolderPath(folderPath))
{
return Task.FromResult(new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString())
{
resultErrorId = 30008,
resultMessage = $"NTFS permission ensure is only supported for folder paths. Shares, DFS namespaces and server roots are skipped: {folderPath}"
});
}
var parentPath = Directory.GetParent(folderPath)?.FullName; var parentPath = Directory.GetParent(folderPath)?.FullName;
var engine = CreateFilesystemEngine( var engine = CreateFilesystemEngine(
folderPath, folderPath,
@@ -517,6 +665,7 @@ namespace C4IT.LIAM
ownerSids, ownerSids,
readerSids, readerSids,
writerSids); writerSids);
engine.WhatIf = whatIf;
return Task.FromResult(engine.ensureDataAreaPermissions(ensureTraverseGroups)); return Task.FromResult(engine.ensureDataAreaPermissions(ensureTraverseGroups));
} }
@@ -564,7 +713,8 @@ namespace C4IT.LIAM
groupTraverseTag = GetRequiredCustomTag("Filesystem_GroupTraverseTag"), groupTraverseTag = GetRequiredCustomTag("Filesystem_GroupTraverseTag"),
groupDLTag = requiresDomainLocalTag ? GetRequiredCustomTag("Filesystem_GroupDomainLocalTag") : string.Empty, groupDLTag = requiresDomainLocalTag ? GetRequiredCustomTag("Filesystem_GroupDomainLocalTag") : string.Empty,
groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"), groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"),
allowExistingGroupWildcardMatch = IsAdditionalConfigurationEnabled("EnsureNtfsPermissionGroupsAllowRegexMatch") CanManagePermissionsForPath = IsPermissionManagedFolderPath,
forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames")
}; };
foreach (var template in BuildSecurityGroupTemplates()) foreach (var template in BuildSecurityGroupTemplates())
@@ -586,6 +736,12 @@ namespace C4IT.LIAM
|| rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase); || rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase);
} }
public bool IsPermissionManagedFolderPath(string path)
{
var classification = ClassifyPath(path);
return classification != null && classification.Kind == eNtfsPathKind.Folder;
}
private IEnumerable<IAM_SecurityGroupTemplate> BuildSecurityGroupTemplates() private IEnumerable<IAM_SecurityGroupTemplate> BuildSecurityGroupTemplates()
{ {
var templates = new List<IAM_SecurityGroupTemplate>(); var templates = new List<IAM_SecurityGroupTemplate>();
@@ -683,7 +839,12 @@ namespace C4IT.LIAM
} }
public static int getDepth(string root, string folder) public static int getDepth(string root, string folder)
{ {
return getDepth(new DirectoryInfo(root), new DirectoryInfo(folder)); if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(folder))
return -1;
var rootSegments = root.Trim().Replace('/', '\\').Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
var folderSegments = folder.Trim().Replace('/', '\\').Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
return folderSegments.Length - rootSegments.Length;
} }
@@ -956,6 +1117,26 @@ namespace C4IT.LIAM
return new List<cLiamDataAreaBase>(); return new List<cLiamDataAreaBase>();
} }
} }
public class cLiamNtfsServerRoot : cLiamDataAreaBase
{
public new readonly cLiamProviderNtfs Provider = null;
public cLiamNtfsServerRoot(cLiamProviderNtfs Provider, string path, int level) : base(Provider)
{
this.Provider = Provider;
this.DisplayName = path.Split('\\').Last();
this.TechnicalName = path;
this.UID = cLiamNtfsFolder.GetUniqueDataAreaID(path);
this.Level = level;
this.DataType = eLiamDataAreaTypes.NtfsServerRoot;
}
public override async Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
{
await Task.Delay(0);
return new List<cLiamDataAreaBase>();
}
}
public class cLiamAdGroup : cLiamDataAreaBase public class cLiamAdGroup : cLiamDataAreaBase
{ {
public new readonly cLiamProviderNtfs Provider = null; public new readonly cLiamProviderNtfs Provider = null;
@@ -989,7 +1170,11 @@ namespace C4IT.LIAM
this.Level = NtfsFolder.Level; this.Level = NtfsFolder.Level;
this.DataType = eLiamDataAreaTypes.NtfsFolder; this.DataType = eLiamDataAreaTypes.NtfsFolder;
this.CreatedDate = NtfsFolder.CreatedDate; this.CreatedDate = NtfsFolder.CreatedDate;
if (ntfsParent != null) if (!string.IsNullOrWhiteSpace(parentPathOverride))
{
this.ParentUID = GetUniqueDataAreaID(parentPathOverride);
}
else if (ntfsParent != null)
{ {
this.ParentUID = GetUniqueDataAreaID(ntfsParent.Path); this.ParentUID = GetUniqueDataAreaID(ntfsParent.Path);
} }
@@ -997,8 +1182,6 @@ namespace C4IT.LIAM
{ {
this.ParentUID = GetUniqueDataAreaID(this.Provider.RootPath); this.ParentUID = GetUniqueDataAreaID(this.Provider.RootPath);
} }
if (string.IsNullOrWhiteSpace(this.ParentUID) && !string.IsNullOrWhiteSpace(parentPathOverride))
this.ParentUID = GetUniqueDataAreaID(parentPathOverride);
} }
public static string GetUniqueDataAreaID(string fullPath) public static string GetUniqueDataAreaID(string fullPath)

View File

@@ -3,6 +3,7 @@ using C4IT_IAM_Engine;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.DirectoryServices; using System.DirectoryServices;
using System.DirectoryServices.AccountManagement; using System.DirectoryServices.AccountManagement;
using System.IO; using System.IO;
@@ -51,7 +52,9 @@ namespace C4IT_IAM_SET
public ICollection<string> ownerUserSids; public ICollection<string> ownerUserSids;
public ICollection<string> readerUserSids; public ICollection<string> readerUserSids;
public ICollection<string> writerUserSids; public ICollection<string> writerUserSids;
public bool allowExistingGroupWildcardMatch; public Func<string, bool> CanManagePermissionsForPath;
public bool forceStrictAdGroupNames;
public bool WhatIf;
public int ReadACLPermission = 0x200A9; public int ReadACLPermission = 0x200A9;
public int WriteACLPermission = 0x301BF; public int WriteACLPermission = 0x301BF;
@@ -133,19 +136,7 @@ namespace C4IT_IAM_SET
resultToken.resultErrorId = 0; resultToken.resultErrorId = 0;
if (checkRequiredVariables().resultErrorId == 0) if (checkRequiredVariables().resultErrorId == 0)
{ {
newDataArea = new DataArea(); InitializeFolderContext();
IAM_Folder folder = new IAM_Folder();
folder.configurationID = ConfigID;
folder.technicalName = newFolderPath;
folder.targetType = (int)IAM_TargetType.FileSystem;
folder.Parent = newFolderParent;
folder.ParentUID = DataArea.GetUniqueDataAreaID(newFolderParent);
newDataArea.IAM_Folders.Add(folder);
newSecurityGroups = new SecurityGroups();
newSecurityGroups.username = username;
newSecurityGroups.domainName = domainName;
newSecurityGroups.password = password;
newSecurityGroups.AllowExistingGroupWildcardMatch = allowExistingGroupWildcardMatch;
try try
{ {
// ImpersonationHelper.Impersonate(domainName, username, new NetworkCredential("", password).Password, delegate // ImpersonationHelper.Impersonate(domainName, username, new NetworkCredential("", password).Password, delegate
@@ -156,22 +147,26 @@ namespace C4IT_IAM_SET
DefaultLogger.LogEntry(LogLevels.Info, $"Establishing connection to {baseFolder}, User: {username}, Password: {Helper.MaskAllButLastAndFirst(new NetworkCredential("", password).Password)}"); DefaultLogger.LogEntry(LogLevels.Info, $"Establishing connection to {baseFolder}, User: {username}, Password: {Helper.MaskAllButLastAndFirst(new NetworkCredential("", password).Password)}");
using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password)) using (Connection = new cNetworkConnection(baseFolder, username, new NetworkCredential("", password).Password))
{ {
if (checkFolder().resultErrorId == 0) var folderCheckResult = checkFolder();
if (folderCheckResult.resultErrorId == 0)
{ {
try try
{ {
createADGroups(); createADGroups(resultToken);
try try
{ {
resultToken = createFolder(); resultToken = MergeResultTokens(resultToken, createFolder());
try if (resultToken.resultErrorId == 0)
{ {
resultToken = SetTraversePermissions(); try
} {
catch (Exception e) resultToken = MergeResultTokens(resultToken, SetTraversePermissions());
{ }
resultToken.resultErrorId = 30200; catch (Exception e)
resultToken.resultMessage = "Fehler beim setzen der Traverserechte \n" + e.Message; {
resultToken.resultErrorId = 30200;
resultToken.resultMessage = "Fehler beim setzen der Traverserechte \n" + e.Message;
}
} }
} }
catch (Exception e) catch (Exception e)
@@ -190,7 +185,7 @@ namespace C4IT_IAM_SET
} }
else else
{ {
resultToken = checkFolder(); resultToken = folderCheckResult;
} }
/* }, /* },
logonType, logonType,
@@ -221,6 +216,29 @@ namespace C4IT_IAM_SET
} }
} }
private ResultToken MergeResultTokens(ResultToken target, ResultToken source)
{
if (target == null)
return source;
if (source == null)
return target;
if (source.resultErrorId != 0 || target.resultErrorId == 0)
target.resultErrorId = source.resultErrorId;
if (!string.IsNullOrWhiteSpace(source.resultMessage))
target.resultMessage = source.resultMessage;
if (!string.IsNullOrWhiteSpace(source.resultFunction))
target.resultFunction = source.resultFunction;
target.createdGroups.AddRange(source.createdGroups);
target.reusedGroups.AddRange(source.reusedGroups);
target.addedAclEntries.AddRange(source.addedAclEntries);
target.skippedAclEntries.AddRange(source.skippedAclEntries);
target.ensuredTraverseGroups.AddRange(source.ensuredTraverseGroups);
target.warnings.AddRange(source.warnings);
return target;
}
private ResultToken checkRequiredVariablesForEnsure() private ResultToken checkRequiredVariablesForEnsure()
{ {
ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString()); ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString());
@@ -277,7 +295,7 @@ namespace C4IT_IAM_SET
username = username, username = username,
domainName = domainName, domainName = domainName,
password = password, password = password,
AllowExistingGroupWildcardMatch = allowExistingGroupWildcardMatch ForceStrictAdGroupNames = forceStrictAdGroupNames
}; };
} }
@@ -317,6 +335,13 @@ namespace C4IT_IAM_SET
if (ensureTraverseGroups) if (ensureTraverseGroups)
{ {
if (WhatIf)
{
resultToken.warnings.Add("Traverse group preview is not supported in WhatIf mode for automatic DataArea ensure.");
resultToken.resultMessage = "Gruppen- und ACL-Vorschau erfolgreich erstellt";
return resultToken;
}
var traverseResult = SetTraversePermissions(); var traverseResult = SetTraversePermissions();
if (traverseResult != null) if (traverseResult != null)
{ {
@@ -335,7 +360,9 @@ namespace C4IT_IAM_SET
} }
} }
resultToken.resultMessage = "Gruppen und ACLs erfolgreich sichergestellt"; resultToken.resultMessage = WhatIf
? "Gruppen- und ACL-Vorschau erfolgreich erstellt"
: "Gruppen und ACLs erfolgreich sichergestellt";
return resultToken; return resultToken;
} }
} }
@@ -455,6 +482,22 @@ namespace C4IT_IAM_SET
break; break;
} }
if (CanManagePermissionsForPath != null && !CanManagePermissionsForPath(parent.FullName))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Überspringe Traverse-Verarbeitung für nicht verwaltbaren NTFS-Pfad: {parent.FullName}");
parent = parent.Parent;
if (parent != null)
{
lvl = DataArea.GetRelativePath(parent.FullName, baseFolder).Count(n => n == Path.DirectorySeparatorChar);
DefaultLogger.LogEntry(LogLevels.Debug, $"Neue Ebene (lvl) nach Überspringen: {lvl}");
}
else
{
DefaultLogger.LogEntry(LogLevels.Debug, "Parent nach Überspringen ist null.");
}
continue;
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Hole ACL für Ordner: {parent.FullName}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Hole ACL für Ordner: {parent.FullName}");
AuthorizationRuleCollection ACLs = null; AuthorizationRuleCollection ACLs = null;
try try
@@ -487,13 +530,25 @@ namespace C4IT_IAM_SET
var folderName = sanitizedSegments.Length > 0 var folderName = sanitizedSegments.Length > 0
? sanitizedSegments[sanitizedSegments.Length - 1] ? sanitizedSegments[sanitizedSegments.Length - 1]
: Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); : Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, relativePath, sanitizedSegments, folderName); var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext(
var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, relativePath, sanitizedSegments, folderName); traverseGroupTemplate.NamingTemplate,
true,
relativePath,
sanitizedSegments,
folderName,
null,
Helper.MaxAdGroupNameLength,
$"Traverse fuer '{parent.FullName}'");
var adjustedTraverseSegments = boundedTraverseContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedTraverseRelativePath = adjustedTraverseSegments.Length > 0 ? string.Join("_", adjustedTraverseSegments) : string.Empty;
var adjustedTraverseFolderName = boundedTraverseContext.FolderName;
var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName);
var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName);
string traverseRegex = null; string traverseRegex = null;
try try
{ {
traverseRegex = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, relativePath, sanitizedSegments, folderName); traverseRegex = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.WildcardTemplate, true, adjustedTraverseRelativePath, adjustedTraverseSegments, adjustedTraverseFolderName);
DefaultLogger.LogEntry(LogLevels.Debug, $"traverseRegex: {traverseRegex}"); DefaultLogger.LogEntry(LogLevels.Debug, $"traverseRegex: {traverseRegex}");
} }
catch (Exception ex) catch (Exception ex)
@@ -591,41 +646,52 @@ namespace C4IT_IAM_SET
if (parent.Parent != null) if (parent.Parent != null)
{ {
DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist nicht null. Erstelle AD-Gruppe."); DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist nicht null. Erstelle AD-Gruppe.");
try if (WhatIf)
{ {
newSecurityGroups.CreateADGroup(groupOUPath, newTraverseGroup, null);
DefaultLogger.LogEntry(LogLevels.Debug, $"AD-Gruppe erstellt: {newTraverseGroup.Name}");
resultToken.createdGroups.Add(newTraverseGroup.Name); resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name); resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
} resultToken.warnings.Add($"Traverse-Gruppe würde angelegt werden: {newTraverseGroup.Name}");
catch (Exception ex) resultToken.addedAclEntries.Add(newTraverseGroup.Name);
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen der AD-Gruppe: {ex.Message}");
continue;
}
parentTraverseGroup = GroupPrincipal.FindByIdentity(domainContext, newTraverseGroup.Name);
if (parentTraverseGroup == null)
{
DefaultLogger.LogEntry(LogLevels.Error, $"parentTraverseGroup konnte nach Erstellung der Gruppe nicht gefunden werden: {newTraverseGroup.Name}");
continue;
}
try
{
var accesscontrol = parent.GetAccessControl();
accesscontrol.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
AccessControlType.Allow));
DefaultLogger.LogEntry(LogLevels.Debug, $"Setze Traverse-ACL auf: {parent.FullName} für {parentTraverseGroup.DistinguishedName}");
parent.SetAccessControl(accesscontrol);
resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
parentTraverseAclExists = true; parentTraverseAclExists = true;
} }
catch (Exception ex) else
{ {
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Setzen der ACL: {ex.Message}"); try
continue; {
newSecurityGroups.CreateADGroup(groupOUPath, newTraverseGroup, null);
DefaultLogger.LogEntry(LogLevels.Debug, $"AD-Gruppe erstellt: {newTraverseGroup.Name}");
resultToken.createdGroups.Add(newTraverseGroup.Name);
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Erstellen der AD-Gruppe: {ex.Message}");
continue;
}
parentTraverseGroup = GroupPrincipal.FindByIdentity(domainContext, newTraverseGroup.Name);
if (parentTraverseGroup == null)
{
DefaultLogger.LogEntry(LogLevels.Error, $"parentTraverseGroup konnte nach Erstellung der Gruppe nicht gefunden werden: {newTraverseGroup.Name}");
continue;
}
try
{
var accesscontrol = parent.GetAccessControl();
accesscontrol.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
AccessControlType.Allow));
DefaultLogger.LogEntry(LogLevels.Debug, $"Setze Traverse-ACL auf: {parent.FullName} für {parentTraverseGroup.DistinguishedName}");
parent.SetAccessControl(accesscontrol);
resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
parentTraverseAclExists = true;
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Setzen der ACL: {ex.Message}");
continue;
}
} }
} }
else else
@@ -652,10 +718,13 @@ namespace C4IT_IAM_SET
} }
else else
{ {
accessControl.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid, if (!WhatIf)
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None, {
AccessControlType.Allow)); accessControl.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
parent.SetAccessControl(accessControl); FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
AccessControlType.Allow));
parent.SetAccessControl(accessControl);
}
resultToken.addedAclEntries.Add(parentTraverseGroup.Name); resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
} }
} }
@@ -671,8 +740,6 @@ namespace C4IT_IAM_SET
if (i == lvl) if (i == lvl)
{ {
DefaultLogger.LogEntry(LogLevels.Debug, "Verarbeite SecurityGroups bei oberster Ebene."); DefaultLogger.LogEntry(LogLevels.Debug, "Verarbeite SecurityGroups bei oberster Ebene.");
DefaultLogger.LogEntry(LogLevels.Debug, "Warte 3min.");
System.Threading.Thread.Sleep(180000); // 60 Sekunden warten
foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups) foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups)
{ {
if (currentSecGroup == null) if (currentSecGroup == null)
@@ -680,32 +747,17 @@ namespace C4IT_IAM_SET
DefaultLogger.LogEntry(LogLevels.Error, "currentSecGroup ist null."); DefaultLogger.LogEntry(LogLevels.Error, "currentSecGroup ist null.");
continue; continue;
} }
using (GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, currentSecGroup.UID)) if (currentSecGroup.Scope != GroupScope.Global)
{ continue;
if (groupPrincipal == null)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"GroupPrincipal nicht gefunden für UID: {currentSecGroup.UID}");
continue;
}
if (currentSecGroup.Scope == GroupScope.Global) if (WhatIf)
{ {
try resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde Mitglied '{currentSecGroup.Name}' erhalten.");
{ continue;
if (!parentTraverseGroup.Members.Contains(groupPrincipal))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Füge {groupPrincipal.DistinguishedName} zur Traverse-Gruppe {parentTraverseGroup.DistinguishedName} hinzu");
parentTraverseGroup.Members.Add(groupPrincipal);
parentTraverseGroup.Save();
}
}
catch (Exception ex)
{
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Hinzufügen der Gruppe: {ex.Message}");
continue;
}
}
} }
if (!TryEnsureGlobalGroupMembershipWithRetry(domainContext, parentTraverseGroup, currentSecGroup))
continue;
} }
traverseGroup = parentTraverseGroup; traverseGroup = parentTraverseGroup;
} }
@@ -717,9 +769,15 @@ namespace C4IT_IAM_SET
{ {
if (!parentTraverseGroup.Members.Contains(traverseGroup)) if (!parentTraverseGroup.Members.Contains(traverseGroup))
{ {
DefaultLogger.LogEntry(LogLevels.Debug, $"Füge {traverseGroup.DistinguishedName} zur Traverse-Gruppe {parentTraverseGroup.DistinguishedName} hinzu"); if (WhatIf)
parentTraverseGroup.Members.Add(traverseGroup); {
parentTraverseGroup.Save(); resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde verschachtelte Gruppe '{traverseGroup.Name}' erhalten.");
}
else
{
if (!TryEnsureNestedTraverseGroupMembershipWithRetry(parentTraverseGroup, traverseGroup))
continue;
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -731,8 +789,11 @@ namespace C4IT_IAM_SET
} }
try try
{ {
parentTraverseGroup.Save(); if (!WhatIf)
DefaultLogger.LogEntry(LogLevels.Debug, $"parentTraverseGroup gespeichert: {parentTraverseGroup.Name}"); {
parentTraverseGroup.Save();
DefaultLogger.LogEntry(LogLevels.Debug, $"parentTraverseGroup gespeichert: {parentTraverseGroup.Name}");
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -773,7 +834,96 @@ namespace C4IT_IAM_SET
} }
} }
private bool TryEnsureGlobalGroupMembershipWithRetry(PrincipalContext domainContext, GroupPrincipal parentTraverseGroup, IAM_SecurityGroup currentSecGroup)
{
if (domainContext == null || parentTraverseGroup == null || currentSecGroup == null || string.IsNullOrWhiteSpace(currentSecGroup.UID))
return false;
return RetryTraverseMembershipAction(
currentSecGroup.Name,
currentSecGroup.CreatedNewEntry,
() =>
{
using (var groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, IdentityType.Sid, currentSecGroup.UID))
{
if (groupPrincipal == null)
return false;
if (parentTraverseGroup.Members.Contains(groupPrincipal))
return true;
DefaultLogger.LogEntry(LogLevels.Debug, $"Füge {groupPrincipal.DistinguishedName} zur Traverse-Gruppe {parentTraverseGroup.DistinguishedName} hinzu");
parentTraverseGroup.Members.Add(groupPrincipal);
parentTraverseGroup.Save();
return true;
}
});
}
private bool TryEnsureNestedTraverseGroupMembershipWithRetry(GroupPrincipal parentTraverseGroup, GroupPrincipal traverseGroup)
{
if (parentTraverseGroup == null || traverseGroup == null)
return false;
return RetryTraverseMembershipAction(
traverseGroup.Name,
true,
() =>
{
if (parentTraverseGroup.Members.Contains(traverseGroup))
return true;
DefaultLogger.LogEntry(LogLevels.Debug, $"Füge {traverseGroup.DistinguishedName} zur Traverse-Gruppe {parentTraverseGroup.DistinguishedName} hinzu");
parentTraverseGroup.Members.Add(traverseGroup);
parentTraverseGroup.Save();
return true;
});
}
private bool RetryTraverseMembershipAction(string memberName, bool allowRetry, Func<bool> tryAction)
{
if (tryAction == null)
return false;
var retryDelaysMs = allowRetry
? new[] { 0, 250, 500, 1000, 2000, 5000 }
: new[] { 0 };
var delayIndex = 0;
var maxWait = allowRetry ? TimeSpan.FromMinutes(3) : TimeSpan.Zero;
var waitStopwatch = Stopwatch.StartNew();
Exception lastException = null;
while (true)
{
var delayMs = retryDelaysMs[Math.Min(delayIndex, retryDelaysMs.Length - 1)];
if (delayMs > 0)
System.Threading.Thread.Sleep(delayMs);
if (delayIndex < retryDelaysMs.Length - 1)
delayIndex++;
try
{
if (tryAction())
{
if (waitStopwatch.ElapsedMilliseconds > 0)
DefaultLogger.LogEntry(LogLevels.Debug, $"Traverse-Mitgliedschaft für '{memberName}' nach {waitStopwatch.Elapsed.TotalSeconds:F1}s erfolgreich.");
return true;
}
}
catch (Exception ex)
{
lastException = ex;
DefaultLogger.LogEntry(LogLevels.Debug, $"Traverse-Mitgliedschaft für '{memberName}' noch nicht möglich: {ex.Message}");
}
if (!allowRetry || waitStopwatch.Elapsed >= maxWait)
break;
}
var suffix = lastException == null ? string.Empty : $" Letzte Exception: {lastException.Message}";
DefaultLogger.LogEntry(LogLevels.Warning, $"Traverse-Mitgliedschaft für '{memberName}' konnte nach {waitStopwatch.Elapsed.TotalSeconds:F1}s nicht sichergestellt werden.{suffix}");
return false;
}
private ResultToken checkFolder() private ResultToken checkFolder()
{ {
@@ -835,6 +985,12 @@ namespace C4IT_IAM_SET
var directory = new DirectoryInfo(newDataArea.IAM_Folders[0].technicalName); var directory = new DirectoryInfo(newDataArea.IAM_Folders[0].technicalName);
foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups) foreach (var currentSecGroup in newSecurityGroups.IAM_SecurityGroups)
{ {
if (WhatIf && string.IsNullOrWhiteSpace(currentSecGroup?.UID) && currentSecGroup?.CreatedNewEntry == true)
{
resultToken.addedAclEntries.Add(currentSecGroup.Name);
continue;
}
if (string.IsNullOrWhiteSpace(currentSecGroup?.UID)) if (string.IsNullOrWhiteSpace(currentSecGroup?.UID))
{ {
resultToken.warnings.Add($"Keine SID für Gruppe '{currentSecGroup?.Name}' verfügbar."); resultToken.warnings.Add($"Keine SID für Gruppe '{currentSecGroup?.Name}' verfügbar.");
@@ -854,7 +1010,9 @@ namespace C4IT_IAM_SET
continue; continue;
} }
DataArea.AddDirectorySecurity(newDataArea.IAM_Folders[0].baseFolder, newDataArea.IAM_Folders[0].technicalName, sid, currentSecGroup.rights, AccessControlType.Allow); if (!WhatIf)
DataArea.AddDirectorySecurity(newDataArea.IAM_Folders[0].baseFolder, newDataArea.IAM_Folders[0].technicalName, sid, currentSecGroup.rights, AccessControlType.Allow);
resultToken.addedAclEntries.Add(currentSecGroup.Name); resultToken.addedAclEntries.Add(currentSecGroup.Name);
} }
@@ -912,11 +1070,20 @@ namespace C4IT_IAM_SET
else else
users = null; users = null;
newSecurityGroups.EnsureADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users); if (WhatIf)
if (newSecurityGroups.IAM_SecurityGroups[i].ReusedExistingEntry) {
resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name); var existingGroup = newSecurityGroups.PreviewADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], newDataArea.IAM_Folders[0].technicalName);
newSecurityGroups.IAM_SecurityGroups[i].CreatedNewEntry = existingGroup == null;
}
else else
{
newSecurityGroups.EnsureADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users, newDataArea.IAM_Folders[0].technicalName);
}
if (newSecurityGroups.IAM_SecurityGroups[i].CreatedNewEntry)
resultToken.createdGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name); resultToken.createdGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
else
resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
} }
} }
catch (Exception E) catch (Exception E)
@@ -947,6 +1114,26 @@ namespace C4IT_IAM_SET
} }
else else
{ {
if (WhatIf)
{
newDataArea.IAM_Folders[0].UID = DataArea.GetUniqueDataAreaID(newDataArea.IAM_Folders[0].technicalName);
resultToken.warnings.Add($"Verzeichnis würde erstellt werden: {newDataArea.IAM_Folders[0].technicalName}");
for (int i = 0; newSecurityGroups.IAM_SecurityGroups.Count > i; i++)
{
var currentSecGroup = newSecurityGroups.IAM_SecurityGroups[i];
if (groupPermissionStrategy == PermissionGroupStrategy.AGDLP && currentSecGroup.Scope == GroupScope.Local
|| groupPermissionStrategy == PermissionGroupStrategy.AGP && currentSecGroup.Scope == GroupScope.Global)
{
resultToken.addedAclEntries.Add(currentSecGroup.Name);
}
}
resultToken.resultErrorId = 0;
resultToken.resultMessage = "Verzeichnis-, Gruppen- und ACL-Vorschau erfolgreich erstellt";
return resultToken;
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Creating folder: {newDataArea.IAM_Folders[0].technicalName}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Creating folder: {newDataArea.IAM_Folders[0].technicalName}");
DirectoryInfo newDir = Directory.CreateDirectory(newDataArea.IAM_Folders[0].technicalName); DirectoryInfo newDir = Directory.CreateDirectory(newDataArea.IAM_Folders[0].technicalName);
newDataArea.IAM_Folders[0].UID = DataArea.GetUniqueDataAreaID(newDir.FullName); newDataArea.IAM_Folders[0].UID = DataArea.GetUniqueDataAreaID(newDir.FullName);
@@ -999,7 +1186,7 @@ namespace C4IT_IAM_SET
LogMethodEnd(MethodBase.GetCurrentMethod()); LogMethodEnd(MethodBase.GetCurrentMethod());
} }
private void createADGroups() private void createADGroups(ResultToken resultToken)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -1058,7 +1245,24 @@ namespace C4IT_IAM_SET
users = readers; users = readers;
else else
users = null; users = null;
newSecurityGroups.CreateADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users);
if (WhatIf)
{
var existingGroup = newSecurityGroups.PreviewADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], newDataArea.IAM_Folders[0].technicalName);
newSecurityGroups.IAM_SecurityGroups[i].CreatedNewEntry = existingGroup == null;
}
else
{
newSecurityGroups.CreateADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users);
}
if (resultToken != null)
{
if (newSecurityGroups.IAM_SecurityGroups[i].CreatedNewEntry)
resultToken.createdGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
else
resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
}
} }
} }
catch (Exception E) catch (Exception E)

View File

@@ -1,4 +1,5 @@
using System; using C4IT.Logging;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -10,57 +11,151 @@ namespace C4IT_IAM_Engine
{ {
public static class Helper public static class Helper
{ {
public const int MaxAdGroupNameLength = 64;
public const int MaxAdGroupLoopDigits = 3;
private const int MinLeadingRelativePathSegmentLength = 3;
private const int MinSingleLeadingRelativePathSegmentLength = 2;
private const int MinLastRelativePathSegmentLength = 12;
public sealed class BoundedTemplateContext
{
public string[] SanitizedSegments { get; set; } = Array.Empty<string>();
public string FolderName { get; set; } = string.Empty;
public bool WasShortened { get; set; }
public string OriginalValue { get; set; } = string.Empty;
public string FinalValue { get; set; } = string.Empty;
public string Strategy { get; set; } = string.Empty;
}
public static string ReplaceLoopTag(this string str, int loop) public static string ReplaceLoopTag(this string str, int loop)
{ {
return Regex.Replace(str, @"(?<loopTag>{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}})", loop <= 0 ? "" : "${prefix}" + loop + "${postfix}"); return Regex.Replace(str, @"(?<loopTag>{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}})", loop <= 0 ? "" : "${prefix}" + loop + "${postfix}");
} }
public static string ReplaceTags(this string str, IDictionary<string, string> dict) public static string ReplaceTags(this string str, IDictionary<string, string> dict)
{ {
if (str.Equals(string.Empty) || str == null || dict == null || dict.Count == 0) if (str.Equals(string.Empty) || str == null || dict == null || dict.Count == 0)
return str; return str;
return dict.Aggregate(str, (current, value) => return dict.Aggregate(str, (current, value) =>
current.Replace("{{" + value.Key + "}}", value.Value)); current.Replace("{{" + value.Key + "}}", value.Value));
} }
public static string ApplyTemplatePlaceholders(string templateValue, bool allowRelativePath, string defaultRelativePath, string[] sanitizedSegments, string folderName) public static string ApplyTemplatePlaceholders(string templateValue, bool allowRelativePath, string defaultRelativePath, string[] sanitizedSegments, string folderName)
{ {
if (templateValue == null) if (templateValue == null)
return string.Empty; return string.Empty;
var result = Regex.Replace(templateValue, @"{{\s*NAME\s*}}", folderName ?? string.Empty, RegexOptions.IgnoreCase); var result = Regex.Replace(templateValue, @"{{\s*NAME\s*}}", folderName ?? string.Empty, RegexOptions.IgnoreCase);
if (allowRelativePath) if (allowRelativePath)
{ {
result = Regex.Replace(result, @"{{\s*RELATIVEPATH(?:\s*\(\s*(\d+)\s*\))?\s*}}", match => result = Regex.Replace(result, @"{{\s*RELATIVEPATH(?:\s*\(\s*(\d+)\s*\))?\s*}}", match =>
{ {
if (sanitizedSegments == null || sanitizedSegments.Length == 0) if (sanitizedSegments == null || sanitizedSegments.Length == 0)
return string.Empty; return string.Empty;
if (!match.Groups[1].Success) if (!match.Groups[1].Success)
return defaultRelativePath; return defaultRelativePath;
if (!int.TryParse(match.Groups[1].Value, out var segmentIndex) || segmentIndex < 0) if (!int.TryParse(match.Groups[1].Value, out var segmentIndex) || segmentIndex < 0)
return defaultRelativePath; return defaultRelativePath;
var segmentCount = Math.Min(sanitizedSegments.Length, segmentIndex + 1); var segmentCount = Math.Min(sanitizedSegments.Length, segmentIndex + 1);
var skip = sanitizedSegments.Length - segmentCount; var skip = sanitizedSegments.Length - segmentCount;
return string.Join("_", sanitizedSegments.Skip(skip)); return string.Join("_", sanitizedSegments.Skip(skip));
}, RegexOptions.IgnoreCase); }, RegexOptions.IgnoreCase);
} }
return result; return result;
} }
public static string SanitizePathSegment(string segment) public static BoundedTemplateContext GetBoundedAdGroupTemplateContext(
{ string templateValue,
if (string.IsNullOrEmpty(segment)) bool allowRelativePath,
return string.Empty; string defaultRelativePath,
string[] sanitizedSegments,
return Regex.Replace(segment, @"[\s\-]", "_"); string folderName,
} IDictionary<string, string> replacementTags,
public static void CreatePathWithWriteAccess(string FilePath) int maxLength,
{ string logContext)
try {
{ var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray();
var PF = Environment.ExpandEnvironmentVariables(FilePath); var effectiveFolderName = folderName ?? string.Empty;
var currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath);
var originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags);
var measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags);
var usesRelativePath = allowRelativePath && Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*RELATIVEPATH", RegexOptions.IgnoreCase);
var usesName = Regex.IsMatch(templateValue ?? string.Empty, @"{{\s*NAME\s*}}", RegexOptions.IgnoreCase);
var strategy = string.Empty;
while (measuredValue.Length > maxLength)
{
var changed = false;
if (usesRelativePath && TryShortenRelativePath(ref effectiveSegments))
{
effectiveFolderName = effectiveSegments.Length > 0 ? effectiveSegments[effectiveSegments.Length - 1] : string.Empty;
if (string.IsNullOrWhiteSpace(strategy))
strategy = "truncate-relativepath";
changed = true;
}
else if (usesName && !usesRelativePath && TryShortenName(ref effectiveFolderName))
{
if (string.IsNullOrWhiteSpace(strategy))
strategy = "truncate-name";
changed = true;
}
if (!changed)
break;
currentRelativePath = GetCurrentRelativePath(effectiveSegments, defaultRelativePath);
originalValue = MaterializeTemplateValue(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags);
measuredValue = MaterializeTemplateValueForLength(templateValue, allowRelativePath, currentRelativePath, effectiveSegments, effectiveFolderName, replacementTags);
}
var initialValue = MaterializeTemplateValue(
templateValue,
allowRelativePath,
GetCurrentRelativePath(sanitizedSegments, defaultRelativePath),
sanitizedSegments,
folderName,
replacementTags);
var result = new BoundedTemplateContext
{
SanitizedSegments = effectiveSegments,
FolderName = effectiveSegments.Length > 0 ? effectiveSegments[effectiveSegments.Length - 1] : effectiveFolderName,
OriginalValue = initialValue,
FinalValue = originalValue,
WasShortened = !string.Equals(initialValue, originalValue, StringComparison.Ordinal),
Strategy = strategy
};
if (result.WasShortened)
{
cLogManager.DefaultLogger.LogEntry(
LogLevels.Warning,
$"AD-Gruppenname gekuerzt ({logContext}): '{result.OriginalValue}' ({GetMeasuredTemplateLength(result.OriginalValue)}) -> '{result.FinalValue}' ({GetMeasuredTemplateLength(result.FinalValue)}), Strategie: {result.Strategy}, Limit: {maxLength}.");
}
if (measuredValue.Length > maxLength)
{
cLogManager.DefaultLogger.LogEntry(
LogLevels.Warning,
$"AD-Gruppenname ueberschreitet weiterhin das sichere Limit ({logContext}): '{result.FinalValue}' ({measuredValue.Length}), Limit: {maxLength}.");
}
return result;
}
public static string SanitizePathSegment(string segment)
{
if (string.IsNullOrEmpty(segment))
return string.Empty;
return Regex.Replace(segment, @"[\s\-]", "_");
}
public static void CreatePathWithWriteAccess(string FilePath)
{
try
{
var PF = Environment.ExpandEnvironmentVariables(FilePath);
Directory.CreateDirectory(PF); Directory.CreateDirectory(PF);
} }
catch { } catch { }
@@ -77,5 +172,146 @@ namespace C4IT_IAM_Engine
else else
return new string(maskingChar, input.Length); return new string(maskingChar, input.Length);
} }
private static string MaterializeTemplateValue(
string templateValue,
bool allowRelativePath,
string defaultRelativePath,
string[] sanitizedSegments,
string folderName,
IDictionary<string, string> replacementTags)
{
return ApplyTemplatePlaceholders(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName)
.ReplaceTags(replacementTags)
.ToUpper();
}
private static string MaterializeTemplateValueForLength(
string templateValue,
bool allowRelativePath,
string defaultRelativePath,
string[] sanitizedSegments,
string folderName,
IDictionary<string, string> replacementTags)
{
return NormalizeLoopPlaceholderLength(
MaterializeTemplateValue(templateValue, allowRelativePath, defaultRelativePath, sanitizedSegments, folderName, replacementTags));
}
private static string NormalizeLoopPlaceholderLength(string templateValue)
{
if (string.IsNullOrWhiteSpace(templateValue))
return templateValue ?? string.Empty;
return Regex.Replace(
templateValue,
@"{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}}",
match => match.Groups["prefix"].Value + new string('9', MaxAdGroupLoopDigits) + match.Groups["postfix"].Value,
RegexOptions.IgnoreCase);
}
private static int GetMeasuredTemplateLength(string templateValue)
{
return NormalizeLoopPlaceholderLength(templateValue).Length;
}
private static string GetCurrentRelativePath(string[] sanitizedSegments, string fallbackRelativePath)
{
if (sanitizedSegments != null && sanitizedSegments.Length > 0)
return string.Join("_", sanitizedSegments);
return fallbackRelativePath ?? string.Empty;
}
private static int GetLoopReservationLength(string templateValue)
{
if (string.IsNullOrWhiteSpace(templateValue))
return 0;
var reservation = 0;
foreach (Match match in Regex.Matches(templateValue, @"{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}}", RegexOptions.IgnoreCase))
{
reservation += match.Groups["prefix"].Value.Length + MaxAdGroupLoopDigits + match.Groups["postfix"].Value.Length;
}
return reservation;
}
private static bool TryShortenRelativePath(ref string[] segments)
{
if (segments == null || segments.Length == 0)
return false;
var lastIndex = segments.Length - 1;
if (segments.Length > 2)
{
var candidateIndex = -1;
var candidateLength = MinLeadingRelativePathSegmentLength;
for (var i = 0; i < lastIndex; i++)
{
if (string.IsNullOrWhiteSpace(segments[i]) || segments[i].Length <= candidateLength)
continue;
candidateIndex = i;
candidateLength = segments[i].Length;
}
if (candidateIndex >= 0)
{
segments[candidateIndex] = segments[candidateIndex].Substring(0, segments[candidateIndex].Length - 1);
return true;
}
segments = segments.Skip(1).ToArray();
return true;
}
if (segments.Length == 2)
{
if (segments[0].Length > MinSingleLeadingRelativePathSegmentLength)
{
segments[0] = segments[0].Substring(0, segments[0].Length - 1);
return true;
}
if (segments[lastIndex].Length > MinLastRelativePathSegmentLength)
{
segments[lastIndex] = segments[lastIndex].Substring(0, segments[lastIndex].Length - 1);
return true;
}
if (segments[0].Length > 1)
{
segments[0] = segments[0].Substring(0, segments[0].Length - 1);
return true;
}
if (segments[lastIndex].Length > 1)
{
segments[lastIndex] = segments[lastIndex].Substring(0, segments[lastIndex].Length - 1);
return true;
}
return false;
}
if (segments[lastIndex].Length > 1)
{
segments[lastIndex] = segments[lastIndex].Substring(0, segments[lastIndex].Length - 1);
return true;
}
return false;
}
private static bool TryShortenName(ref string folderName)
{
if (string.IsNullOrWhiteSpace(folderName) || folderName.Length <= 1)
return false;
folderName = folderName.Substring(0, folderName.Length - 1);
return true;
}
} }
} }

View File

@@ -23,7 +23,7 @@ namespace C4IT_IAM_Engine
public string domainName; public string domainName;
public string username; public string username;
public SecureString password; public SecureString password;
public bool AllowExistingGroupWildcardMatch; public bool ForceStrictAdGroupNames;
public List<IAM_SecurityGroup> IAM_SecurityGroups; public List<IAM_SecurityGroup> IAM_SecurityGroups;
public string rootUID; public string rootUID;
@@ -189,16 +189,39 @@ namespace C4IT_IAM_Engine
tags.Add("GROUPTYPEPOSTFIX", GroupTypeTag); tags.Add("GROUPTYPEPOSTFIX", GroupTypeTag);
tags.Add("SCOPETAG", GroupScopeTag); tags.Add("SCOPETAG", GroupScopeTag);
template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, relativePath, sanitizedSegments, folderName) var replacementTags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (customTags != null)
{
foreach (var customTag in customTags)
replacementTags[customTag.Key] = customTag.Value;
}
foreach (var tag in tags)
replacementTags[tag.Key] = tag.Value;
var boundedNameContext = Helper.GetBoundedAdGroupTemplateContext(
template.NamingTemplate,
template.Type != SecurityGroupType.Traverse,
relativePath,
sanitizedSegments,
folderName,
replacementTags,
Helper.MaxAdGroupNameLength,
$"{template.Type}/{template.Scope} fuer '{newFolderPath}'");
var adjustedSegments = boundedNameContext.SanitizedSegments ?? Array.Empty<string>();
var adjustedRelativePath = adjustedSegments.Length > 0 ? string.Join("_", adjustedSegments) : string.Empty;
var adjustedFolderName = boundedNameContext.FolderName;
template.NamingTemplate = Helper.ApplyTemplatePlaceholders(template.NamingTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();
template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, relativePath, sanitizedSegments, folderName) template.DescriptionTemplate = Helper.ApplyTemplatePlaceholders(template.DescriptionTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();
template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, relativePath, sanitizedSegments, folderName) template.WildcardTemplate = Helper.ApplyTemplatePlaceholders(template.WildcardTemplate, template.Type != SecurityGroupType.Traverse, adjustedRelativePath, adjustedSegments, adjustedFolderName)
.ReplaceTags(customTags).ReplaceTags(tags) .ReplaceTags(customTags).ReplaceTags(tags)
.ToUpper(); .ToUpper();
@@ -480,9 +503,74 @@ namespace C4IT_IAM_Engine
return new DirectoryEntry("LDAP://" + domainName + "/" + matchedDistinguishedName, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing); return new DirectoryEntry("LDAP://" + domainName + "/" + matchedDistinguishedName, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
} }
private DirectoryEntry FindGroupEntryFromFolderAcl(string folderPath, string wildcardPattern)
{
if (string.IsNullOrWhiteSpace(folderPath) || string.IsNullOrWhiteSpace(wildcardPattern) || !Directory.Exists(folderPath))
return null;
Regex wildcardRegex;
try
{
wildcardRegex = new Regex(wildcardPattern, RegexOptions.IgnoreCase);
}
catch (Exception E)
{
cLogManager.DefaultLogger.LogException(E);
return null;
}
try
{
var directory = new DirectoryInfo(folderPath);
var rules = directory.GetAccessControl(AccessControlSections.Access)
.GetAccessRules(true, false, typeof(SecurityIdentifier))
.Cast<FileSystemAccessRule>();
var matchedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var domainContext = new PrincipalContext(ContextType.Domain, domainName, username, new NetworkCredential("", password).Password))
{
foreach (var rule in rules)
{
if (rule.AccessControlType != AccessControlType.Allow)
continue;
var sid = rule.IdentityReference?.Value;
if (string.IsNullOrWhiteSpace(sid) || sid == "S-1-1-0")
continue;
using (var group = GroupPrincipal.FindByIdentity(domainContext, IdentityType.Sid, sid))
{
var samAccountName = group?.SamAccountName;
if (string.IsNullOrWhiteSpace(samAccountName) || !wildcardRegex.IsMatch(samAccountName))
continue;
matchedNames.Add(samAccountName);
if (matchedNames.Count > 1)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"Multiple ACL groups on folder '{folderPath}' matched wildcard '{wildcardPattern}'. ACL-based reuse is skipped.");
return null;
}
}
}
}
if (matchedNames.Count == 0)
return null;
var matchedName = matchedNames.First();
DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing ACL-linked AD group '{matchedName}' via wildcard '{wildcardPattern}' on folder '{folderPath}'.");
return FindGroupEntry(matchedName);
}
catch (Exception E)
{
cLogManager.DefaultLogger.LogException(E);
return null;
}
}
private void ApplyExistingGroup(IAM_SecurityGroup secGroup, DirectoryEntry existingGroup) private void ApplyExistingGroup(IAM_SecurityGroup secGroup, DirectoryEntry existingGroup)
{ {
secGroup.ReusedExistingEntry = true; secGroup.CreatedNewEntry = false;
secGroup.UID = getSID(existingGroup); secGroup.UID = getSID(existingGroup);
if (existingGroup.Properties.Contains("sAMAccountName") && existingGroup.Properties["sAMAccountName"].Count > 0) if (existingGroup.Properties.Contains("sAMAccountName") && existingGroup.Properties["sAMAccountName"].Count > 0)
@@ -539,14 +627,20 @@ namespace C4IT_IAM_Engine
group.CommitChanges(); group.CommitChanges();
} }
public DirectoryEntry EnsureADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users) public DirectoryEntry EnsureADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users, string folderPath = null)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
try try
{ {
secGroup.ReusedExistingEntry = false; secGroup.CreatedNewEntry = false;
var existingGroup = FindGroupEntry(secGroup.Name); DirectoryEntry existingGroup = null;
if (existingGroup == null && AllowExistingGroupWildcardMatch) if (!ForceStrictAdGroupNames)
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
if (existingGroup == null && !ForceStrictAdGroupNames && string.IsNullOrWhiteSpace(folderPath))
existingGroup = FindGroupEntryByWildcard(ouPath, secGroup.WildcardPattern); existingGroup = FindGroupEntryByWildcard(ouPath, secGroup.WildcardPattern);
if (existingGroup == null) if (existingGroup == null)
@@ -567,12 +661,45 @@ namespace C4IT_IAM_Engine
} }
} }
public DirectoryEntry PreviewADGroup(string ouPath, IAM_SecurityGroup secGroup, string folderPath = null)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
try
{
secGroup.CreatedNewEntry = false;
DirectoryEntry existingGroup = null;
if (!ForceStrictAdGroupNames)
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
if (existingGroup == null && !ForceStrictAdGroupNames && string.IsNullOrWhiteSpace(folderPath))
existingGroup = FindGroupEntryByWildcard(ouPath, secGroup.WildcardPattern);
if (existingGroup == null)
return null;
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
catch (Exception E)
{
cLogManager.DefaultLogger.LogException(E);
throw;
}
finally
{
LogMethodEnd(MethodBase.GetCurrentMethod());
}
}
public DirectoryEntry CreateADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users) public DirectoryEntry CreateADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users)
{ {
LogMethodBegin(MethodBase.GetCurrentMethod()); LogMethodBegin(MethodBase.GetCurrentMethod());
try try
{ {
secGroup.ReusedExistingEntry = false; secGroup.CreatedNewEntry = false;
if (!GroupAllreadyExisting(secGroup.Name.ToUpper())) if (!GroupAllreadyExisting(secGroup.Name.ToUpper()))
{ {
@@ -609,6 +736,7 @@ namespace C4IT_IAM_Engine
var objectid = SecurityGroups.getSID(ent); var objectid = SecurityGroups.getSID(ent);
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}"); DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}");
secGroup.UID = objectid; secGroup.UID = objectid;
secGroup.CreatedNewEntry = true;
return ent; return ent;
} }
else else
@@ -686,7 +814,7 @@ namespace C4IT_IAM_Engine
public string Parent = ""; public string Parent = "";
public string description; public string description;
public string WildcardPattern; public string WildcardPattern;
public bool ReusedExistingEntry; public bool CreatedNewEntry;
public List<IAM_SecurityGroup> memberGroups; public List<IAM_SecurityGroup> memberGroups;
public string Name; public string Name;
public string technicalName; public string technicalName;

View File

@@ -101,6 +101,35 @@ namespace C4IT_IAM
} }
} }
public static bool TryGetDfsEntryPath(string dfsEntryPath, out string entryPath)
{
entryPath = string.Empty;
if (string.IsNullOrWhiteSpace(dfsEntryPath))
return false;
IntPtr buffer = IntPtr.Zero;
try
{
int result = NetDfsGetInfo(dfsEntryPath, null, null, 1, ref buffer);
if (result != NERR_Success || buffer == IntPtr.Zero)
return false;
DFS_INFO_1 info = (DFS_INFO_1)Marshal.PtrToStructure(buffer, typeof(DFS_INFO_1));
entryPath = info.EntryPath ?? dfsEntryPath;
return !string.IsNullOrWhiteSpace(entryPath);
}
catch (Exception ex)
{
DefaultLogger.LogException(ex);
return false;
}
finally
{
if (buffer != IntPtr.Zero)
NetApiBufferFree(buffer);
}
}
[DllImport("mpr.dll")] [DllImport("mpr.dll")]
private static extern int WNetAddConnection2(NetResource netResource, private static extern int WNetAddConnection2(NetResource netResource,
string password, string username, int flags); string password, string username, int flags);
@@ -123,6 +152,15 @@ namespace C4IT_IAM
ref int resume_handle ref int resume_handle
); );
[DllImport("Netapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int NetDfsGetInfo(
string DfsEntryPath,
string ServerName,
string ShareName,
int Level,
ref IntPtr Buffer
);
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@@ -205,4 +243,10 @@ namespace C4IT_IAM
return shi1_netname; return shi1_netname;
} }
} }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DFS_INFO_1
{
public string EntryPath;
}
} }

View File

@@ -17,6 +17,9 @@ namespace LiamNtfs
{ {
public class cNtfsBase public class cNtfsBase
{ {
private const int ErrorSessionCredentialConflict = 1219;
private const int ErrorNotConnected = 2250;
private cNtfsLogonInfo privLogonInfo = null; private cNtfsLogonInfo privLogonInfo = null;
private int scanningDepth; private int scanningDepth;
public PrincipalContext adContext = null; public PrincipalContext adContext = null;
@@ -58,15 +61,40 @@ namespace LiamNtfs
LogonInfo.UserSecret, LogonInfo.UserSecret,
LogonInfo.User, LogonInfo.User,
0); 0);
if(result == 1219)
if (result == ErrorSessionCredentialConflict)
{ {
result = WNetCancelConnection2(LogonInfo.TargetNetworkName,0,true); var originalResult = result;
if (result == 0) var cancelResult = WNetCancelConnection2(LogonInfo.TargetNetworkName, 0, true);
return await privLogonAsync(LogonInfo);
if (cancelResult == 0 || cancelResult == ErrorNotConnected)
{
result = WNetAddConnection2(
netResource,
LogonInfo.UserSecret,
LogonInfo.User,
0);
if (result == 0)
{
cLogManager.LogEntry(
$"NTFS login retry succeeded after SMB conflict on '{LogonInfo.TargetNetworkName}'. Initial add returned {originalResult} ({BuildWin32Message(originalResult)}), cancel returned {cancelResult} ({BuildWin32Message(cancelResult)}).",
LogLevels.Debug);
}
else
{
throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, result);
}
}
else
{
throw CreateNtfsLoginException(LogonInfo, originalResult, cancelResult, null);
}
} }
if (result != 0) if (result != 0)
{ {
throw new Win32Exception(result); throw CreateNtfsLoginException(LogonInfo, result, null, null);
} }
var FSLogon = true; var FSLogon = true;
@@ -83,6 +111,39 @@ namespace LiamNtfs
return false; return false;
} }
private static Exception CreateNtfsLoginException(cNtfsLogonInfo logonInfo, int addResult, int? cancelResult, int? retryResult)
{
var stages = new List<string>
{
$"WNetAddConnection2={addResult} ({BuildWin32Message(addResult)})"
};
if (cancelResult.HasValue)
stages.Add($"WNetCancelConnection2('{logonInfo?.TargetNetworkName}')={cancelResult.Value} ({BuildWin32Message(cancelResult.Value)})");
if (retryResult.HasValue)
stages.Add($"RetryAdd={retryResult.Value} ({BuildWin32Message(retryResult.Value)})");
var details = string.Join(", ", stages);
var message = $"NTFS login to '{logonInfo?.TargetNetworkName}' for user '{logonInfo?.User}' failed. {details}.";
if (addResult == ErrorSessionCredentialConflict)
{
message += " This usually indicates an existing SMB/DFS session to the same server with a different credential context.";
if (cancelResult == ErrorNotConnected)
message += " The exact DFS/share path was not registered as a disconnectable connection, so the conflicting session is likely held under a different remote name or DFS target.";
}
var nativeResult = retryResult ?? cancelResult ?? addResult;
return new InvalidOperationException(message, new Win32Exception(nativeResult));
}
private static string BuildWin32Message(int errorCode)
{
return new Win32Exception(errorCode).Message;
}
private async Task<bool> privRelogon() private async Task<bool> privRelogon()
{ {
if (privLogonInfo == null) if (privLogonInfo == null)
@@ -140,43 +201,67 @@ namespace LiamNtfs
scanningDepth = depth; scanningDepth = depth;
return privRequestFoldersListAsync(new DirectoryInfo(rootPath), depth); return privRequestFoldersListAsync(new DirectoryInfo(rootPath), depth);
} }
private List<cNtfsResultBase> privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null) private List<cNtfsResultBase> privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null)
{ {
ResetError(); ResetError();
List<cNtfsResultBase> folders = new List<cNtfsResultBase>(); var folders = new List<cNtfsResultBase>();
try try
{ {
var res = new List<cNtfsResultBase>();
if (depth == 0) if (depth == 0)
return res; return folders;
DirectoryInfo[] directories;
try try
{ {
foreach (var directory in rootPath.GetDirectories()) directories = rootPath.GetDirectories();
}
catch (Exception E)
{
cLogManager.LogEntry($"Could not enumerate directories under '{rootPath.FullName}': {E.Message}", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
return folders;
}
foreach (var directory in directories)
{
cNtfsResultFolder folder;
try
{ {
cNtfsResultFolder folder = new cNtfsResultFolder() folder = new cNtfsResultFolder()
{ {
ID = generateUniquId(directory.FullName), ID = generateUniquId(directory.FullName),
Path = directory.FullName, Path = directory.FullName,
CreatedDate = directory.CreationTimeUtc.ToString("s"), CreatedDate = directory.CreationTimeUtc.ToString("s"),
DisplayName = directory.Name, DisplayName = directory.Name,
Level = scanningDepth - depth+1, Level = scanningDepth - depth + 1,
Parent = parent Parent = parent
}; };
}
catch (Exception E)
{
cLogManager.LogEntry($"Could not read directory metadata for '{directory.FullName}': {E.Message}", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
continue;
}
folders.Add(folder); folders.Add(folder);
if (depth > 0) if (depth <= 0)
{ continue;
var result = privRequestFoldersListAsync(directory, depth - 1, folder);
try
{
var result = privRequestFoldersListAsync(directory, depth - 1, folder);
if (result != null && result.Count > 0)
folders.AddRange(result); folders.AddRange(result);
} }
catch (Exception E)
{
cLogManager.LogEntry($"Could not scan subtree '{directory.FullName}': {E.Message}", LogLevels.Warning);
cLogManager.LogException(E, LogLevels.Debug);
} }
} }
catch
{
return new List<cNtfsResultBase>(folders);
}
return new List<cNtfsResultBase>(folders); return folders;
} }
catch (Exception E) catch (Exception E)
{ {

View File

@@ -41,6 +41,9 @@
<PropertyGroup> <PropertyGroup>
<ApplicationIcon>C4IT.ico</ApplicationIcon> <ApplicationIcon>C4IT.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(MSBuildRuntimeType)' == 'Core' ">
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
@@ -61,6 +64,11 @@
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(MSBuildRuntimeType)' == 'Core' and Exists('$(MSBuildBinPath)/System.Resources.Extensions.dll') ">
<Reference Include="System.Resources.Extensions">
<HintPath>$(MSBuildBinPath)/System.Resources.Extensions.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\..\Common Code\Configuration\C4IT.Configuration.ConfigHelper.cs"> <Compile Include="..\..\Common Code\Configuration\C4IT.Configuration.ConfigHelper.cs">
<Link>Common\C4IT.Configuration.ConfigHelper.cs</Link> <Link>Common\C4IT.Configuration.ConfigHelper.cs</Link>
@@ -152,4 +160,4 @@
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@@ -449,23 +449,18 @@ namespace C4IT.LIAM.Activities
EnsureDataProviders(context); EnsureDataProviders(context);
var result = cloneTeam(ConfigID.Get(context), TeamId.Get(context), Name.Get(context), Description.Get(context), Visibility.Get(context), PartsToClone.Get(context), AdditionalMembers.Get(context), AdditionalOwners.Get(context)).GetAwaiter().GetResult(); var providerEntry = getDataProvider(ConfigID.Get(context));
Success.Set(context, result != null); var result = LiamWorkflowRuntime.CloneTeamAsync(
providerEntry?.Provider,
if (result?.Result?.targetResourceId != null) TeamId.Get(context),
{ Name.Get(context),
string idString = result.Result.targetResourceId.ToString(); Description.Get(context),
if (Guid.TryParse(idString, out Guid teamGuid)) Visibility.Get(context),
{ PartsToClone.Get(context),
CreatedTeamId.Set(context, teamGuid); AdditionalMembers.Get(context),
} AdditionalOwners.Get(context)).GetAwaiter().GetResult();
else Success.Set(context, result != null && result.Success);
{ CreatedTeamId.Set(context, result?.CreatedTeamId ?? Guid.Empty);
LogEntry($"targetResourceId '{idString}' is not a valid Guid.", LogLevels.Warning);
// Optional: alternativ hier einen Fehler werfen oder Guid.Empty zuweisen
CreatedTeamId.Set(context, Guid.Empty);
}
}
} }
catch (Exception E) catch (Exception E)
{ {
@@ -606,45 +601,17 @@ namespace C4IT.LIAM.Activities
ErrorMessage.Set(context, string.Empty); ErrorMessage.Set(context, string.Empty);
var entry = getDataProvider(ConfigID.Get(context)); var entry = getDataProvider(ConfigID.Get(context));
if (entry != null && entry.Provider is cLiamProviderExchange ex) var result = LiamWorkflowRuntime.CreateDistributionGroup(
{ entry?.Provider,
var result = ex.exchangeManager.CreateDistributionGroupWithOwnershipGroups( Name.Get(context),
Name.Get(context), Alias.Get(context),
Alias.Get(context), DistributionListDisplayName.Get(context),
DistributionListDisplayName.Get(context), PrimarySmtpAddress.Get(context));
PrimarySmtpAddress.Get(context), Success.Set(context, result.Success);
out string errorCode, ObjectGuid.Set(context, result.ObjectGuid);
out string errorMessage CreatedGroups.Set(context, result.CreatedGroups);
); ErrorCode.Set(context, result.ErrorCode);
ErrorCode.Set(context, errorCode); ErrorMessage.Set(context, result.ErrorMessage);
ErrorMessage.Set(context, errorMessage);
if (result != null)
{
Success.Set(context, true);
ObjectGuid.Set(context, result.Item1);
CreatedGroups.Set(context, result.Item2);
LogEntry(
$"Distribution group creation succeeded. ObjectGuid='{result.Item1}', CreatedGroups='{result.Item2?.Count ?? 0}'",
LogLevels.Info);
}
else
{
Success.Set(context, false);
LogEntry(
$"Distribution group creation failed [{errorCode}] {errorMessage}",
LogLevels.Error);
}
}
else
{
Success.Set(context, false);
ErrorCode.Set(context, "WF_PROVIDER_INVALID");
ErrorMessage.Set(context, $"Provider is not a cLiamProviderExchange for config '{ConfigID.Get(context)}'.");
LogEntry(
$"Distribution group creation failed [WF_PROVIDER_INVALID] Provider is not a cLiamProviderExchange for config '{ConfigID.Get(context)}'.",
LogLevels.Error);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -729,45 +696,17 @@ namespace C4IT.LIAM.Activities
ErrorMessage.Set(context, string.Empty); ErrorMessage.Set(context, string.Empty);
var entry = getDataProvider(ConfigID.Get(context)); var entry = getDataProvider(ConfigID.Get(context));
if (entry != null && entry.Provider is cLiamProviderExchange ex) var result = LiamWorkflowRuntime.CreateSharedMailbox(
{ entry?.Provider,
var result = ex.exchangeManager.CreateSharedMailboxWithOwnershipGroups( Name.Get(context),
Name.Get(context), Alias.Get(context),
Alias.Get(context), MailboxDisplayName.Get(context),
MailboxDisplayName.Get(context), PrimarySmtpAddress.Get(context));
PrimarySmtpAddress.Get(context), Success.Set(context, result.Success);
out string errorCode, ObjectGuid.Set(context, result.ObjectGuid);
out string errorMessage CreatedGroups.Set(context, result.CreatedGroups);
); ErrorCode.Set(context, result.ErrorCode);
ErrorCode.Set(context, errorCode); ErrorMessage.Set(context, result.ErrorMessage);
ErrorMessage.Set(context, errorMessage);
if (result != null)
{
Success.Set(context, true);
ObjectGuid.Set(context, result.Item1);
CreatedGroups.Set(context, result.Item2);
LogEntry(
$"Shared mailbox creation succeeded. ObjectGuid='{result.Item1}', CreatedGroups='{result.Item2?.Count ?? 0}'",
LogLevels.Info);
}
else
{
Success.Set(context, false);
LogEntry(
$"Shared mailbox creation failed [{errorCode}] {errorMessage}",
LogLevels.Error);
}
}
else
{
Success.Set(context, false);
ErrorCode.Set(context, "WF_PROVIDER_INVALID");
ErrorMessage.Set(context, $"Provider is not a cLiamProviderExchange for config '{ConfigID.Get(context)}'.");
LogEntry(
$"Shared mailbox creation failed [WF_PROVIDER_INVALID] Provider is not a cLiamProviderExchange for config '{ConfigID.Get(context)}'.",
LogLevels.Error);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -892,15 +831,16 @@ namespace C4IT.LIAM.Activities
var ownerList = OwnerSids.Expression != null ? OwnerSids.Get(context) : null; var ownerList = OwnerSids.Expression != null ? OwnerSids.Get(context) : null;
var memberList = MemberSids.Expression != null ? MemberSids.Get(context) : null; var memberList = MemberSids.Expression != null ? MemberSids.Get(context) : null;
var groups = adProv.CreateServiceGroups( var result = LiamWorkflowRuntime.CreateAdServiceGroups(
adProv,
svcName, svcName,
desc, desc,
scopeEnum, scopeEnum,
typeEnum, typeEnum,
ownerList, ownerList,
memberList); memberList);
Success.Set(context, groups != null); Success.Set(context, result.Success);
CreatedGroups.Set(context, groups); CreatedGroups.Set(context, result.CreatedGroups);
} }
else else
{ {
@@ -937,9 +877,9 @@ namespace C4IT.LIAM.Activities
{ {
EnsureDataProviders(context); EnsureDataProviders(context);
var cfgId = ConfigID.Get(context); var cfgId = ConfigID.Get(context);
var provider = getDataProvider(cfgId).Provider as cLiamProviderNtfs; var provider = getDataProvider(cfgId)?.Provider;
// evtl. CustomTags, OwnerSIDs etc. aus Activity-Inputs holen var result = LiamWorkflowRuntime.CreateDataAreaAsync(
var res = provider.CreateDataAreaAsync( provider,
NewFolderPath.Get(context), NewFolderPath.Get(context),
ParentFolderPath.Get(context), ParentFolderPath.Get(context),
/*customTags*/null, /*customTags*/null,
@@ -947,7 +887,7 @@ namespace C4IT.LIAM.Activities
/*readerSids*/null, /*readerSids*/null,
/*writerSids*/null /*writerSids*/null
).GetAwaiter().GetResult(); ).GetAwaiter().GetResult();
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(res))); ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result.ResultToken)));
} }
private void EnsureDataProviders(NativeActivityContext context) private void EnsureDataProviders(NativeActivityContext context)
{ {
@@ -1002,45 +942,22 @@ namespace C4IT.LIAM.Activities
EnsureDataProviders(context); EnsureDataProviders(context);
var cfgId = ConfigID.Get(context); var cfgId = ConfigID.Get(context);
var providerEntry = getDataProvider(cfgId);
var provider = providerEntry?.Provider as cLiamProviderNtfs;
var folderPath = FolderPath.Get(context);
if (provider == null || string.IsNullOrWhiteSpace(folderPath))
{
Success.Set(context, false);
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(new ResultToken(GetType().Name)
{
resultErrorId = 1,
resultMessage = provider == null ? "Configured provider is not NTFS or not initialized." : "Folder path is missing."
})));
return;
}
var ownerSids = OwnerSids.Expression != null ? OwnerSids.Get(context) : null; var ownerSids = OwnerSids.Expression != null ? OwnerSids.Get(context) : null;
var readerSids = ReaderSids.Expression != null ? ReaderSids.Get(context) : null; var readerSids = ReaderSids.Expression != null ? ReaderSids.Get(context) : null;
var writerSids = WriterSids.Expression != null ? WriterSids.Get(context) : null; var writerSids = WriterSids.Expression != null ? WriterSids.Get(context) : null;
var result = provider.EnsureMissingPermissionGroupsAsync( var providerEntry = getDataProvider(cfgId);
folderPath, var result = LiamWorkflowRuntime.EnsureNtfsPermissionGroupsAsync(
providerEntry?.Provider,
FolderPath.Get(context),
null, null,
NormalizeSidList(ownerSids), ownerSids,
NormalizeSidList(readerSids), readerSids,
NormalizeSidList(writerSids), writerSids,
EnsureTraverse.Get(context)).GetAwaiter().GetResult(); EnsureTraverse.Get(context)).GetAwaiter().GetResult();
Success.Set(context, result != null && result.resultErrorId == 0); Success.Set(context, result.Success);
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result))); ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result.ResultToken)));
}
private IEnumerable<string> NormalizeSidList(IEnumerable<string> rawSids)
{
if (rawSids == null)
return Enumerable.Empty<string>();
return rawSids
.Select(i => i?.Trim())
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase);
} }
private void EnsureDataProviders(NativeActivityContext context) private void EnsureDataProviders(NativeActivityContext context)

View File

@@ -435,52 +435,20 @@ namespace LiamWorkflowActivities
return null; return null;
} }
var lstSecurityGroups = await ProviderEntry.Provider.getSecurityGroupsAsync(ProviderEntry.Provider.GroupFilter); var result = await LiamWorkflowRuntime.GetSecurityGroupsFromProviderAsync(ProviderEntry.Provider);
if (lstSecurityGroups == null) if (!result.Success)
{ {
SetOperationErrorFromProvider( SetOperationError(result.ErrorCode, result.ErrorMessage);
ProviderEntry.Provider,
"WF_GET_SECURITYGROUPS_PROVIDER_CALL_FAILED",
"Provider returned null while reading security groups.");
return null; return null;
} }
if (lstSecurityGroups.Count == 0) if (result.SecurityGroups.Count == 0)
{ {
LogEntry($"No security groups found for Provider config class with ID {ProviderConfigClassID}", LogLevels.Warning); LogEntry($"No security groups found for Provider config class with ID {ProviderConfigClassID}", LogLevels.Warning);
return new List<SecurityGroupEntry>(); return new List<SecurityGroupEntry>();
} }
var SGs = new List<SecurityGroupEntry>(); return result.SecurityGroups;
foreach (var sg in lstSecurityGroups)
{
var entry = new SecurityGroupEntry
{
DisplayName = sg.TechnicalName,
TechnicalName = sg.UID,
TargetType = ((int)sg.Provider.ProviderType).ToString()
};
switch (sg)
{
case cLiamAdGroup adGroup:
entry.UID = adGroup.dn;
entry.Scope = adGroup.scope;
break;
case cLiamAdGroup2 adGroup:
entry.UID = adGroup.dn;
entry.Scope = adGroup.scope;
break;
case cLiamExchangeSecurityGroup exGroup:
entry.UID = exGroup.dn; // SID der Exchange-Gruppe
//entry.Scope = exGroup.dn; // Distinguished Name der Exchange-Gruppe
break;
}
SGs.Add(entry);
}
return SGs;
} }
catch (Exception E) catch (Exception E)
{ {
@@ -518,95 +486,22 @@ namespace LiamWorkflowActivities
return null; return null;
} }
var lstDataAreas = await ProviderEntry.Provider.getDataAreasAsync(ProviderEntry.Provider.MaxDepth); var result = await LiamWorkflowRuntime.GetDataAreasFromProviderAsync(
if (lstDataAreas == null) ProviderEntry.Provider,
ProviderEntry.ObjectID.ToString());
if (!result.Success)
{ {
SetOperationErrorFromProvider( SetOperationError(result.ErrorCode, result.ErrorMessage);
ProviderEntry.Provider,
"WF_GET_DATAAREAS_PROVIDER_CALL_FAILED",
"Provider returned null while reading data areas.");
return null; return null;
} }
if (lstDataAreas.Count <= 0) if (result.DataAreas.Count <= 0)
{ {
LogEntry($"No data areas found for Provider config class with ID {ProviderConfigClassID}", LogLevels.Warning); LogEntry($"No data areas found for Provider config class with ID {ProviderConfigClassID}", LogLevels.Warning);
return new List<DataAreaEntry>(); return new List<DataAreaEntry>();
} }
if (!await EnsureNtfsPermissionGroupsIfConfiguredAsync(ProviderEntry, lstDataAreas)) return result.DataAreas;
return null;
return lstDataAreas
.Select(DataArea =>
{
var ntfsPermissionArea = DataArea as cLiamNtfsPermissionDataAreaBase;
var adGrp = DataArea as cLiamAdGroupAsDataArea;
var exchMB = DataArea as cLiamExchangeSharedMailbox;
var exchDL = DataArea as cLiamExchangeDistributionGroup;
// 1) Owner
// - Shared Mailbox: OwnerGroupIdentifier
// - Distribution Group: OwnerGroupIdentifier
// - AD-Group: ManagedBySID
// - NTFS-Folder: OwnerGroupIdentifier
string owner = exchMB?.OwnerGroupIdentifier
?? exchDL?.OwnerGroupIdentifier
?? adGrp?.ManagedBySID
?? ntfsPermissionArea?.OwnerGroupIdentifier
?? string.Empty;
// 2) WriteSID
// - Shared Mailbox: FullAccessGroupSid
// - Distribution Group: MemberGroupSid
// - AD-Group: UID
// - NTFS-Folder: WriteGroupIdentifier
string write = exchMB != null
? exchMB.FullAccessGroupSid
: exchDL != null
? exchDL.MemberGroupSid
: adGrp?.UID
?? ntfsPermissionArea?.WriteGroupIdentifier
?? string.Empty;
// 3) ReadSID
// - Shared Mailbox: SendAsGroupSid
// - Distribution Group: (nicht verwendet)
// - NTFS-Folder: ReadGroupIdentifier
string read = exchMB != null
? exchMB.SendAsGroupSid
: ntfsPermissionArea?.ReadGroupIdentifier
?? string.Empty;
// 4) Traverse nur NTFS-Objekte
string traverse = ntfsPermissionArea?.TraverseGroupIdentifier ?? string.Empty;
// 5) CreatedDate nur NTFS-Objekte
string created = ntfsPermissionArea?.CreatedDate ?? DateTime.MinValue.ToString("o");
// 6) Description: nur AD-Group
string desc = adGrp?.Description ?? string.Empty;
return new DataAreaEntry
{
DisplayName = DataArea.DisplayName ?? string.Empty,
UID = DataArea.UID,
TechnicalName = DataArea.TechnicalName,
Description = desc,
TargetType = ((int)DataArea.Provider.ProviderType).ToString(),
ParentUID = DataArea.ParentUID ?? string.Empty,
Level = DataArea.Level.ToString(),
ConfigurationId = ProviderEntry.ObjectID.ToString(),
DataAreaType = DataArea.DataType.ToString(),
Owner = owner,
Write = write,
Read = read,
Traverse = traverse,
CreatedDate = created,
};
})
.ToList();
} }
catch (Exception E) catch (Exception E)
{ {
@@ -620,57 +515,6 @@ namespace LiamWorkflowActivities
} }
} }
private async Task<bool> EnsureNtfsPermissionGroupsIfConfiguredAsync(ProviderCacheEntry providerEntry, List<cLiamDataAreaBase> dataAreas)
{
if (!(providerEntry?.Provider is cLiamProviderNtfs ntfsProvider))
return true;
if (!IsAdditionalConfigurationEnabled(providerEntry.Provider, "EnsureNtfsPermissionGroups"))
return true;
foreach (var ntfsArea in dataAreas.OfType<cLiamNtfsFolder>())
{
var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath))
continue;
if (!Directory.Exists(folderPath))
{
LogEntry($"Skipping automatic NTFS permission group ensure for '{folderPath}' because the directory does not exist.", LogLevels.Warning);
continue;
}
var result = await ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath,
null,
null,
null,
null,
false);
if (result == null)
{
SetOperationError(
"WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED",
$"Automatic NTFS permission group ensure failed for '{folderPath}' because the provider returned no result.");
return false;
}
if (result.resultErrorId != 0)
{
SetOperationError(
"WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED",
$"Automatic NTFS permission group ensure failed for '{folderPath}': {result.resultMessage}");
return false;
}
await ntfsArea.ResolvePermissionGroupsAsync(folderPath);
}
return true;
}
private async Task<cLiamDataAreaBase> getDataAreaFromUID(string UID) private async Task<cLiamDataAreaBase> getDataAreaFromUID(string UID)
{ {
var CM = MethodBase.GetCurrentMethod(); var CM = MethodBase.GetCurrentMethod();

View File

@@ -85,6 +85,7 @@
</Compile> </Compile>
<Compile Include="C4IT.LIAM.WorkflowactivityBase.cs" /> <Compile Include="C4IT.LIAM.WorkflowactivityBase.cs" />
<Compile Include="C4IT.LIAM.WorkflowActivities.cs" /> <Compile Include="C4IT.LIAM.WorkflowActivities.cs" />
<Compile Include="LiamWorkflowRuntime.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -122,4 +123,4 @@
<None Include="SignSourceFiles.cmd" /> <None Include="SignSourceFiles.cmd" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

View File

@@ -0,0 +1,694 @@
using C4IT.LIAM;
using C4IT.Logging;
using C4IT.MsGraph;
using C4IT_IAM_Engine;
using LiamAD;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using static C4IT.Logging.cLogManager;
using static LiamAD.ADServiceGroupCreator;
namespace LiamWorkflowActivities
{
public class GetDataAreasOperationResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
public List<DataAreaEntry> DataAreas { get; set; } = new List<DataAreaEntry>();
public List<NtfsAutomaticEnsurePreviewEntry> AutomaticEnsurePreview { get; set; } = new List<NtfsAutomaticEnsurePreviewEntry>();
}
public class NtfsAutomaticEnsurePreviewEntry
{
public string FolderPath { get; set; } = string.Empty;
public bool WhatIf { get; set; } = true;
public string Message { get; set; } = string.Empty;
public List<string> WouldCreateGroups { get; set; } = new List<string>();
public List<string> WouldReuseGroups { get; set; } = new List<string>();
public List<string> WouldAddAclEntries { get; set; } = new List<string>();
public List<string> ExistingAclEntries { get; set; } = new List<string>();
public List<string> WouldEnsureTraverseGroups { get; set; } = new List<string>();
public List<string> Warnings { get; set; } = new List<string>();
}
public class GetSecurityGroupsOperationResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
public List<SecurityGroupEntry> SecurityGroups { get; set; } = new List<SecurityGroupEntry>();
}
public class NtfsOperationResult
{
public bool Success { get; set; }
public ResultToken ResultToken { get; set; }
}
public class AdServiceGroupOperationResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
public List<Tuple<string, string, string, string>> CreatedGroups { get; set; } = new List<Tuple<string, string, string, string>>();
}
public class ExchangeProvisionOperationResult
{
public bool Success { get; set; }
public Guid ObjectGuid { get; set; } = Guid.Empty;
public List<Tuple<string, string, string, string>> CreatedGroups { get; set; } = new List<Tuple<string, string, string, string>>();
public string ErrorCode { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
}
public class CloneTeamOperationResult
{
public bool Success { get; set; }
public Guid CreatedTeamId { get; set; } = Guid.Empty;
public cMsGraphResultBase Result { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string ErrorMessage { get; set; } = string.Empty;
}
public static class LiamWorkflowRuntime
{
public static async Task<GetDataAreasOperationResult> GetDataAreasFromProviderAsync(cLiamProviderBase provider, string configurationId = null, bool? simulateConfiguredNtfsPermissionEnsure = null)
{
var result = new GetDataAreasOperationResult();
if (provider == null)
{
result.ErrorCode = "WF_GET_DATAAREAS_PROVIDER_NOT_FOUND";
result.ErrorMessage = "Configured provider is not initialized.";
return result;
}
try
{
var dataAreas = await provider.getDataAreasAsync(provider.MaxDepth);
if (dataAreas == null)
{
SetErrorFromProvider(result, provider, "WF_GET_DATAAREAS_PROVIDER_CALL_FAILED", "Provider returned null while reading data areas.");
return result;
}
var simulateAutomaticEnsure = simulateConfiguredNtfsPermissionEnsure ?? IsWorkflowWhatIfEnabled(provider);
if (!await EnsureNtfsPermissionGroupsIfConfiguredAsync(provider, dataAreas, result, simulateAutomaticEnsure))
return result;
result.DataAreas = dataAreas
.Select(dataArea => MapDataAreaEntry(dataArea, configurationId))
.ToList();
result.Success = true;
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_GET_DATAAREAS_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
public static async Task<GetSecurityGroupsOperationResult> GetSecurityGroupsFromProviderAsync(cLiamProviderBase provider)
{
var result = new GetSecurityGroupsOperationResult();
if (provider == null)
{
result.ErrorCode = "WF_GET_SECURITYGROUPS_PROVIDER_NOT_FOUND";
result.ErrorMessage = "Configured provider is not initialized.";
return result;
}
try
{
var securityGroups = await provider.getSecurityGroupsAsync(provider.GroupFilter);
if (securityGroups == null)
{
SetErrorFromProvider(result, provider, "WF_GET_SECURITYGROUPS_PROVIDER_CALL_FAILED", "Provider returned null while reading security groups.");
return result;
}
result.SecurityGroups = securityGroups
.Select(MapSecurityGroupEntry)
.ToList();
result.Success = true;
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_GET_SECURITYGROUPS_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
public static async Task<NtfsOperationResult> CreateDataAreaAsync(
cLiamProviderBase provider,
string newFolderPath,
string parentFolderPath,
IDictionary<string, string> customTags,
IEnumerable<string> ownerSids,
IEnumerable<string> readerSids,
IEnumerable<string> writerSids)
{
var result = new NtfsOperationResult();
if (!(provider is cLiamProviderNtfs ntfsProvider))
{
result.ResultToken = CreateInvalidNtfsResultToken("Configured provider is not NTFS or not initialized.");
return result;
}
var token = await ntfsProvider.CreateDataAreaAsync(
newFolderPath,
parentFolderPath,
customTags,
NormalizeIdentifierList(ownerSids),
NormalizeIdentifierList(readerSids),
NormalizeIdentifierList(writerSids),
IsWorkflowWhatIfEnabled(provider));
if (token == null)
token = CreateInvalidNtfsResultToken(ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result while creating the data area.");
result.ResultToken = token;
result.Success = token != null && token.resultErrorId == 0;
return result;
}
public static async Task<NtfsOperationResult> EnsureNtfsPermissionGroupsAsync(
cLiamProviderBase provider,
string folderPath,
IDictionary<string, string> customTags,
IEnumerable<string> ownerSids,
IEnumerable<string> readerSids,
IEnumerable<string> writerSids,
bool ensureTraverseGroups)
{
var result = new NtfsOperationResult();
if (!(provider is cLiamProviderNtfs ntfsProvider) || string.IsNullOrWhiteSpace(folderPath))
{
result.ResultToken = CreateInvalidNtfsResultToken(provider is cLiamProviderNtfs
? "Folder path is missing."
: "Configured provider is not NTFS or not initialized.");
return result;
}
var token = await ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath,
customTags,
NormalizeIdentifierList(ownerSids),
NormalizeIdentifierList(readerSids),
NormalizeIdentifierList(writerSids),
ensureTraverseGroups,
IsWorkflowWhatIfEnabled(provider));
if (token == null)
token = CreateInvalidNtfsResultToken(ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result while ensuring NTFS permission groups.");
result.ResultToken = token;
result.Success = token != null && token.resultErrorId == 0;
return result;
}
public static AdServiceGroupOperationResult CreateAdServiceGroups(
cLiamProviderBase provider,
string serviceName,
string description,
eLiamAccessRoleScopes scope,
ADGroupType groupType,
IEnumerable<string> ownerSids,
IEnumerable<string> memberSids)
{
var result = new AdServiceGroupOperationResult();
if (!(provider is cLiamProviderAD adProvider))
{
result.ErrorCode = "WF_PROVIDER_INVALID";
result.ErrorMessage = "Configured provider is not Active Directory or not initialized.";
return result;
}
try
{
var groups = adProvider.CreateServiceGroups(
serviceName,
description,
scope,
groupType,
NormalizeIdentifierList(ownerSids),
NormalizeIdentifierList(memberSids));
result.Success = groups != null;
result.CreatedGroups = groups ?? new List<Tuple<string, string, string, string>>();
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_ACTIVITY_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
public static async Task<CloneTeamOperationResult> CloneTeamAsync(
cLiamProviderBase provider,
string teamId,
string name,
string description,
int visibility,
int partsToClone,
string additionalMembers,
string additionalOwners)
{
var result = new CloneTeamOperationResult();
if (!(provider is cLiamProviderMsTeams msTeamsProvider))
{
result.ErrorCode = "WF_PROVIDER_INVALID";
result.ErrorMessage = "Configured provider is not MsTeams or not initialized.";
return result;
}
try
{
var cloneResult = await msTeamsProvider.cloneTeam(teamId, name, description, visibility, partsToClone, additionalMembers, additionalOwners);
result.Result = cloneResult;
result.Success = cloneResult != null;
if (cloneResult?.Result?.targetResourceId != null)
{
string idString = cloneResult.Result.targetResourceId.ToString();
Guid createdTeamId;
if (Guid.TryParse(idString, out createdTeamId))
{
result.CreatedTeamId = createdTeamId;
}
else
{
LogEntry($"targetResourceId '{idString}' is not a valid Guid.", LogLevels.Warning);
}
}
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_ACTIVITY_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
public static ExchangeProvisionOperationResult CreateDistributionGroup(
cLiamProviderBase provider,
string name,
string alias,
string displayName,
string primarySmtpAddress)
{
var result = new ExchangeProvisionOperationResult();
if (!(provider is cLiamProviderExchange exchangeProvider))
{
result.ErrorCode = "WF_PROVIDER_INVALID";
result.ErrorMessage = "Configured provider is not Exchange or not initialized.";
return result;
}
try
{
var created = exchangeProvider.exchangeManager.CreateDistributionGroupWithOwnershipGroups(
name,
alias,
displayName,
primarySmtpAddress,
out string errorCode,
out string errorMessage);
result.ErrorCode = errorCode ?? string.Empty;
result.ErrorMessage = errorMessage ?? string.Empty;
if (created != null)
{
result.Success = true;
result.ObjectGuid = created.Item1;
result.CreatedGroups = created.Item2 ?? new List<Tuple<string, string, string, string>>();
}
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_ACTIVITY_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
public static ExchangeProvisionOperationResult CreateSharedMailbox(
cLiamProviderBase provider,
string name,
string alias,
string displayName,
string primarySmtpAddress)
{
var result = new ExchangeProvisionOperationResult();
if (!(provider is cLiamProviderExchange exchangeProvider))
{
result.ErrorCode = "WF_PROVIDER_INVALID";
result.ErrorMessage = "Configured provider is not Exchange or not initialized.";
return result;
}
try
{
var created = exchangeProvider.exchangeManager.CreateSharedMailboxWithOwnershipGroups(
name,
alias,
displayName,
primarySmtpAddress,
out string errorCode,
out string errorMessage);
result.ErrorCode = errorCode ?? string.Empty;
result.ErrorMessage = errorMessage ?? string.Empty;
if (created != null)
{
result.Success = true;
result.ObjectGuid = created.Item1;
result.CreatedGroups = created.Item2 ?? new List<Tuple<string, string, string, string>>();
}
return result;
}
catch (Exception ex)
{
LogException(ex);
result.ErrorCode = "WF_ACTIVITY_EXCEPTION";
result.ErrorMessage = ex.Message;
return result;
}
}
private static ResultToken CreateInvalidNtfsResultToken(string message)
{
return new ResultToken("LiamWorkflowRuntime")
{
resultErrorId = 1,
resultMessage = message ?? string.Empty
};
}
private static IEnumerable<string> NormalizeIdentifierList(IEnumerable<string> identifiers)
{
if (identifiers == null)
return Enumerable.Empty<string>();
return identifiers
.Select(i => i?.Trim())
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static async Task<bool> EnsureNtfsPermissionGroupsIfConfiguredAsync(cLiamProviderBase provider, List<cLiamDataAreaBase> dataAreas, GetDataAreasOperationResult result, bool simulateOnly)
{
if (!(provider is cLiamProviderNtfs ntfsProvider))
return true;
if (!IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups"))
return true;
foreach (var ntfsArea in dataAreas.OfType<cLiamNtfsFolder>())
{
var folderPath = ntfsArea.TechnicalName;
if (string.IsNullOrWhiteSpace(folderPath))
continue;
if (!Directory.Exists(folderPath))
{
LogEntry($"Skipping automatic NTFS permission group ensure for '{folderPath}' because the directory does not exist.", LogLevels.Warning);
continue;
}
var ensureResult = await ntfsProvider.EnsureMissingPermissionGroupsAsync(
folderPath,
null,
null,
null,
null,
false,
simulateOnly);
if (ensureResult == null)
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED";
result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}' because the provider returned no result.";
return false;
}
if (ensureResult.resultErrorId != 0)
{
result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED";
result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}': {ensureResult.resultMessage}";
return false;
}
if (simulateOnly)
{
LogAutomaticNtfsEnsurePreviewDebug(folderPath, ensureResult);
result.AutomaticEnsurePreview.Add(MapAutomaticEnsurePreview(folderPath, ensureResult));
continue;
}
LogAutomaticNtfsEnsureDebug(folderPath, ensureResult);
await ntfsArea.ResolvePermissionGroupsAsync(folderPath);
}
return true;
}
private static NtfsAutomaticEnsurePreviewEntry MapAutomaticEnsurePreview(string folderPath, ResultToken ensureResult)
{
return new NtfsAutomaticEnsurePreviewEntry
{
FolderPath = folderPath ?? string.Empty,
WhatIf = true,
Message = ensureResult?.resultMessage ?? string.Empty,
WouldCreateGroups = ensureResult?.createdGroups?.ToList() ?? new List<string>(),
WouldReuseGroups = ensureResult?.reusedGroups?.ToList() ?? new List<string>(),
WouldAddAclEntries = ensureResult?.addedAclEntries?.ToList() ?? new List<string>(),
ExistingAclEntries = ensureResult?.skippedAclEntries?.ToList() ?? new List<string>(),
WouldEnsureTraverseGroups = ensureResult?.ensuredTraverseGroups?.ToList() ?? new List<string>(),
Warnings = ensureResult?.warnings?.ToList() ?? new List<string>()
};
}
private static void LogAutomaticNtfsEnsurePreviewDebug(string folderPath, ResultToken ensureResult)
{
if (ensureResult == null)
return;
LogEntry(
$"Automatic NTFS permission group ensure preview finished for '{folderPath}'. " +
$"WouldCreateGroups={ensureResult.createdGroups.Count}, " +
$"WouldReuseGroups={ensureResult.reusedGroups.Count}, " +
$"WouldAddAcls={ensureResult.addedAclEntries.Count}, " +
$"ExistingAcls={ensureResult.skippedAclEntries.Count}, " +
$"WouldEnsureTraverseGroups={ensureResult.ensuredTraverseGroups.Count}, " +
$"Warnings={ensureResult.warnings.Count}, " +
$"ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Debug);
}
private static void LogAutomaticNtfsEnsureDebug(string folderPath, ResultToken ensureResult)
{
if (ensureResult == null)
return;
LogEntry(
$"Automatic NTFS permission group ensure finished for '{folderPath}'. " +
$"CreatedGroups={ensureResult.createdGroups.Count}, " +
$"ReusedGroups={ensureResult.reusedGroups.Count}, " +
$"AddedAcls={ensureResult.addedAclEntries.Count}, " +
$"SkippedAcls={ensureResult.skippedAclEntries.Count}, " +
$"TraverseGroups={ensureResult.ensuredTraverseGroups.Count}, " +
$"Warnings={ensureResult.warnings.Count}, " +
$"ResultMessage='{ensureResult.resultMessage ?? string.Empty}'",
LogLevels.Debug);
if (ensureResult.createdGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure detected missing AD groups for '{folderPath}' and created them: {string.Join(", ", ensureResult.createdGroups)}",
LogLevels.Debug);
}
if (ensureResult.reusedGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure reused existing AD groups for '{folderPath}': {string.Join(", ", ensureResult.reusedGroups)}",
LogLevels.Debug);
}
if (ensureResult.addedAclEntries.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure added missing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.addedAclEntries)}",
LogLevels.Debug);
}
if (ensureResult.skippedAclEntries.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure kept existing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.skippedAclEntries)}",
LogLevels.Debug);
}
if (ensureResult.ensuredTraverseGroups.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure touched traverse groups for '{folderPath}': {string.Join(", ", ensureResult.ensuredTraverseGroups)}",
LogLevels.Debug);
}
if (ensureResult.warnings.Count > 0)
{
LogEntry(
$"Automatic NTFS permission group ensure produced warnings for '{folderPath}': {string.Join(" | ", ensureResult.warnings)}",
LogLevels.Debug);
}
}
private static bool IsAdditionalConfigurationEnabled(cLiamProviderBase provider, string key)
{
if (provider?.AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key))
return false;
if (!provider.AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue))
return false;
return rawValue.Equals("true", StringComparison.OrdinalIgnoreCase)
|| rawValue.Equals("1", StringComparison.OrdinalIgnoreCase)
|| rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
private static bool IsWorkflowWhatIfEnabled(cLiamProviderBase provider)
{
return IsAdditionalConfigurationEnabled(provider, "WhatIf");
}
private static void SetErrorFromProvider(GetDataAreasOperationResult result, cLiamProviderBase provider, string fallbackCode, string fallbackMessage)
{
var error = ExtractProviderError(provider, fallbackCode, fallbackMessage);
result.ErrorCode = error.Item1;
result.ErrorMessage = error.Item2;
}
private static void SetErrorFromProvider(GetSecurityGroupsOperationResult result, cLiamProviderBase provider, string fallbackCode, string fallbackMessage)
{
var error = ExtractProviderError(provider, fallbackCode, fallbackMessage);
result.ErrorCode = error.Item1;
result.ErrorMessage = error.Item2;
}
private static Tuple<string, string> ExtractProviderError(cLiamProviderBase provider, string fallbackCode, string fallbackMessage)
{
if (provider is cLiamProviderExchange exchangeProvider)
{
var code = exchangeProvider.GetLastErrorCode();
var message = exchangeProvider.GetLastErrorMessage();
if (!string.IsNullOrWhiteSpace(code) || !string.IsNullOrWhiteSpace(message))
{
return Tuple.Create(
string.IsNullOrWhiteSpace(code) ? fallbackCode : code,
string.IsNullOrWhiteSpace(message) ? fallbackMessage : message);
}
}
var providerMessage = provider?.GetLastErrorMessage();
return Tuple.Create(
fallbackCode,
string.IsNullOrWhiteSpace(providerMessage) ? fallbackMessage : providerMessage);
}
private static DataAreaEntry MapDataAreaEntry(cLiamDataAreaBase dataArea, string configurationId)
{
var ntfsPermissionArea = dataArea as cLiamNtfsPermissionDataAreaBase;
var ntfsFolder = dataArea as cLiamNtfsFolder;
var adGroup = dataArea as cLiamAdGroupAsDataArea;
var exchangeMailbox = dataArea as cLiamExchangeSharedMailbox;
var exchangeDistribution = dataArea as cLiamExchangeDistributionGroup;
var owner = exchangeMailbox?.OwnerGroupIdentifier
?? exchangeDistribution?.OwnerGroupIdentifier
?? adGroup?.ManagedBySID
?? ntfsPermissionArea?.OwnerGroupIdentifier
?? string.Empty;
var write = exchangeMailbox != null
? exchangeMailbox.FullAccessGroupSid
: exchangeDistribution != null
? exchangeDistribution.MemberGroupSid
: adGroup?.UID
?? ntfsPermissionArea?.WriteGroupIdentifier
?? string.Empty;
var read = exchangeMailbox != null
? exchangeMailbox.SendAsGroupSid
: ntfsPermissionArea?.ReadGroupIdentifier
?? string.Empty;
var traverse = ntfsPermissionArea?.TraverseGroupIdentifier ?? string.Empty;
var created = ntfsPermissionArea?.CreatedDate ?? DateTime.MinValue.ToString("o");
var description = adGroup?.Description ?? string.Empty;
return new DataAreaEntry
{
DisplayName = dataArea.DisplayName ?? string.Empty,
UID = dataArea.UID ?? string.Empty,
TechnicalName = dataArea.TechnicalName ?? string.Empty,
Description = description,
TargetType = ((int)dataArea.Provider.ProviderType).ToString(),
ParentUID = dataArea.ParentUID ?? string.Empty,
Level = dataArea.Level.ToString(),
Owner = owner,
Write = write,
Read = read,
Traverse = traverse,
CreatedDate = created,
ConfigurationId = configurationId ?? string.Empty,
BaseFolder = ntfsFolder?.Share?.TechnicalName ?? dataArea.Provider?.RootPath ?? string.Empty,
UniqueId = dataArea.UID ?? string.Empty,
DataAreaType = dataArea.DataType.ToString()
};
}
private static SecurityGroupEntry MapSecurityGroupEntry(cLiamDataAreaBase securityGroup)
{
var entry = new SecurityGroupEntry
{
DisplayName = securityGroup.TechnicalName,
TechnicalName = securityGroup.UID,
TargetType = ((int)securityGroup.Provider.ProviderType).ToString()
};
switch (securityGroup)
{
case cLiamAdGroup adGroup:
entry.UID = adGroup.dn;
entry.Scope = adGroup.scope;
break;
case cLiamAdGroup2 adGroup2:
entry.UID = adGroup2.dn;
entry.Scope = adGroup2.scope;
break;
case cLiamExchangeSecurityGroup exchangeGroup:
entry.UID = exchangeGroup.dn;
break;
}
return entry;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="Build">
<Message Text="Skipping LiamWorkflowDiagnostics on non-Windows because the WPF diagnostics tool requires Windows MSBuild." Importance="high" />
<MakeDir Directories="$(OutputPath)" />
<WriteLinesToFile File="$(OutputPath)LiamWorkflowDiagnostics.skipped.txt" Lines="Skipped on non-Windows because the WPF diagnostics tool requires Windows MSBuild." Overwrite="true" />
</Target>
<Target Name="Rebuild" DependsOnTargets="Clean;Build" />
<Target Name="Clean">
<Delete Files="$(OutputPath)LiamWorkflowDiagnostics.skipped.txt" />
</Target>
</Project>

View File

@@ -18,6 +18,9 @@
<SccAuxPath>SAK</SccAuxPath> <SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider> <SccProvider>SAK</SccProvider>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ApplicationIcon>AppIcon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
@@ -95,6 +98,7 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</ApplicationDefinition> </ApplicationDefinition>
<Resource Include="AppIcon.ico" />
<Page Include="MainWindow.xaml"> <Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
@@ -116,5 +120,6 @@
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" Condition=" '$(OS)' == 'Windows_NT' " />
</Project> <Import Project="LiamWorkflowDiagnostics.NonWindows.targets" Condition=" '$(OS)' != 'Windows_NT' " />
</Project>

View File

@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="LIAM Workflow Diagnostics" Width="1100" Title="LIAM Workflow Diagnostics" Width="1100"
Icon="/AppIcon.ico"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid Margin="12"> <Grid Margin="12">
@@ -454,14 +455,32 @@
</Grid> </Grid>
</GroupBox> </GroupBox>
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,8"> <Grid Grid.Row="5" Margin="0,0,0,8">
<Button x:Name="LoadJsonButton" Content="Load JSON" Width="110" Margin="0,0,8,0" Click="LoadJsonButton_Click"/> <Grid.ColumnDefinitions>
<Button x:Name="ExportJsonButton" Content="Export Sanitized JSON" Width="170" Margin="0,0,8,0" Click="ExportJsonButton_Click"/> <ColumnDefinition Width="Auto"/>
<Button x:Name="InitializeButton" Content="Initialize Provider" Width="160" Margin="0,0,8,0" Click="InitializeButton_Click"/> <ColumnDefinition Width="*"/>
<Button x:Name="FetchDataAreasButton" Content="Fetch Data Areas" Width="160" Margin="0,0,8,0" Click="FetchDataAreasButton_Click"/> </Grid.ColumnDefinitions>
<Button x:Name="FetchSecurityGroupsButton" Content="Fetch Security Groups" Width="170" Margin="0,0,8,0" Click="FetchSecurityGroupsButton_Click"/>
<Button x:Name="ClearLogButton" Content="Clear Log" Width="110" Click="ClearLogButton_Click"/> <CheckBox x:Name="WhatIfCheckBox"
</StackPanel> Grid.Column="0"
Content="WhatIf aktiv (Schreibaktionen nur simulieren)"
Margin="0,0,16,0"
VerticalAlignment="Center"
IsChecked="True"
Click="WhatIfCheckBox_Click"/>
<WrapPanel Grid.Column="1"
HorizontalAlignment="Right"
ItemHeight="26"
Orientation="Horizontal">
<Button x:Name="LoadJsonButton" Content="Load JSON" Width="110" Margin="0,0,8,8" Click="LoadJsonButton_Click"/>
<Button x:Name="ExportJsonButton" Content="Export Sanitized JSON" Width="170" Margin="0,0,8,8" Click="ExportJsonButton_Click"/>
<Button x:Name="InitializeButton" Content="Initialize Provider" Width="160" Margin="0,0,8,8" Click="InitializeButton_Click"/>
<Button x:Name="FetchDataAreasButton" Content="Fetch Data Areas" Width="160" Margin="0,0,8,8" Click="FetchDataAreasButton_Click"/>
<Button x:Name="FetchSecurityGroupsButton" Content="Fetch Security Groups" Width="170" Margin="0,0,8,8" Click="FetchSecurityGroupsButton_Click"/>
<Button x:Name="ClearLogButton" Content="Clear Log" Width="110" Margin="0,0,0,8" Click="ClearLogButton_Click"/>
</WrapPanel>
</Grid>
<GroupBox Header="Result" Grid.Row="6" Margin="0,0,0,8"> <GroupBox Header="Result" Grid.Row="6" Margin="0,0,0,8">
<Grid Margin="10"> <Grid Margin="10">

View File

@@ -113,6 +113,7 @@ namespace LiamWorkflowDiagnostics
MsTeamsVisibilityComboBox.SelectedValue = MsTeamsVisibilityPrivate; MsTeamsVisibilityComboBox.SelectedValue = MsTeamsVisibilityPrivate;
MsTeamsCloneSettingsCheckBox.IsChecked = true; MsTeamsCloneSettingsCheckBox.IsChecked = true;
MsTeamsCloneChannelsCheckBox.IsChecked = true; MsTeamsCloneChannelsCheckBox.IsChecked = true;
WhatIfCheckBox.IsChecked = true;
FetchDataAreasButton.IsEnabled = false; FetchDataAreasButton.IsEnabled = false;
FetchSecurityGroupsButton.IsEnabled = false; FetchSecurityGroupsButton.IsEnabled = false;
@@ -179,12 +180,13 @@ namespace LiamWorkflowDiagnostics
ApplyMatrix42Environment(ServerNameTextBox.Text, UseHttpsCheckBox.IsChecked ?? false); ApplyMatrix42Environment(ServerNameTextBox.Text, UseHttpsCheckBox.IsChecked ?? false);
ApplyLicense(LicenseTextBox.Text); ApplyLicense(LicenseTextBox.Text);
_session = new ProviderTestSession(msg => AppendLog(msg)); var session = new ProviderTestSession(msg => AppendLog(msg));
var success = await _session.InitializeAsync(providerData, maskToken, CreateProviderInstance, providerConfigClassId, providerConfigObjectId); _session = session;
var success = await Task.Run(() => session.InitializeAsync(providerData, maskToken, CreateProviderInstance, providerConfigClassId, providerConfigObjectId));
if (success) if (success)
{ {
AppendLog("Provider initialisiert und authentifiziert.", LogLevels.Info); AppendLog("Provider initialisiert und authentifiziert.", LogLevels.Info);
ResultTextBox.Text = _session.SanitizedConfigJson; ResultTextBox.Text = session.SanitizedConfigJson;
} }
else else
{ {
@@ -240,6 +242,18 @@ namespace LiamWorkflowDiagnostics
AppendLog("Log gelöscht.", LogLevels.Debug); AppendLog("Log gelöscht.", LogLevels.Debug);
} }
private void WhatIfCheckBox_Click(object sender, RoutedEventArgs e)
{
if (_isInitializingUi)
return;
UpdateActionHint();
SaveSettings();
AppendLog(IsWhatIfEnabled
? "WhatIf aktiviert. Schreibende Aktionen im Diagnostics Tool werden nur simuliert."
: "WhatIf deaktiviert. Schreibende Aktionen im Diagnostics Tool werden real ausgeführt.", LogLevels.Warning);
}
private void LogListBox_KeyDown(object sender, KeyEventArgs e) private void LogListBox_KeyDown(object sender, KeyEventArgs e)
{ {
if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control) if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)
@@ -327,6 +341,7 @@ namespace LiamWorkflowDiagnostics
ServerNameTextBox.Text = settings.ServerName ?? string.Empty; ServerNameTextBox.Text = settings.ServerName ?? string.Empty;
UseHttpsCheckBox.IsChecked = settings.UseHttps; UseHttpsCheckBox.IsChecked = settings.UseHttps;
LicenseTextBox.Text = settings.License ?? string.Empty; LicenseTextBox.Text = settings.License ?? string.Empty;
WhatIfCheckBox.IsChecked = settings.EnableWhatIf;
NtfsCreateFolderPathTextBox.Text = settings.NtfsCreateFolderPath ?? string.Empty; NtfsCreateFolderPathTextBox.Text = settings.NtfsCreateFolderPath ?? string.Empty;
NtfsCreateParentPathTextBox.Text = settings.NtfsCreateParentPath ?? string.Empty; NtfsCreateParentPathTextBox.Text = settings.NtfsCreateParentPath ?? string.Empty;
NtfsCreateOwnerSidsTextBox.Text = settings.NtfsCreateOwnerSids ?? string.Empty; NtfsCreateOwnerSidsTextBox.Text = settings.NtfsCreateOwnerSids ?? string.Empty;
@@ -405,6 +420,7 @@ namespace LiamWorkflowDiagnostics
ServerName = ServerNameTextBox.Text ?? string.Empty, ServerName = ServerNameTextBox.Text ?? string.Empty,
UseHttps = UseHttpsCheckBox.IsChecked ?? false, UseHttps = UseHttpsCheckBox.IsChecked ?? false,
License = LicenseTextBox.Text ?? string.Empty, License = LicenseTextBox.Text ?? string.Empty,
EnableWhatIf = WhatIfCheckBox.IsChecked ?? true,
GroupStrategy = GroupStrategyCombo.SelectedItem is eLiamGroupStrategies gs ? (int)gs : (int)eLiamGroupStrategies.Ntfs_AGDLP, GroupStrategy = GroupStrategyCombo.SelectedItem is eLiamGroupStrategies gs ? (int)gs : (int)eLiamGroupStrategies.Ntfs_AGDLP,
NtfsCreateFolderPath = NtfsCreateFolderPathTextBox.Text ?? string.Empty, NtfsCreateFolderPath = NtfsCreateFolderPathTextBox.Text ?? string.Empty,
NtfsCreateParentPath = NtfsCreateParentPathTextBox.Text ?? string.Empty, NtfsCreateParentPath = NtfsCreateParentPathTextBox.Text ?? string.Empty,
@@ -522,9 +538,13 @@ namespace LiamWorkflowDiagnostics
break; break;
} }
var modeHint = IsWhatIfEnabled
? " WhatIf ist aktiv: Schreibende Diagnostics-Aktionen werden nur simuliert."
: " WhatIf ist deaktiviert: Schreibende Diagnostics-Aktionen werden real ausgeführt.";
ActionHintTextBlock.Text = providerReady ActionHintTextBlock.Text = providerReady
? providerHint ? providerHint + modeHint
: $"Initialisiere zuerst einen Provider. {providerHint}"; : $"Initialisiere zuerst einen Provider. {providerHint}{modeHint}";
} }
private async void FetchDataAreasButton_Click(object sender, RoutedEventArgs e) private async void FetchDataAreasButton_Click(object sender, RoutedEventArgs e)
@@ -537,39 +557,37 @@ namespace LiamWorkflowDiagnostics
try try
{ {
var maxDepth = _session.Provider.MaxDepth >= 0 ? _session.Provider.MaxDepth : 1; var provider = _session.Provider;
var configurationId = !string.IsNullOrWhiteSpace(_session.ProviderConfigObjectId)
? _session.ProviderConfigObjectId
: (_session.ProviderConfigId ?? string.Empty);
var runWhatIf = IsWhatIfEnabled;
var maxDepth = provider.MaxDepth;
AppendLog($"Lese DataAreas (MaxDepth={maxDepth}) ..."); AppendLog($"Lese DataAreas (MaxDepth={maxDepth}) ...");
var areas = await _session.Provider.getDataAreasAsync(maxDepth); var result = await Task.Run(() => LiamWorkflowRuntime.GetDataAreasFromProviderAsync(
if (areas == null) provider,
{ configurationId,
var providerMessage = _session.Provider.GetLastErrorMessage(); runWhatIf));
if (_session.Provider is cLiamProviderExchange exchangeProvider) ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
{
var code = exchangeProvider.GetLastErrorCode();
if (string.IsNullOrWhiteSpace(code))
code = "EXCH_GET_DATAAREAS_FAILED";
AppendLog($"DataAreas-Call fehlgeschlagen [{code}]: {providerMessage}", LogLevels.Error);
}
else
{
AppendLog($"DataAreas-Call fehlgeschlagen: {providerMessage}", LogLevels.Error);
}
ResultTextBox.Text = "[]"; if (!result.Success)
{
AppendLog($"DataAreas-Call fehlgeschlagen [{result.ErrorCode}]: {result.ErrorMessage}", LogLevels.Error);
return; return;
} }
if (areas.Count == 0) if (result.DataAreas.Count == 0)
{ {
AppendLog("Keine DataAreas gefunden.", LogLevels.Warning); AppendLog("Keine DataAreas gefunden.", LogLevels.Warning);
ResultTextBox.Text = "[]";
return; return;
} }
var entries = ConvertDataAreas(areas); if (runWhatIf && result.AutomaticEnsurePreview != null && result.AutomaticEnsurePreview.Count > 0)
var json = JsonConvert.SerializeObject(entries, Formatting.Indented); {
ResultTextBox.Text = json; AppendLog($"EnsureNtfsPermissionGroups wurde nur simuliert fuer {result.AutomaticEnsurePreview.Count} Ordner. Details stehen im Result-JSON.", LogLevels.Warning);
AppendLog($"DataAreas erhalten: {entries.Count}"); }
AppendLog($"DataAreas erhalten: {result.DataAreas.Count}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -580,7 +598,7 @@ namespace LiamWorkflowDiagnostics
private async void ExecuteNtfsCreateButton_Click(object sender, RoutedEventArgs e) private async void ExecuteNtfsCreateButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("NTFS Folder Create", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS"); var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS");
var folderPath = GetRequiredText(NtfsCreateFolderPathTextBox.Text, "New Folder Path"); var folderPath = GetRequiredText(NtfsCreateFolderPathTextBox.Text, "New Folder Path");
@@ -594,41 +612,101 @@ namespace LiamWorkflowDiagnostics
var ownerSids = ParseIdentifierList(NtfsCreateOwnerSidsTextBox.Text, "Owner SIDs"); var ownerSids = ParseIdentifierList(NtfsCreateOwnerSidsTextBox.Text, "Owner SIDs");
if (ownerSids.Count == 0) if (ownerSids.Count == 0)
throw new InvalidOperationException("Owner SIDs: mindestens ein Eintrag ist fuer die Ordneranlage erforderlich."); throw new InvalidOperationException("Owner SIDs: mindestens ein Eintrag ist fuer die Ordneranlage erforderlich.");
var readerSids = ParseIdentifierList(NtfsCreateReaderSidsTextBox.Text, "Reader SIDs");
var writerSids = ParseIdentifierList(NtfsCreateWriterSidsTextBox.Text, "Writer SIDs");
var result = EnsureSuccessfulResultToken(await provider.CreateDataAreaAsync( await ExecuteProviderActionAsync("NTFS Folder Create", async () =>
folderPath, {
parentPath, var result = await Task.Run(() => LiamWorkflowRuntime.CreateDataAreaAsync(
ParseKeyValueLines(CustomTagsTextBox.Text, "Custom Tags"), provider,
ownerSids, folderPath,
ParseIdentifierList(NtfsCreateReaderSidsTextBox.Text, "Reader SIDs"), parentPath,
ParseIdentifierList(NtfsCreateWriterSidsTextBox.Text, "Writer SIDs")), null,
"NTFS Folder Create"); ownerSids,
readerSids,
writerSids));
return MapResultToken(result); return new
}); {
result.Success,
ResultToken = MapResultToken(result.ResultToken)
};
}, () =>
{
return CreateWhatIfResult(
"NTFS Folder Create",
"Wuerde einen Ordner anlegen und fehlende Gruppen sowie ACLs sicherstellen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
ProviderRootPath = provider.RootPath,
NewFolderPath = folderPath,
ParentFolderPath = parentPath,
OwnerSids = ownerSids,
ReaderSids = readerSids,
WriterSids = writerSids
});
});
}
catch (Exception ex)
{
AppendLog($"NTFS Folder Create fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "NTFS Folder Create", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void ExecuteNtfsEnsureButton_Click(object sender, RoutedEventArgs e) private async void ExecuteNtfsEnsureButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("NTFS Ensure Groups / ACLs", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS"); var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS");
var folderPath = GetRequiredText(NtfsEnsureFolderPathTextBox.Text, "Folder Path"); var folderPath = GetRequiredText(NtfsEnsureFolderPathTextBox.Text, "Folder Path");
var result = await provider.EnsureMissingPermissionGroupsAsync( var ownerSids = ParseIdentifierList(NtfsEnsureOwnerSidsTextBox.Text, "Owner SIDs");
folderPath, var readerSids = ParseIdentifierList(NtfsEnsureReaderSidsTextBox.Text, "Reader SIDs");
ParseKeyValueLines(CustomTagsTextBox.Text, "Custom Tags"), var writerSids = ParseIdentifierList(NtfsEnsureWriterSidsTextBox.Text, "Writer SIDs");
ParseIdentifierList(NtfsEnsureOwnerSidsTextBox.Text, "Owner SIDs"), var ensureTraverse = NtfsEnsureTraverseCheckBox.IsChecked ?? false;
ParseIdentifierList(NtfsEnsureReaderSidsTextBox.Text, "Reader SIDs"),
ParseIdentifierList(NtfsEnsureWriterSidsTextBox.Text, "Writer SIDs"),
NtfsEnsureTraverseCheckBox.IsChecked ?? false);
return MapResultToken(EnsureSuccessfulResultToken(result, "NTFS Ensure Groups / ACLs")); await ExecuteProviderActionAsync("NTFS Ensure Groups / ACLs", async () =>
}); {
var result = await Task.Run(() => LiamWorkflowRuntime.EnsureNtfsPermissionGroupsAsync(
provider,
folderPath,
null,
ownerSids,
readerSids,
writerSids,
ensureTraverse));
return new
{
result.Success,
ResultToken = MapResultToken(result.ResultToken)
};
}, () =>
{
return CreateWhatIfResult(
"NTFS Ensure Groups / ACLs",
"Wuerde fehlende NTFS-Berechtigungsgruppen und ACLs additiv sicherstellen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
ProviderRootPath = provider.RootPath,
FolderPath = folderPath,
OwnerSids = ownerSids,
ReaderSids = readerSids,
WriterSids = writerSids,
EnsureTraverseGroups = ensureTraverse
});
});
}
catch (Exception ex)
{
AppendLog($"NTFS Ensure Groups / ACLs fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "NTFS Ensure Groups / ACLs", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void ExecuteAdCreateButton_Click(object sender, RoutedEventArgs e) private async void ExecuteAdCreateButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("AD Ensure Service Groups", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderAD>("Active Directory"); var provider = EnsureInitializedProvider<cLiamProviderAD>("Active Directory");
var serviceName = GetRequiredText(AdServiceNameTextBox.Text, "Service Name"); var serviceName = GetRequiredText(AdServiceNameTextBox.Text, "Service Name");
@@ -642,84 +720,171 @@ namespace LiamWorkflowDiagnostics
var ownerSids = ParseIdentifierList(AdOwnerSidsTextBox.Text, "Owner SIDs"); var ownerSids = ParseIdentifierList(AdOwnerSidsTextBox.Text, "Owner SIDs");
var memberSids = ParseIdentifierList(AdMemberSidsTextBox.Text, "Member SIDs"); var memberSids = ParseIdentifierList(AdMemberSidsTextBox.Text, "Member SIDs");
var result = await Task.Run(() => provider.CreateServiceGroups( await ExecuteProviderActionAsync("AD Ensure Service Groups", async () =>
serviceName, {
description, var result = await Task.Run(() => LiamWorkflowRuntime.CreateAdServiceGroups(
scope, provider,
groupType, serviceName,
ownerSids, description,
memberSids)); scope,
groupType,
ownerSids,
memberSids));
return MapSecurityGroupResults(result); return result;
}); }, () =>
{
return CreateWhatIfResult(
"AD Ensure Service Groups",
"Wuerde Service-Gruppen im Active Directory anhand der Namenskonvention sicherstellen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
ServiceName = serviceName,
Description = description,
Scope = scope.ToString(),
GroupType = groupType.ToString(),
OwnerSids = ownerSids,
MemberSids = memberSids
});
});
}
catch (Exception ex)
{
AppendLog($"AD Ensure Service Groups fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "AD Ensure Service Groups", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void ExecuteMsTeamsCloneButton_Click(object sender, RoutedEventArgs e) private async void ExecuteMsTeamsCloneButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("MsTeams Clone Team", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderMsTeams>("MsTeams"); var provider = EnsureInitializedProvider<cLiamProviderMsTeams>("MsTeams");
var sourceTeamId = GetRequiredText(MsTeamsSourceTeamIdTextBox.Text, "Source Team ID"); var sourceTeamId = GetRequiredText(MsTeamsSourceTeamIdTextBox.Text, "Source Team ID");
var newTeamName = GetRequiredText(MsTeamsNewNameTextBox.Text, "New Team Name"); var newTeamName = GetRequiredText(MsTeamsNewNameTextBox.Text, "New Team Name");
var description = NormalizeOptionalText(MsTeamsDescriptionTextBox.Text);
var visibility = GetSelectedMsTeamsVisibility(); var visibility = GetSelectedMsTeamsVisibility();
var partsToClone = GetSelectedCloneParts();
var additionalMembers = ParseIdentifierList(MsTeamsAdditionalMembersTextBox.Text, "Additional Members");
var additionalOwners = ParseIdentifierList(MsTeamsAdditionalOwnersTextBox.Text, "Additional Owners");
var result = await provider.cloneTeam( await ExecuteProviderActionAsync("MsTeams Clone Team", async () =>
sourceTeamId, {
newTeamName, var result = await Task.Run(() => LiamWorkflowRuntime.CloneTeamAsync(
NormalizeOptionalText(MsTeamsDescriptionTextBox.Text), provider,
visibility, sourceTeamId,
GetSelectedCloneParts(), newTeamName,
string.Join(";", ParseIdentifierList(MsTeamsAdditionalMembersTextBox.Text, "Additional Members")), description,
string.Join(";", ParseIdentifierList(MsTeamsAdditionalOwnersTextBox.Text, "Additional Owners"))); visibility,
partsToClone,
string.Join(";", additionalMembers),
string.Join(";", additionalOwners)));
return MapMsGraphResult(result); return result;
}); }, () =>
{
return CreateWhatIfResult(
"MsTeams Clone Team",
"Wuerde ein Team anhand der gewaehlten Clone-Bestandteile klonen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
SourceTeamId = sourceTeamId,
NewTeamName = newTeamName,
Description = description,
Visibility = visibility,
PartsToClone = partsToClone,
AdditionalMembers = additionalMembers,
AdditionalOwners = additionalOwners
});
});
}
catch (Exception ex)
{
AppendLog($"MsTeams Clone Team fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "MsTeams Clone Team", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void ExecuteExchangeMailboxButton_Click(object sender, RoutedEventArgs e) private async void ExecuteExchangeMailboxButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("Exchange Create Shared Mailbox", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange"); var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange");
var name = GetRequiredText(ExchangeMailboxNameTextBox.Text, "Name"); var name = GetRequiredText(ExchangeMailboxNameTextBox.Text, "Name");
var alias = GetRequiredText(ExchangeMailboxAliasTextBox.Text, "Alias"); var alias = GetRequiredText(ExchangeMailboxAliasTextBox.Text, "Alias");
var displayName = NormalizeOptionalText(ExchangeMailboxDisplayNameTextBox.Text); var displayName = NormalizeOptionalText(ExchangeMailboxDisplayNameTextBox.Text);
var primarySmtp = NormalizeOptionalText(ExchangeMailboxPrimarySmtpTextBox.Text); var primarySmtp = NormalizeOptionalText(ExchangeMailboxPrimarySmtpTextBox.Text);
var result = await Task.Run(() => provider.exchangeManager.CreateSharedMailboxWithOwnershipGroups(
name,
alias,
displayName,
primarySmtp));
return new await ExecuteProviderActionAsync("Exchange Create Shared Mailbox", async () =>
{ {
ObjectGuid = result.Item1, var result = await Task.Run(() => LiamWorkflowRuntime.CreateSharedMailbox(
Groups = MapSecurityGroupResults(result.Item2) provider,
}; name,
}); alias,
displayName,
primarySmtp));
return result;
}, () =>
{
return CreateWhatIfResult(
"Exchange Create Shared Mailbox",
"Wuerde eine Shared Mailbox inklusive Ownership-Gruppen erzeugen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
Name = name,
Alias = alias,
DisplayName = displayName,
PrimarySmtpAddress = primarySmtp
});
});
}
catch (Exception ex)
{
AppendLog($"Exchange Create Shared Mailbox fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "Exchange Create Shared Mailbox", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void ExecuteExchangeDistributionButton_Click(object sender, RoutedEventArgs e) private async void ExecuteExchangeDistributionButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteProviderActionAsync("Exchange Create Distribution Group", async () => try
{ {
var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange"); var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange");
var name = GetRequiredText(ExchangeDistributionNameTextBox.Text, "Name"); var name = GetRequiredText(ExchangeDistributionNameTextBox.Text, "Name");
var alias = GetRequiredText(ExchangeDistributionAliasTextBox.Text, "Alias"); var alias = GetRequiredText(ExchangeDistributionAliasTextBox.Text, "Alias");
var displayName = NormalizeOptionalText(ExchangeDistributionDisplayNameTextBox.Text); var displayName = NormalizeOptionalText(ExchangeDistributionDisplayNameTextBox.Text);
var primarySmtp = NormalizeOptionalText(ExchangeDistributionPrimarySmtpTextBox.Text); var primarySmtp = NormalizeOptionalText(ExchangeDistributionPrimarySmtpTextBox.Text);
var result = await Task.Run(() => provider.exchangeManager.CreateDistributionGroupWithOwnershipGroups(
name,
alias,
displayName,
primarySmtp));
return new await ExecuteProviderActionAsync("Exchange Create Distribution Group", async () =>
{ {
ObjectGuid = result.Item1, var result = await Task.Run(() => LiamWorkflowRuntime.CreateDistributionGroup(
Groups = MapSecurityGroupResults(result.Item2) provider,
}; name,
}); alias,
displayName,
primarySmtp));
return result;
}, () =>
{
return CreateWhatIfResult(
"Exchange Create Distribution Group",
"Wuerde eine Distribution Group inklusive Ownership-Gruppen erzeugen. Es wurden keine Aenderungen ausgefuehrt.",
new
{
Name = name,
Alias = alias,
DisplayName = displayName,
PrimarySmtpAddress = primarySmtp
});
});
}
catch (Exception ex)
{
AppendLog($"Exchange Create Distribution Group fehlgeschlagen: {ex.Message}", LogLevels.Error);
MessageBox.Show(this, ex.ToString(), "Exchange Create Distribution Group", MessageBoxButton.OK, MessageBoxImage.Error);
}
} }
private async void FetchSecurityGroupsButton_Click(object sender, RoutedEventArgs e) private async void FetchSecurityGroupsButton_Click(object sender, RoutedEventArgs e)
@@ -732,38 +897,25 @@ namespace LiamWorkflowDiagnostics
try try
{ {
AppendLog($"Lese SecurityGroups (Filter='{_session.Provider.GroupFilter}') ..."); var provider = _session.Provider;
var groups = await _session.Provider.getSecurityGroupsAsync(_session.Provider.GroupFilter); var filter = provider.GroupFilter;
if (groups == null) AppendLog($"Lese SecurityGroups (Filter='{filter}') ...");
{ var result = await Task.Run(() => LiamWorkflowRuntime.GetSecurityGroupsFromProviderAsync(provider));
var providerMessage = _session.Provider.GetLastErrorMessage(); ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
if (_session.Provider is cLiamProviderExchange exchangeProvider)
{
var code = exchangeProvider.GetLastErrorCode();
if (string.IsNullOrWhiteSpace(code))
code = "EXCH_GET_SECURITYGROUPS_FAILED";
AppendLog($"SecurityGroups-Call fehlgeschlagen [{code}]: {providerMessage}", LogLevels.Error);
}
else
{
AppendLog($"SecurityGroups-Call fehlgeschlagen: {providerMessage}", LogLevels.Error);
}
ResultTextBox.Text = "[]"; if (!result.Success)
{
AppendLog($"SecurityGroups-Call fehlgeschlagen [{result.ErrorCode}]: {result.ErrorMessage}", LogLevels.Error);
return; return;
} }
if (groups.Count == 0) if (result.SecurityGroups.Count == 0)
{ {
AppendLog("Keine SecurityGroups gefunden.", LogLevels.Warning); AppendLog("Keine SecurityGroups gefunden.", LogLevels.Warning);
ResultTextBox.Text = "[]";
return; return;
} }
var entries = ConvertSecurityGroups(groups); AppendLog($"SecurityGroups erhalten: {result.SecurityGroups.Count}");
var json = JsonConvert.SerializeObject(entries, Formatting.Indented);
ResultTextBox.Text = json;
AppendLog($"SecurityGroups erhalten: {entries.Count}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -798,7 +950,7 @@ namespace LiamWorkflowDiagnostics
} }
} }
private async Task ExecuteProviderActionAsync(string actionName, Func<Task<object>> action) private async Task ExecuteProviderActionAsync(string actionName, Func<Task<object>> action, Func<object> whatIfAction = null)
{ {
if (action == null) if (action == null)
throw new ArgumentNullException(nameof(action)); throw new ArgumentNullException(nameof(action));
@@ -807,10 +959,20 @@ namespace LiamWorkflowDiagnostics
try try
{ {
SaveSettings(); SaveSettings();
AppendLog($"{actionName} gestartet."); var runInWhatIfMode = IsWhatIfEnabled && whatIfAction != null;
var result = await action(); AppendLog(runInWhatIfMode
? $"{actionName} im WhatIf-Modus gestartet. Schreibende Aenderungen werden nur simuliert."
: $"{actionName} gestartet.");
var result = runInWhatIfMode
? await Task.Run(whatIfAction)
: await action();
ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented); ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
AppendLog($"{actionName} erfolgreich abgeschlossen."); if (TryGetSuccessFlag(result, out var success) && !success)
AppendLog($"{actionName} mit Fehlerstatus abgeschlossen.", LogLevels.Warning);
else if (runInWhatIfMode)
AppendLog($"{actionName} im WhatIf-Modus simuliert abgeschlossen.", LogLevels.Warning);
else
AppendLog($"{actionName} erfolgreich abgeschlossen.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -823,6 +985,39 @@ namespace LiamWorkflowDiagnostics
} }
} }
private bool TryGetSuccessFlag(object instance, out bool success)
{
success = false;
if (instance == null)
return false;
var property = instance.GetType().GetProperty("Success", BindingFlags.Instance | BindingFlags.Public);
if (property == null || property.PropertyType != typeof(bool))
return false;
var rawValue = property.GetValue(instance);
if (!(rawValue is bool boolValue))
return false;
success = boolValue;
return true;
}
private bool IsWhatIfEnabled => WhatIfCheckBox.IsChecked ?? true;
private object CreateWhatIfResult(string actionName, string message, object input)
{
return new
{
Success = true,
WhatIf = true,
Operation = actionName,
Message = message ?? string.Empty,
TimestampUtc = DateTime.UtcNow.ToString("s") + "Z",
Input = input
};
}
private TProvider EnsureInitializedProvider<TProvider>(string providerName) where TProvider : cLiamProviderBase private TProvider EnsureInitializedProvider<TProvider>(string providerName) where TProvider : cLiamProviderBase
{ {
if (_session?.Provider == null) if (_session?.Provider == null)
@@ -914,51 +1109,6 @@ namespace LiamWorkflowDiagnostics
}; };
} }
private ResultToken EnsureSuccessfulResultToken(ResultToken token, string actionName)
{
if (token == null)
throw new InvalidOperationException($"{actionName}: kein Ergebnis vom Provider erhalten.");
if (token.resultErrorId != 0)
{
var message = string.IsNullOrWhiteSpace(token.resultMessage)
? "Unbekannter Fehler im Provider."
: token.resultMessage.Trim();
throw new InvalidOperationException($"[{token.resultErrorId}] {message}");
}
return token;
}
private List<object> MapSecurityGroupResults(IEnumerable<Tuple<string, string, string, string>> groups)
{
return (groups ?? Enumerable.Empty<Tuple<string, string, string, string>>())
.Select(i => (object)new
{
Role = i.Item1 ?? string.Empty,
Sid = i.Item2 ?? string.Empty,
Name = i.Item3 ?? string.Empty,
DistinguishedName = i.Item4 ?? string.Empty
})
.ToList();
}
private object MapMsGraphResult(object result)
{
if (result == null)
return null;
var resultType = result.GetType();
return new
{
Id = ReadPropertyValue<string>(result, resultType, "ID"),
DisplayName = ReadPropertyValue<string>(result, resultType, "DisplayName"),
ODataId = ReadPropertyValue<string>(result, resultType, "ODataId"),
Context = ReadPropertyValue<string>(result, resultType, "Context"),
Result = ReadPropertyValue<object>(result, resultType, "Result")
};
}
private int GetSelectedMsTeamsVisibility() private int GetSelectedMsTeamsVisibility()
{ {
var selectedValue = MsTeamsVisibilityComboBox.SelectedValue; var selectedValue = MsTeamsVisibilityComboBox.SelectedValue;
@@ -978,22 +1128,6 @@ namespace LiamWorkflowDiagnostics
|| value == MsTeamsVisibilityHiddenMembership; || value == MsTeamsVisibilityHiddenMembership;
} }
private T ReadPropertyValue<T>(object instance, Type instanceType, string propertyName)
{
var property = instanceType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
if (property == null)
return default(T);
var value = property.GetValue(instance);
if (value == null)
return default(T);
if (value is T typedValue)
return typedValue;
return default(T);
}
private cLiamProviderData ParseProviderDataFromInput(string input) private cLiamProviderData ParseProviderDataFromInput(string input)
{ {
if (string.IsNullOrWhiteSpace(input)) if (string.IsNullOrWhiteSpace(input))
@@ -1236,100 +1370,6 @@ namespace LiamWorkflowDiagnostics
} }
} }
private List<DataAreaEntry> ConvertDataAreas(IEnumerable<cLiamDataAreaBase> dataAreas)
{
var result = new List<DataAreaEntry>();
foreach (var dataArea in dataAreas ?? Enumerable.Empty<cLiamDataAreaBase>())
{
var ntfsPermissionArea = dataArea as cLiamNtfsPermissionDataAreaBase;
var ntfsFolder = dataArea as cLiamNtfsFolder;
var adGroup = dataArea as cLiamAdGroupAsDataArea;
var exchMailbox = dataArea as cLiamExchangeSharedMailbox;
var exchDistribution = dataArea as cLiamExchangeDistributionGroup;
var owner = exchMailbox?.OwnerGroupIdentifier
?? exchDistribution?.OwnerGroupIdentifier
?? adGroup?.ManagedBySID
?? ntfsPermissionArea?.OwnerGroupIdentifier
?? string.Empty;
var write = exchMailbox != null
? exchMailbox.FullAccessGroupSid
: exchDistribution != null
? exchDistribution.MemberGroupSid
: adGroup?.UID
?? ntfsPermissionArea?.WriteGroupIdentifier
?? string.Empty;
var read = exchMailbox != null
? exchMailbox.SendAsGroupSid
: ntfsPermissionArea?.ReadGroupIdentifier
?? string.Empty;
var traverse = ntfsPermissionArea?.TraverseGroupIdentifier ?? string.Empty;
var created = ntfsFolder?.CreatedDate ?? string.Empty;
var description = adGroup?.Description ?? string.Empty;
result.Add(new DataAreaEntry
{
DisplayName = dataArea.DisplayName ?? string.Empty,
UID = dataArea.UID ?? string.Empty,
TechnicalName = dataArea.TechnicalName ?? string.Empty,
Description = description,
TargetType = ((int)dataArea.Provider.ProviderType).ToString(),
ParentUID = dataArea.ParentUID ?? string.Empty,
Level = dataArea.Level.ToString(),
Owner = owner,
Write = write,
Read = read,
Traverse = traverse,
CreatedDate = created,
ConfigurationId = !string.IsNullOrWhiteSpace(_session?.ProviderConfigObjectId)
? _session.ProviderConfigObjectId
: (!string.IsNullOrWhiteSpace(_session?.ProviderConfigId) ? _session.ProviderConfigId : string.Empty),
BaseFolder = ntfsFolder?.Share?.TechnicalName ?? dataArea.Provider?.RootPath ?? string.Empty,
UniqueId = dataArea.UID ?? string.Empty,
DataAreaType = ((int)dataArea.DataType).ToString()
});
}
return result;
}
private List<SecurityGroupEntry> ConvertSecurityGroups(IEnumerable<cLiamDataAreaBase> groups)
{
var result = new List<SecurityGroupEntry>();
foreach (var sg in groups ?? Enumerable.Empty<cLiamDataAreaBase>())
{
var entry = new SecurityGroupEntry
{
DisplayName = sg.TechnicalName ?? sg.DisplayName ?? string.Empty,
TechnicalName = sg.UID ?? string.Empty,
TargetType = ((int)sg.Provider.ProviderType).ToString()
};
switch (sg)
{
case cLiamAdGroup adGroup:
entry.UID = adGroup.dn;
entry.Scope = adGroup.scope;
break;
case cLiamAdGroup2 adGroup2:
entry.UID = adGroup2.dn;
entry.Scope = adGroup2.scope;
break;
case cLiamExchangeSecurityGroup exchangeGroup:
entry.UID = exchangeGroup.dn;
break;
default:
entry.UID = sg.UID;
break;
}
result.Add(entry);
}
return result;
}
private void PopulateFields(cLiamProviderData data) private void PopulateFields(cLiamProviderData data)
{ {
if (data == null) if (data == null)
@@ -1489,6 +1529,7 @@ namespace LiamWorkflowDiagnostics
public string ServerName { get; set; } = string.Empty; public string ServerName { get; set; } = string.Empty;
public bool UseHttps { get; set; } = false; public bool UseHttps { get; set; } = false;
public string License { get; set; } = string.Empty; public string License { get; set; } = string.Empty;
public bool EnableWhatIf { get; set; } = true;
public string NtfsCreateFolderPath { get; set; } = string.Empty; public string NtfsCreateFolderPath { get; set; } = string.Empty;
public string NtfsCreateParentPath { get; set; } = string.Empty; public string NtfsCreateParentPath { get; set; } = string.Empty;
public string NtfsCreateOwnerSids { get; set; } = string.Empty; public string NtfsCreateOwnerSids { get; set; } = string.Empty;

View File

@@ -0,0 +1,172 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$Path,
[Parameter()]
[string]$ExportJsonPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ([System.Environment]::OSVersion.Platform -ne [System.PlatformID]::Win32NT) {
throw 'Dieses Demo-Script kann nur unter Windows ausgefuehrt werden.'
}
$nativeMethods = @"
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public static class NtfsNativeMethods
{
[StructLayout(LayoutKind.Sequential)]
public struct BY_HANDLE_FILE_INFORMATION
{
public uint FileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
public uint VolumeSerialNumber;
public uint FileSizeHigh;
public uint FileSizeLow;
public uint NumberOfLinks;
public uint FileIndexHigh;
public uint FileIndexLow;
}
public const uint FILE_SHARE_READ = 0x00000001;
public const uint FILE_SHARE_WRITE = 0x00000002;
public const uint FILE_SHARE_DELETE = 0x00000004;
public const uint OPEN_EXISTING = 3;
public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetFileInformationByHandle(
SafeFileHandle hFile,
out BY_HANDLE_FILE_INFORMATION lpFileInformation);
}
"@
if (-not ('NtfsNativeMethods' -as [type])) {
Add-Type -TypeDefinition $nativeMethods
}
function Get-Md5Hex {
param(
[Parameter(Mandatory = $true)]
[string]$Text
)
$md5 = [System.Security.Cryptography.MD5]::Create()
try {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
$hash = $md5.ComputeHash($bytes)
return ([System.BitConverter]::ToString($hash)).Replace('-', '').ToLowerInvariant()
}
finally {
$md5.Dispose()
}
}
function Get-NtfsStableFolderId {
param(
[Parameter(Mandatory = $true)]
[string]$LiteralPath
)
$item = Get-Item -LiteralPath $LiteralPath -Force
if (-not $item.PSIsContainer) {
throw "Der Pfad '$LiteralPath' ist kein Ordner."
}
$handle = [NtfsNativeMethods]::CreateFile(
$item.FullName,
0,
[NtfsNativeMethods]::FILE_SHARE_READ -bor [NtfsNativeMethods]::FILE_SHARE_WRITE -bor [NtfsNativeMethods]::FILE_SHARE_DELETE,
[IntPtr]::Zero,
[NtfsNativeMethods]::OPEN_EXISTING,
[NtfsNativeMethods]::FILE_FLAG_BACKUP_SEMANTICS,
[IntPtr]::Zero)
if ($handle.IsInvalid) {
$lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
throw "CreateFile fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError."
}
try {
$info = New-Object NtfsNativeMethods+BY_HANDLE_FILE_INFORMATION
if (-not [NtfsNativeMethods]::GetFileInformationByHandle($handle, [ref]$info)) {
$lastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
throw "GetFileInformationByHandle fehlgeschlagen fuer '$($item.FullName)' mit Win32-Fehler $lastError."
}
$fileIndex = ([UInt64]$info.FileIndexHigh -shl 32) -bor [UInt64]$info.FileIndexLow
$stableAnchor = ('{0:x8}:{1:x16}' -f $info.VolumeSerialNumber, $fileIndex)
[pscustomobject]@{
Path = $item.FullName
Name = $item.Name
VolumeSerialNumberHex = ('0x{0:x8}' -f $info.VolumeSerialNumber)
FileIndexHex = ('0x{0:x16}' -f $fileIndex)
StableAnchor = $stableAnchor
StableDataAreaId = (Get-Md5Hex -Text $stableAnchor)
CreatedUtc = $item.CreationTimeUtc.ToString('o')
LastWriteUtc = $item.LastWriteTimeUtc.ToString('o')
}
}
finally {
$handle.Dispose()
}
}
$results = foreach ($currentPath in $Path) {
Get-NtfsStableFolderId -LiteralPath $currentPath
}
if ($PSBoundParameters.ContainsKey('ExportJsonPath')) {
$exportDirectory = Split-Path -Parent $ExportJsonPath
if (-not [string]::IsNullOrWhiteSpace($exportDirectory) -and -not (Test-Path -LiteralPath $exportDirectory)) {
New-Item -ItemType Directory -Path $exportDirectory | Out-Null
}
$results | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $ExportJsonPath -Encoding UTF8
}
$results
<#
Demo fuer Rename/Move auf demselben NTFS-Volume:
1. Vorher ausfuehren:
.\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\TeamA' -ExportJsonPath '.\before.json'
2. Ordner umbenennen oder innerhalb desselben Volumes verschieben.
3. Nachher erneut ausfuehren:
.\Get-NtfsStableFolderId.ps1 -Path 'C:\Daten\Fachbereich\TeamA_Renamed' -ExportJsonPath '.\after.json'
4. Vergleichen:
- `Path` ist anders
- `StableAnchor` bleibt gleich
- `StableDataAreaId` bleibt gleich
Wichtige Einschraenkung:
- Rename und Move auf demselben NTFS-Volume behalten die Identitaet.
- Copy-and-delete oder Move ueber ein anderes Volume erzeugen in der Regel eine neue Identitaet.
- Auf manchen Remote-/DFS-Szenarien kann der Dateiserver diese Metadaten nicht verlaesslich liefern.
#>

View File

@@ -94,6 +94,15 @@ Betroffene Stelle:
- [DataArea_FileSystem.cs#L676](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L676) - [DataArea_FileSystem.cs#L676](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L676)
- [DataArea_FileSystem.cs#L678](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L678) - [DataArea_FileSystem.cs#L678](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L678)
Status:
- Am 2026-03-18 umgesetzt.
- Das harte `Thread.Sleep(180000)` wurde entfernt.
- Zunächst wurde der Wait auf die bloße Auflösbarkeit neu erzeugter Gruppen umgestellt. Nach fachlicher Rückmeldung wurde der Fix auf den tatsächlich kritischen Folgeschritt verschoben: die Membership-Änderung an der Traverse-Gruppe.
- Die Traverse-Logik retryt jetzt direkt `Members.Contains(...)`, `Members.Add(...)` und `Save()` mit sofortigem Erstversuch und kurzem Backoff.
- Die maximale Obergrenze bleibt bewusst bei 3 Minuten, damit das bisherige Sicherheitsfenster für langsame AD-Konsistenz erhalten bleibt.
- Im Normalfall endet die Wartezeit jetzt deutlich früher, sobald die Membership-Änderung erfolgreich durchläuft.
### 4. Hoch: Unterschiedliches SMB-Verhalten zwischen Lesen und Schreiben ### 4. Hoch: Unterschiedliches SMB-Verhalten zwischen Lesen und Schreiben
Der Provider verwendet für das Lesen des NTFS-Baums `cNtfsBase.LogonAsync()`. Dort wird der bekannte SMB-Fehler `1219` abgefangen, die bestehende Verbindung getrennt und anschließend ein neuer Versuch gestartet. Der Provider verwendet für das Lesen des NTFS-Baums `cNtfsBase.LogonAsync()`. Dort wird der bekannte SMB-Fehler `1219` abgefangen, die bestehende Verbindung getrennt und anschließend ein neuer Versuch gestartet.
@@ -172,7 +181,59 @@ Betroffene Stellen:
- [C4IT.LIAM.Ntfs.cs#L63](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L63) - [C4IT.LIAM.Ntfs.cs#L63](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L63)
- [C4IT.LIAM.Ntfs.cs#L88](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L88) - [C4IT.LIAM.Ntfs.cs#L88](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L88)
### 8. Mittel-Hoch: `LoadDataArea()` behandelt UNC-Pfade fachlich falsch ### 8. Hoch: AD-Gruppennamen werden nicht gegen Längenlimits abgesichert
Die Namensbildung für AD-Gruppen materialisiert Templates, Platzhalter und CustomTags direkt zu einem finalen Gruppennamen. Dieser Wert wird anschließend unverändert sowohl als `CN` als auch als `sAMAccountName` verwendet.
Aktuell gibt es davor keine echte Längenprüfung und keine Kürzungslogik. Der Code prüft nur, ob der Name bereits existiert, und erhöht bei Bedarf über den `LOOP`-Mechanismus die Eindeutigkeit. Gegen zu lange Namen schützt das aber nicht.
Dadurch entsteht ein reales Betriebsrisiko:
- tiefe Ordnerpfade oder lange Ordnernamen können AD-seitig unzulässige Gruppennamen erzeugen
- der Fehler tritt erst spät beim eigentlichen AD-Create auf
- das Verhalten ist nicht deterministisch vorbereitet, sondern von der AD-Rückmeldung abhängig
- auch eine spätere manuelle Korrektur ist unsauber, weil Naming und ACL-Zuordnung bereits auf dem ursprünglichen Namen basieren können
Betroffene Stellen:
- [SecurityGroup.cs#L192](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs#L192)
- [SecurityGroup.cs#L680](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/SecurityGroup.cs#L680)
- [DataArea_FileSystem.cs#L1186](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L1186)
Status:
- Am 2026-03-18 umgesetzt.
- Vor der AD-Gruppenerzeugung wird jetzt zentral eine konservative Namensgrenze von 64 Zeichen angewendet.
- Die Begrenzung reserviert zusätzlich Platz für einen möglichen `LOOP`-Suffix, damit spätere Kollisionsauflösung nicht wieder über das sichere Limit hinausschießt.
- Die Kürzung greift nur auf dem dynamischen Pfadanteil, nicht auf stabilen fachlichen Tags wie Prefix, Scope oder Gruppentyp.
- Die gemeinsame Logik wird sowohl im normalen Security-Group-Pfad als auch im Traverse-Pfad verwendet.
- Wenn gekürzt werden muss, wird das nur im Log protokolliert. Es wird kein fachlicher Fehler geworfen.
- Die Implementierung arbeitet bewusst ohne Hash-Fallback, damit die resultierenden Namen vollständig human-readable bleiben.
- Verifiziert mit `msbuild LiamNtfs/LiamNtfs.csproj /p:Configuration=Debug`. Das bestehende Warning `CS0162` in `SecurityGroup.cs` blieb unverändert.
Vorschlag zum Fixen:
- Vor dem AD-Write jeden final generierten Gruppennamen gegen eine zentrale, konservative Maximalgrenze prüfen, die für die tatsächlich beschriebenen AD-Attribute sicher eingehalten wird.
- Wenn ein Name innerhalb der Grenze liegt, unverändert weiterarbeiten.
- Wenn ein Name zu lang ist, keinen fachlichen Fehler werfen, sondern den Umstand nur im Log dokumentieren und anschließend automatisch auf einen kontrolliert verkürzten Namen umschalten.
- Die Verkürzung sollte deterministisch sein und die stabilen fachlichen Tags erhalten. Kürzen sollte möglichst nur der dynamische Pfadanteil (`NAME` bzw. `RELATIVEPATH`).
- Der bestehende `LOOP`-Mechanismus zur Kollisionsbehandlung sollte danach unverändert weiterlaufen, damit Längenbegrenzung und Eindeutigkeitslogik sauber getrennt bleiben.
Konkreter Vorschlag für die Verkürzungslogik:
- Den finalen AD-Gruppennamen nicht als Ganzes kürzen, sondern vor dem Zusammenbau logisch in `prefixPart`, `dynamicPart` und `suffixPart` aufteilen.
- `prefixPart` umfasst die stabilen fachlichen Tags wie Prefix, Scope-Tag und feste Typbestandteile. `suffixPart` umfasst Typ-/Scope-Enden und einen eventuell benötigten `LOOP`-Puffer. Beide Bereiche bleiben unverändert.
- Nur `dynamicPart` darf verkürzt werden. Dieser Teil stammt fachlich aus `NAME` oder `RELATIVEPATH`.
- Wenn `dynamicPart` aus `NAME` stammt, wird zuerst nur dieser Name gekürzt.
- Wenn `dynamicPart` aus `RELATIVEPATH` stammt, sollte segmentbasiert gekürzt werden. Das letzte Segment bleibt möglichst am längsten erhalten, weil es fachlich meist den eigentlichen Zielordner beschreibt.
- Erste Kürzungsstufe: frühe oder mittlere Segmente verkürzen, bevor das letzte Segment gekappt wird. Beispiel: `Abteilung_Standort_Projekt_Unterordner` wird eher zu `Abt_Sta_Proj_Unterordner` als zu `Abteilung_Standort_Projekt_Unter`.
- Zweite Kürzungsstufe: wenn die segmentbasierte Verkürzung noch nicht reicht, werden zuerst die frühen Segmente weiter reduziert und danach bei Bedarf ganze frühe Segmente entfernt, bevor das letzte Segment angetastet wird.
- Nur wenn kein frühes Segment mehr sinnvoll gekürzt oder entfernt werden kann, wird das letzte verbleibende Segment weiter reduziert.
- Vor der Kürzung muss bereits Reserve für einen möglichen `LOOP`-Suffix eingeplant werden, damit die spätere Kollisionsauflösung nicht erneut über die Maximalgrenze hinausschießt.
- Logging nur dann, wenn tatsächlich gekürzt wurde. Im Log sollten Originalname, Zielname, alte Länge, neue Länge und die verwendete Strategie (`truncate-name`, `truncate-relativepath`) stehen.
- Die gesamte Logik sollte in einer zentralen Hilfsmethode gekapselt werden, damit Create, Ensure, Traverse und Preview denselben Namensalgorithmus verwenden.
### 9. Mittel-Hoch: `LoadDataArea()` behandelt UNC-Pfade fachlich falsch
Die Einzel-Ladefunktion `LoadDataArea()` ist sichtbar unfertig. Das ist nicht nur ein Stilproblem, sondern erzeugt reales Fehlverhalten. Die Einzel-Ladefunktion `LoadDataArea()` ist sichtbar unfertig. Das ist nicht nur ein Stilproblem, sondern erzeugt reales Fehlverhalten.
@@ -192,7 +253,7 @@ Betroffene Stellen:
- [C4IT.LIAM.Ntfs.cs#L227](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L227) - [C4IT.LIAM.Ntfs.cs#L227](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L227)
- [C4IT.LIAM.Ntfs.cs#L236](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L236) - [C4IT.LIAM.Ntfs.cs#L236](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L236)
### 9. Mittel-Hoch: `Depth = 0` verliert die Root-DataArea ### 10. Mittel-Hoch: `Depth = 0` verliert die Root-DataArea
Der Provider baut in `getDataAreasAsync()` zunächst korrekt das Root-Objekt auf. Danach wird die Unterordnerliste über `ntfsBase.RequestFoldersListAsync()` geladen. Wenn `Depth == 0` ist, liefert `RequestFoldersListAsync()` aber `null` statt einer leeren Liste zurück. Der Provider baut in `getDataAreasAsync()` zunächst korrekt das Root-Objekt auf. Danach wird die Unterordnerliste über `ntfsBase.RequestFoldersListAsync()` geladen. Wenn `Depth == 0` ist, liefert `RequestFoldersListAsync()` aber `null` statt einer leeren Liste zurück.
@@ -207,7 +268,7 @@ Betroffene Stellen:
- [cNtfsBase.cs#L108](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L108) - [cNtfsBase.cs#L108](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L108)
- [cNtfsBase.cs#L114](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L114) - [cNtfsBase.cs#L114](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L114)
### 10. Mittel: AD-Gruppenabfragen können still unvollständig sein ### 11. Mittel: AD-Gruppenabfragen können still unvollständig sein
`cActiveDirectoryBase.privRequestSecurityGroupsListAsync()` baut die Gruppenliste aus LDAP-Ergebnissen auf. Dort wird jedoch `GroupPrincipal.FindByIdentity(...).GroupScope` ohne Null-Check verwendet. `cActiveDirectoryBase.privRequestSecurityGroupsListAsync()` baut die Gruppenliste aus LDAP-Ergebnissen auf. Dort wird jedoch `GroupPrincipal.FindByIdentity(...).GroupScope` ohne Null-Check verwendet.
@@ -221,7 +282,7 @@ Betroffene Stellen:
- [cActiveDirectoryBase.cs#L186](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L186) - [cActiveDirectoryBase.cs#L186](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L186)
- [cActiveDirectoryBase.cs#L192](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L192) - [cActiveDirectoryBase.cs#L192](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L192)
### 11. Mittel: Zentrale Fehlerschnittstelle des Providers deckt Schreibfehler nicht ab ### 12. Mittel: Zentrale Fehlerschnittstelle des Providers deckt Schreibfehler nicht ab
`GetLastErrorMessage()` sammelt nur Fehler aus `ntfsBase` und `activeDirectoryBase`. Die eigentlichen Fehler aus `DataArea_FileSystem` laufen aber über `ResultToken` und landen nicht in dieser zentralen Fehlerschnittstelle. `GetLastErrorMessage()` sammelt nur Fehler aus `ntfsBase` und `activeDirectoryBase`. Die eigentlichen Fehler aus `DataArea_FileSystem` laufen aber über `ResultToken` und landen nicht in dieser zentralen Fehlerschnittstelle.
@@ -237,7 +298,7 @@ Betroffene Stellen:
- [C4IT.LIAM.Ntfs.cs#L506](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L506) - [C4IT.LIAM.Ntfs.cs#L506](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L506)
- [DataArea_FileSystem.cs#L137](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L137) - [DataArea_FileSystem.cs#L137](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.cs#L137)
### 12. Mittel: ACL-Mapping ist stark von vollständigen Naming-Conventions abhängig und fällt auf Sentinel-Werte zurück ### 13. Mittel: ACL-Mapping ist stark von vollständigen Naming-Conventions abhängig und fällt auf Sentinel-Werte zurück
Die Permission-Auflösung in `ResolvePermissionGroupsAsync()` verwendet `.First(...)` für die Conventions von Owner, Write und Read. Fehlt eine passende Konvention, wirft der Code sofort eine Exception. Die Permission-Auflösung in `ResolvePermissionGroupsAsync()` verwendet `.First(...)` für die Conventions von Owner, Write und Read. Fehlt eine passende Konvention, wirft der Code sofort eine Exception.
@@ -253,7 +314,7 @@ Betroffene Stellen:
- [C4IT.LIAM.Ntfs.cs#L522](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L522) - [C4IT.LIAM.Ntfs.cs#L522](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L522)
- [C4IT.LIAM.Ntfs.cs#L611](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L611) - [C4IT.LIAM.Ntfs.cs#L611](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L611)
### 13. Mittel: Mitgliederauflösung bricht intern mit NullReference ab und wird dann nur noch als `null` sichtbar ### 14. Mittel: Mitgliederauflösung bricht intern mit NullReference ab und wird dann nur noch als `null` sichtbar
`GetMembersAsync()` ruft `privGetMembersAsync(sid).ToList()` auf, bevor geprüft wird, ob überhaupt ein Result vorliegt. Wenn die Gruppe nicht gefunden wird, kommt `null` zurück und `.ToList()` wirft intern. `GetMembersAsync()` ruft `privGetMembersAsync(sid).ToList()` auf, bevor geprüft wird, ob überhaupt ein Result vorliegt. Wenn die Gruppe nicht gefunden wird, kommt `null` zurück und `.ToList()` wirft intern.
@@ -269,7 +330,7 @@ Betroffene Stellen:
- [cActiveDirectoryBase.cs#L274](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L274) - [cActiveDirectoryBase.cs#L274](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L274)
- [cActiveDirectoryBase.cs#L299](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L299) - [cActiveDirectoryBase.cs#L299](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L299)
### 14. Mittel/Niedrig: Share-Objekte können keine Kinder nachladen ### 15. Mittel/Niedrig: Share-Objekte können keine Kinder nachladen
`cLiamNtfsShare.getFolders()` und `getChildrenAsync()` liefern aktuell immer nur leere Listen zurück. Das bedeutet, dass Share-Objekte zwar initial erzeugt werden können, ein späteres Nachladen über die Objektmethoden aber praktisch nicht funktioniert. `cLiamNtfsShare.getFolders()` und `getChildrenAsync()` liefern aktuell immer nur leere Listen zurück. Das bedeutet, dass Share-Objekte zwar initial erzeugt werden können, ein späteres Nachladen über die Objektmethoden aber praktisch nicht funktioniert.
@@ -280,7 +341,7 @@ Betroffene Stellen:
- [C4IT.LIAM.Ntfs.cs#L706](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L706) - [C4IT.LIAM.Ntfs.cs#L706](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L706)
- [C4IT.LIAM.Ntfs.cs#L727](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L727) - [C4IT.LIAM.Ntfs.cs#L727](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L727)
### 15. Niedrig, aber inkonsistent: Der alte GET-Pfad verhält sich anders als der aktuelle Providerpfad ### 16. Niedrig, aber inkonsistent: Der alte GET-Pfad verhält sich anders als der aktuelle Providerpfad
Im älteren `DataArea_FileSystem_GET`-Pfad wird die Root-Struktur anders aufgebaut als im aktuellen Providerpfad. Ebene 0 bekommt dort kein Root-ACL-Mapping, und das Verhalten unterscheidet sich insgesamt von der neueren Logik im eigentlichen Provider. Im älteren `DataArea_FileSystem_GET`-Pfad wird die Root-Struktur anders aufgebaut als im aktuellen Providerpfad. Ebene 0 bekommt dort kein Root-ACL-Mapping, und das Verhalten unterscheidet sich insgesamt von der neueren Logik im eigentlichen Provider.

View File

@@ -0,0 +1,396 @@
# LIAM NTFS / DFS Metadaten-Klassifikation - konkretes Umsetzungskonzept
## Ziel
Dieses Dokument beschreibt die konkrete Zielumsetzung fuer die robuste Klassifikation von UNC-Pfaden im LIAM-NTFS-Provider.
Rahmenbedingungen:
- keine Erweiterung der bestehenden Provider-Config
- der Code darf nicht voraussetzen, auf dem DFS- oder SMB-Server selbst zu laufen
- DFS-Strukturen koennen unterschiedlich aufgebaut sein
- es muss sauber unterschieden werden zwischen:
- klassischem SMB-Share
- DFS-Namespace-Root
- DFS-Link
- echtem Folder
Die bestehende Config liefert dafuer nur den Einstiegspunkt und die Zugriffsrechte:
- `RootPath`
- `Domain`
- `Credential`
Die eigentliche Typbestimmung muss daher zur Laufzeit ueber Metadaten des adressierten UNC-Pfads erfolgen.
## Kurzfazit
Die robuste Loesung ist:
1. UNC-Pfad normalisieren
2. DFS-Metadaten fuer den Pfad und relevante Praefixe abfragen
3. wenn kein DFS-Treffer vorliegt, SMB-Share-Metadaten abfragen
4. alles unterhalb einer erkannten fachlichen Grenze als Folder behandeln
Die aktuelle Segment-Heuristik wird damit ersetzt, nicht nur verbessert.
## Fachregeln
Es gelten folgende semantische Regeln:
- `\\server\\namespace` ist ein `DfsNamespaceRoot`, wenn der Pfad als DFS-Namespace-Root aufgeloest werden kann.
- `\\server\\namespace\\link` ist ein `DfsLink`, wenn der Pfad als DFS-Link aufgeloest werden kann.
- `\\server\\share` ist ein `ClassicShare`, wenn der Share auf dem Server publiziert ist und kein DFS-Treffer vorliegt.
- jeder Pfad unterhalb eines `DfsLink` ist ein `Folder`
- jeder Pfad unterhalb eines `ClassicShare` ist ein `Folder`
- jeder Pfad unterhalb eines `DfsNamespaceRoot`, aber oberhalb eines `DfsLink`, ist nur dann `Folder`, wenn er kein DFS-Link ist und fachlich wirklich Dateisystemstruktur repraesentiert; fuer den Normalfall sind direkte Kinder eines Namespace-Roots als DFS-Links zu erwarten
## Verwendbare Laufzeit-Metadaten
### 1. DFS-Metadaten
Primaere Quelle fuer DFS:
- Win32 DFS API, bevorzugt `NetDfsGetInfo`
Damit kann fuer einen UNC-Pfad geprueft werden:
- ist der Pfad ein DFS-Objekt?
- handelt es sich um Root oder Link?
- welche Targets sind hinterlegt?
Wichtig:
- diese Abfrage funktioniert remote gegen den adressierten Namespace
- sie benoetigt keine lokale Ausfuehrung auf dem DFS-Server
- sie ist fachlich deutlich verlaesslicher als `Directory.Exists(...)`
### 2. SMB-Share-Metadaten
Primaere Quelle fuer klassische Shares:
- `NetShareEnum`
- optional ergaenzend `NetShareGetInfo`
Das ist fuer LIAM bereits teilweise vorhanden:
- `cNetworkConnection.EnumNetShares(server)`
Damit kann geprueft werden:
- ob `\\server\\share` ein echter publizierter SMB-Share ist
- welche sichtbaren Disk-Shares auf dem Host existieren
### 3. Dateisystem-Metadaten
Nur fuer Folder:
- `Directory.Exists`
- `DirectoryInfo`
Diese Daten duerfen nicht fuer die Unterscheidung zwischen DFS und Share benutzt werden, sondern nur nachdem bereits eine fachliche Grenze erkannt wurde.
## Nicht ausreichende Datenquellen
Folgende Informationen reichen fuer die Typbestimmung nicht aus:
- Anzahl der UNC-Segmente
- `RootPath` alleine
- `Directory.Exists`
- ACLs
- Naming Conventions
- Custom Tags
- Group Strategy
Diese Daten koennen ergaenzend helfen, liefern aber keine robuste Aussage ueber DFS vs. SMB.
## Zielmodell
Internes Modell:
- `ServerRoot`
- `ClassicShare`
- `DfsNamespaceRoot`
- `DfsLink`
- `Folder`
- `Unknown`
Externe LIAM-Abbildung:
- `ClassicShare` -> `NtfsShare`
- `DfsNamespaceRoot` -> `DfsNamespaceRoot`
- `DfsLink` -> `NtfsShare`
- `Folder` -> `NtfsFolder`
Damit bleibt die fachliche Aussenwirkung stabil:
- DFS-Link bleibt nach aussen ein share-aehnliches Objekt
- DFS-Namespace-Root bleibt eigenstaendig sichtbar
## Konkreter Klassifikationsalgorithmus
### Schritt 1: Normalisierung
Jeder Eingabepfad wird zuerst vereinheitlicht:
- Slash zu Backslash
- doppelte Separatoren bereinigen
- UNC-Praefix sicherstellen
- Trailing Backslash entfernen
Beispiel:
- `\\SERVER\\share\\`
- `//server/share`
werden intern zu:
- `\\server\\share`
fuer Vergleiche kann case-insensitive gearbeitet werden.
### Schritt 2: DFS zuerst pruefen
Fuer den gesamten Pfad und seine relevanten Praefixe werden DFS-Metadaten geprueft.
Beispiele:
- bei `\\server\\namespace\\link\\folder`
- pruefe `\\server\\namespace`
- pruefe `\\server\\namespace\\link`
Entscheidungsregel:
- wenn der volle Pfad ein DFS-Namespace-Root ist -> `DfsNamespaceRoot`
- wenn der volle Pfad ein DFS-Link ist -> `DfsLink`
- wenn ein Praefix ein DFS-Link ist und der Gesamtpfad darunter liegt -> `Folder`
- wenn ein Praefix ein DFS-Namespace-Root ist, aber noch kein DFS-Link erkannt wurde, wird weiter geprueft
Wichtig:
- der tiefste erkannte DFS-Link gewinnt als fachliche Grenze
- die Share-Grenze ist dann genau dieser Link-Pfad
### Schritt 3: SMB-Share pruefen
Nur wenn kein DFS-Ergebnis vorliegt:
- ermittle sichtbare Shares des Servers per `NetShareEnum`
- pruefe, ob `\\server\\share` exakt ein publizierter Share ist
Entscheidungsregel:
- wenn der volle Pfad exakt einem Share entspricht -> `ClassicShare`
- wenn ein Praefix exakt einem Share entspricht und der Gesamtpfad darunter liegt -> `Folder`
### Schritt 4: Folder nur relativ zu einer erkannten Grenze
Ein Pfad ist nur dann sicher `Folder`, wenn bereits eine fachliche Grenze erkannt wurde:
- klassischer Share
- DFS-Link
Beispiele:
- `\\server\\share\\dept` -> `Folder`, wenn `\\server\\share` ein ClassicShare ist
- `\\server\\namespace\\link\\dept` -> `Folder`, wenn `\\server\\namespace\\link` ein DfsLink ist
### Schritt 5: Unknown statt falscher Sicherheit
Wenn weder DFS noch SMB sicher bestimmt werden koennen:
- liefere intern `Unknown`
- logge die Ursache
- triff keine stille Segment-basierten Fallback-Entscheidung
## Erforderliche Runtime-Abfragen
### DFSResolver
Neue interne Hilfskomponente:
- `TryGetDfsMetadata(path, out metadata)`
Rueckgabedaten:
- `Exists`
- `IsNamespaceRoot`
- `IsLink`
- `Path`
- `EntryPath`
- `Targets`
Empfohlene Implementierung:
- P/Invoke gegen `NetDfsGetInfo`
Sinnvolle Aufrufmuster:
- pruefe `\\server\\namespace`
- pruefe `\\server\\namespace\\link`
- cache Ergebnisse je Pfad
### ShareResolver
Bestehende Hilfskomponente weiterverwenden:
- `EnumNetShares(server)`
Ergaenzung:
- optional Hilfsmethode `IsPublishedShare(server, shareName)`
- Cache je Server
## Neue zentrale Datenstruktur
Empfohlene interne Rueckgabe:
```text
PathClassification
- NormalizedPath
- Kind
- BoundaryPath
- ParentBoundaryPath
- BackingType
- ResolvedFrom
- Diagnostics
```
Dabei bedeutet:
- `BoundaryPath`: fachliche Share-Grenze
- `ParentBoundaryPath`: Parent-Objekt fuer LIAM-ParentUID
- `BackingType`: `DFS` oder `SMB`
- `ResolvedFrom`: z. B. `DfsLinkPrefix`, `SharePrefix`, `FullPathDfsRoot`
## Verhalten fuer typische Faelle
### Fall A
Pfad:
- `\\SRVWSM001.imagoverum.com\\file_shares`
Erwartung:
- wenn DFS-Metadaten Root bestaetigen -> `DfsNamespaceRoot`
### Fall B
Pfad:
- `\\SRVWSM001.imagoverum.com\\file_shares\\share2`
Erwartung:
- wenn DFS-Metadaten Link bestaetigen -> intern `DfsLink`, extern `NtfsShare`
### Fall C
Pfad:
- `\\SRVWSM001.imagoverum.com\\file_shares\\share2\\test33`
Erwartung:
- wenn `\\...\\file_shares\\share2` als DFS-Link erkannt wurde -> `Folder`
## Anpassung von `getDataAreasAsync()`
Die Enumeration darf nicht mehr nur mit `RequestFoldersListAsync(RootPath, depth)` arbeiten.
Stattdessen:
- Root zuerst klassifizieren
- Kinder je nach Root-Typ enumerieren
Regeln:
- `ClassicShare`
- Kinder per Dateisystem-Verzeichnisliste
- `DfsNamespaceRoot`
- direkte Kinder primaer ueber DFS-Namespace-Inhaltsmetadaten
- nur diese direkten Kinder sind fachlich Share-aehnliche Eintraege
- `DfsLink`
- Kinder per Dateisystem-Verzeichnisliste unterhalb des Link-Ziels bzw. unter dem UNC-Linkpfad
- `Folder`
- Kinder per Dateisystem-Verzeichnisliste
Der aktuelle Fehler entsteht gerade dadurch, dass direkte Kinder eines DFS-Namespace-Roots wie normale Ordner enumeriert und spaeter wieder als Folder behandelt werden.
## Anpassung von `LoadDataArea()`
`LoadDataArea()` muss dieselbe zentrale Klassifikation benutzen wie `getDataAreasAsync()`.
Wichtig:
- keine eigene Kurzlogik
- keine Typentscheidung ueber Segmentzahl
- gleiche `PathClassification` fuer denselben Pfad wie im Listenpfad
## Logging
Fuer Diagnosefaelle sollten folgende Logeintraege vorhanden sein:
- welcher Pfad wird klassifiziert
- welcher DFS-Check wurde ausgefuehrt
- welcher Share-Check wurde ausgefuehrt
- welcher Praefix als Boundary erkannt wurde
- warum ein Pfad `Unknown` wurde
Beispiel:
- `Path '\\server\\namespace\\link\\team' classified as Folder via DFS link boundary '\\server\\namespace\\link'`
## Fehlerverhalten
Wenn DFS-Abfragen fehlschlagen:
- Fehler loggen
- weiter mit SMB-Pruefung
Wenn SMB-Abfragen fehlschlagen:
- Fehler loggen
- nicht automatisch Folder annehmen
Wenn beides fehlschlaegt:
- `Unknown`
Damit wird falsche Typvergabe vermieden.
## Performance
Noetige Caches:
- DFS-Metadaten-Cache pro Pfad
- Share-Liste pro Server
Empfehlung:
- innerhalb eines Provider-Laufs cachen
- keine globale Langzeitpersistenz
## Konkrete Implementierungsschritte
1. neue interne Resolver-Komponente fuer DFS-Metadaten einfuehren
2. bestehende Share-Abfrage in dedizierte Share-Resolver-Methode kapseln
3. `ClassifyPath()` auf Metadaten-basierte Entscheidung umbauen
4. `getDataAreasAsync()` root-typabhaengig enumerieren
5. `LoadDataArea()` auf dieselbe Klassifikation umstellen
6. JSON-Rueckgabe unveraendert lassen:
- `DfsNamespaceRoot` als eigener `DataAreaType`
- `DfsLink` nach aussen als `NtfsShare`
- `Folder` als `NtfsFolder`
## Entscheidung
Die empfohlene Umsetzung ohne Config-Erweiterung ist:
- `RootPath` aus der vorhandenen Config nur als Einstieg verwenden
- DFS-Metadaten zur Laufzeit ueber API abfragen
- SMB-Share-Metadaten zur Laufzeit ueber `NetShareEnum` abfragen
- Folder ausschliesslich relativ zu einer erkannten fachlichen Grenze ableiten
Nur so koennen klassische Shares, DFS-Namespaces, DFS-Links und echte Folder belastbar unterschieden werden, auch wenn LIAM nicht auf dem Zielserver selbst laeuft.

View File

@@ -0,0 +1,421 @@
# LIAM NTFS Massenverarbeitung / AD-Gruppenanlage - konkretes Konzept
## Ziel
Dieses Dokument beschreibt ein konkretes Zielkonzept fuer die Verarbeitung grosser NTFS-Strukturen mit vielen Ordnern und automatischer AD-Gruppenanlage.
Beispielgroessenordnung:
- 1000 Ordner
- pro Ordner 3 bis 6 Berechtigungsgruppen
- optional Traverse-Gruppen
Ziel ist eine fachlich korrekte, aber vor allem skalierbare Verarbeitung ohne unkontrolliert lange Laufzeiten.
## Problem des aktuellen Zustands
Der aktuelle Code ist funktional auf einzelne Ordner oder kleine Mengen ausgerichtet.
Bei grossen Strukturen ergeben sich mehrere Engpaesse:
- Verarbeitung erfolgt pro Ordner sequentiell
- pro Ordner werden Gruppen einzeln aufgeloest oder angelegt
- bestehende Gruppen werden teilweise ueber OU-weite Wildcard-Suche gesucht
- ACLs werden pro Ordner direkt gesetzt
- Traverse-Verarbeitung enthaelt eine harte Wartezeit
Damit ist die aktuelle Implementierung fuer Massenlaeufe fachlich nutzbar, aber technisch nicht belastbar.
## Ist-Zustand
### 1. Automatischer Ensure-Pfad bei `Get DataAreas`
Wenn `EnsureNtfsPermissionGroups=true` gesetzt ist, wird fuer jede erkannte `cLiamNtfsFolder` nacheinander `EnsureMissingPermissionGroupsAsync(...)` aufgerufen.
Das bedeutet:
- jeder Ordner wird isoliert behandelt
- keine gemeinsame Voranalyse
- kein Caching ueber den gesamten Lauf
### 2. Create-/Ensure-Pfad
Pro Ordner passiert derzeit im Wesentlichen:
1. Soll-Gruppen generieren
2. Benutzer-SIDs aufloesen
3. pro Gruppe in AD suchen
4. fehlende Gruppe anlegen
5. Mitglieder setzen
6. ACLs setzen
7. optional Traverse-Gruppen verarbeiten
### 3. Teure Stellen
Die teuersten Einzelschritte sind aktuell:
- OU-weite Wildcard-Suche in AD
- mehrfaches Erzeugen und Pruefen von Gruppennamen bei Kollisionen
- sequentielle LDAP-/AD-Zugriffe
- sequentielle ACL-Updates
- `Thread.Sleep(180000)` im Traverse-Pfad
## Skalierungsannahmen
Grobe Groessenordnung fuer 1000 Ordner:
- AGP:
- 3 Gruppen pro Ordner
- ca. 3000 Gruppenoperationen
- AGDLP:
- 6 Gruppen pro Ordner
- ca. 6000 Gruppenoperationen
Falls jeder Zugriff separat gegen AD und Dateisystem geht, ist die Gesamtlaufzeit nicht mehr akzeptabel.
## Zielbild
Die Verarbeitung soll von einer ordnerweisen Online-Logik zu einem mehrstufigen Batch-Modell umgebaut werden.
Empfohlenes Zielbild:
1. Struktur erfassen
2. Soll-Zustand fuer alle Ordner berechnen
3. Ist-Zustand aus AD und ACLs gesammelt laden
4. Delta bilden
5. Gruppen in kontrollierten Batches anlegen oder wiederverwenden
6. ACLs in kontrollierten Batches setzen
7. Traverse separat behandeln
## Grundprinzip
Nicht pro Ordner sofort alles erledigen, sondern:
- zuerst global analysieren
- dann gesammelt abgleichen
- dann nur das Delta ausfuehren
Das reduziert:
- LDAP-Calls
- Share-/Dateisystemzugriffe
- Redundanz
- Gefahr von doppelten Gruppen
## Fachliche Verarbeitungsschritte
### Schritt 1: Ordnerliste erfassen
Fuer den gewaehlten Root werden alle relevanten Ordner einmalig geladen.
Ergebnis je Ordner:
- technischer Pfad
- Parent-Pfad
- Tiefe
- fachlicher Typ
Wichtig:
- dieser Schritt dient nur der Strukturerfassung
- noch keine AD-Anlage
### Schritt 2: Soll-Gruppen fuer alle Ordner berechnen
Fuer jeden Ordner werden die erwarteten Gruppen aus Naming Conventions und Tags berechnet.
Ergebnis je Soll-Gruppe:
- Ordnerpfad
- Gruppe
- Scope
- AccessRole
- exakter Soll-Name
- Wildcard
- benoetigte Mitglieder
- benoetigte verschachtelte Gruppen
Damit existiert ein kompletter Soll-Bestand fuer den gesamten Lauf.
### Schritt 3: AD-Ist-Zustand einmalig oder in grossen Batches laden
Statt pro Gruppe die OU neu zu durchsuchen, wird die Ziel-OU gesammelt geladen.
Empfohlene Daten:
- `sAMAccountName`
- `distinguishedName`
- `objectSid`
- `member`
Die geladene OU wird in Dictionaries abgelegt:
- nach `sAMAccountName`
- optional nach Regex-relevanten Merkmalen
Vorteil:
- keine OU-Vollsuche pro Gruppe
- kein O(n*m)-Verhalten bei vielen Gruppen
## Wichtige Optimierung
Die aktuelle Wildcard-Suche darf im Batch-Pfad nicht mehr als LDAP-Vollscan pro Gruppe stattfinden.
Stattdessen:
- OU einmal lesen
- lokal in Memory gegen Regex pruefen
Das ist einer der wichtigsten Performance-Gewinne.
### Schritt 4: ACL-Ist-Zustand gesammelt erfassen
Wenn bestehende ACL-Gruppen wiederverwendet werden sollen, werden die ACLs der Zielordner ebenfalls zuerst gesammelt gelesen.
Ergebnis je Ordner:
- direkt gesetzte ACL-Gruppen
- gematchte Owner-/Write-/Read-/Traverse-Gruppen
Dadurch kann die Wiederverwendung bestehender Gruppen lokal entschieden werden, ohne spaetere AD-Neuanlagen zu provozieren.
### Schritt 5: Delta bilden
Fuer jede Soll-Gruppe wird entschieden:
- exakter Treffer vorhanden
- eindeutiger ACL-Treffer vorhanden
- eindeutiger Regex-Treffer vorhanden
- Gruppe fehlt und muss erzeugt werden
Nur fuer fehlende Gruppen wird eine Anlage geplant.
### Schritt 6: Gruppenanlage in Batches
Die Neuanlage sollte kontrolliert parallelisiert werden.
Empfehlung:
- kleiner, begrenzter Parallelitaetsgrad
- z. B. 4 bis 8 parallele AD-Operationen
Nicht empfohlen:
- ungebremste Parallelisierung ueber Hunderte von Gruppen
Grund:
- AD, DCs und Netzwerk sollen nicht ueberlastet werden
- Kollisionen und Transienten bleiben beherrschbar
### Schritt 7: Mitgliedschaften und Verschachtelung als eigener Schritt
Gruppen anlegen und Mitglieder setzen sollten getrennt betrachtet werden.
Empfohlene Reihenfolge:
1. alle fehlenden Gruppen erzeugen
2. SIDs/DNs refreshen
3. Mitglieder und Nested Groups setzen
Das verhindert Folgefehler durch noch nicht existierende Gruppen.
### Schritt 8: ACLs in eigenem Batch-Schritt
ACLs werden erst gesetzt, wenn die benoetigten Gruppen sicher existieren.
Auch hier:
- kontrollierte Parallelitaet
- Fehler pro Ordner isolieren
- Ergebnislisten sammeln
## Traverse-Gruppen
Traverse muss aus dem normalen Massenpfad herausgezogen werden.
Der aktuelle Ansatz mit harter Wartezeit ist fuer grosse Strukturen nicht tragbar.
Fuer grosse Laeufe gilt:
- Traverse standardmaessig deaktivieren oder separieren
- Traverse als eigener Nachlauf
- keine feste `Thread.Sleep`-Pause
Stattdessen:
- Polling oder Retry mit kleinem Intervall
- nur fuer die konkret benoetigten Gruppen
## Neue logische Komponenten
### 1. `NtfsBulkPlanBuilder`
Verantwortung:
- Ordnerliste laden
- Soll-Gruppen pro Ordner berechnen
- Soll-ACLs vorbereiten
### 2. `AdSnapshotLoader`
Verantwortung:
- OU einmalig oder abschnittsweise laden
- In-Memory-Index fuer:
- `sAMAccountName`
- `SID`
- `DN`
### 3. `AclSnapshotLoader`
Verantwortung:
- direkte ACLs aller Zielordner einlesen
- passende Berechtigungsgruppen markieren
### 4. `NtfsBulkDeltaResolver`
Verantwortung:
- Soll/Ist vergleichen
- Wiederverwendung oder Neuanlage entscheiden
### 5. `AdBatchExecutor`
Verantwortung:
- Gruppenanlage
- Mitglieder setzen
- verschachtelte Gruppen setzen
### 6. `AclBatchExecutor`
Verantwortung:
- ACLs setzen
- vorhandene ACLs erkennen
- nur fehlende ACEs schreiben
## Empfohlenes Ausfuehrungsmodell
### Modus A: Analyse
Nur ermitteln:
- welche Gruppen fehlen
- welche wiederverwendet wuerden
- welche ACLs fehlen
Ohne Schreibzugriffe.
### Modus B: Ensure
Nur Delta anwenden:
- fehlende Gruppen anlegen
- fehlende Mitglieder ergaenzen
- fehlende ACLs setzen
### Modus C: Traverse-Nachlauf
Separat und optional:
- Traverse-Gruppen erzeugen
- Traverserechte setzen
## Fehlerstrategie
Bei grossen Mengen darf ein Einzelfehler nicht immer den Gesamtjob abbrechen.
Empfohlen:
- Fehler pro Ordner oder Gruppe sammeln
- Schwellenwert definieren
- am Ende Gesamtstatus ableiten
Beispiel:
- `Success`
- `SuccessWithWarnings`
- `PartialFailure`
- `Failure`
## Logging und Monitoring
Bei grossen Laeufen werden strukturierte Fortschrittsdaten gebraucht.
Empfohlene Kennzahlen:
- Anzahl gescannter Ordner
- Anzahl Soll-Gruppen
- Anzahl wiederverwendeter Gruppen
- Anzahl neu erzeugter Gruppen
- Anzahl geaenderter Gruppenmitgliedschaften
- Anzahl gesetzter ACLs
- Anzahl uebersprungener ACLs
- Anzahl Fehler
- Laufzeit je Phase
Wichtig:
- Fortschritt nicht nur pro Objekt loggen
- zusaetzlich Phasen- und Batch-Statistik loggen
## Konkrete technische Leitplanken
### Nicht mehr so
- pro Ordner komplette OU-Wildcard-Suche
- pro Gruppe sofortiger AD-Zugriff
- Traverse mit fester 180-Sekunden-Wartezeit
- keine Trennung zwischen Analyse und Ausfuehrung
### Stattdessen
- OU-Snapshot einmalig laden
- ACL-Snapshot gesammelt laden
- Delta zentral berechnen
- kontrollierte Parallelitaet
- Traverse separat
## Rueckwaertskompatibilitaet
Der bestehende Einzelordner-Pfad kann bestehen bleiben.
Empfehlung:
- bestehende Methoden fuer Einzelobjekte weiter nutzen
- neuen Bulk-Pfad zusaetzlich einfuehren
Beispiele:
- `EnsureMissingPermissionGroupsAsync(...)` bleibt fuer Einzelordner
- neuer Bulk-Einstieg fuer Listen oder Root-Strukturen
## Minimal sinnvolle erste Ausbaustufe
Wenn nicht sofort das gesamte Zielbild umgesetzt werden soll, ist die sinnvollste erste Stufe:
1. OU nicht mehr pro Gruppe scannen, sondern einmalig laden
2. ACL-Wiederverwendung fuer alle Zielordner gesammelt vorbereiten
3. Traverse aus grossen Laeufen herausnehmen
4. Ensure fuer grosse Mengen explizit batchen
Schon diese vier Punkte wuerden die groessten praktischen Probleme stark reduzieren.
## Entscheidung
Fuer grosse Strukturen mit 1000+ Ordnern ist die aktuelle ordnerweise Online-Logik nicht ausreichend.
Die empfohlene Zielumsetzung ist daher:
- Batch-orientierte Voranalyse
- AD-Snapshot statt OU-Scan pro Gruppe
- ACL-Snapshot statt Einzelfallentscheidungen
- Delta-basierte Ausfuehrung
- kontrollierte Parallelisierung
- Traverse als separater Spezialpfad
Nur damit wird die AD-Gruppenanlage fuer grosse Strukturen technisch belastbar und operativ beherrschbar.

View File

@@ -0,0 +1,493 @@
# LIAM NTFS Share-Provisionierung und AD-Gruppen - Konzept
## Ziel
Dieses Dokument beschreibt ein Zielkonzept, um neue Benutzer analog zum bestehenden NTFS-Ordnerprinzip auch fuer Shares ueber AD-Gruppen zu berechtigen.
Betrachtet werden zwei Faelle:
1. komplette Neuanlage eines Shares
2. Erzeugung und Konfiguration passender AD-Gruppen fuer einen bereits bestehenden Share
Das Dokument ist bewusst ein Konzept. Es beschreibt Zielbild, notwendige Schritte, technische Erweiterungen und offene Entscheidungen, aber keine direkte Implementierung.
## Ausgangslage im aktuellen Stand
Der aktuelle NTFS-Pfad deckt heute nur einen Teil des benoetigten Verhaltens ab:
- `CreateDataAreaAsync(...)` erzeugt nur Verzeichnisse und NTFS-ACLs fuer einen Ordnerpfad.
- `EnsureMissingPermissionGroupsAsync(...)` stellt fehlende AD-Gruppen und NTFS-ACLs fuer einen bestehenden Ordnerpfad sicher.
- `cLiamNtfsShare` existiert als DataArea-Typ, aber es gibt keinen eigenen Schreibpfad fuer die Share-Neuanlage.
- Die aktuelle Berechtigungslogik arbeitet technisch auf Dateisystempfaden und NTFS-ACLs.
- Eine echte SMB-Share-Erzeugung oder Pflege von Share-Berechtigungen ist derzeit nicht vorhanden.
Wichtig ist daher:
- Das bestehende Prinzip fuer Ordner kann teilweise wiederverwendet werden.
- Fuer Shares reicht eine reine Wiederverwendung des Ordnerpfads nicht aus.
- Es braucht zusaetzlich einen eigenen Share-Provisionierungspfad.
## Fachliche Grundentscheidung
Bevor implementiert wird, sollte fachlich festgelegt werden, was "Share berechtigen" in LIAM genau bedeutet.
Es gibt zwei moegliche Zielbilder:
1. nur NTFS-Berechtigungen auf dem Share-Root-Ordner pflegen
2. NTFS-Berechtigungen auf dem Share-Root-Ordner und zusaetzlich echte SMB-Share-Berechtigungen pflegen
Empfehlung:
- Fuer ein wirklich analoges Share-Prinzip sollten beide Ebenen gepflegt werden.
- Nur NTFS zu pflegen waere technisch einfacher, bildet aber Share-Berechtigungen nur unvollstaendig ab.
Das Konzept unten geht deshalb vom vollstaendigen Zielbild aus:
- AD-Gruppen erzeugen oder wiederverwenden
- NTFS-ACL auf dem Share-Root sicherstellen
- SMB-Share anlegen oder aktualisieren
- SMB-Share-Berechtigungen auf dieselben Gruppen ausrichten
## Zielbild Architektur
Empfohlen ist eine Trennung in zwei technische Ebenen:
1. Dateisystem-/AD-Ebene
2. Share-Ebene
Die Dateisystem-/AD-Ebene kann weitgehend auf dem bestehenden `DataArea_FileSystem`-Prinzip aufbauen.
Die Share-Ebene ist neu und sollte separat gekapselt werden, zum Beispiel als eigener Share-Provisionierer oder als Erweiterung von `cNetworkConnection`.
Grund:
- Share-Erzeugung und Share-Berechtigungen sind fachlich nicht dasselbe wie NTFS-ACLs.
- Sie brauchen andere API-Aufrufe, andere Fehlerbehandlung und andere Validierungen.
## Fall 1: komplette Neuanlage eines Shares
### Ziel
Ein neuer Share soll vollstaendig bereitgestellt werden:
- physischer Zielordner vorhanden
- AD-Gruppen vorhanden
- Benutzer und Gruppenmitgliedschaften gesetzt
- NTFS-ACLs auf dem Root-Ordner gesetzt
- SMB-Share publiziert
- SMB-Share-Berechtigungen gesetzt
- Ergebnis als LIAM-DataArea nutzbar
### Zusaetzliche Eingaben
Fuer einen echten Share-Create reichen die bisherigen Ordnerparameter nicht aus. Noetig sind zusaetzlich mindestens:
- `ServerName`
- `ShareName`
- `ShareLocalPath` oder ein technisch gleichwertiger physischer Zielpfad auf dem Server
- optional `ShareDescription`
- optional Share-spezifische CustomTags
- Owner-/Read-/Write-SIDs analog zum bestehenden Ordnerpfad
Optional sinnvoll:
- gewuenschtes Caching-Verhalten des Shares
- Offline-Availability
- ABE oder weitere Share-Optionen
- explizite Strategie fuer Share-Permissions
### Warum ein eigener Input noetig ist
Der heutige Ordner-Create-Pfad arbeitet auf einem existierenden UNC-/Dateisystempfad. Bei einem neuen Share existiert der veroeffentlichte UNC-Pfad aber noch nicht.
Deshalb muss die Share-Neuanlage mit einem physischen Serverpfad arbeiten und darf nicht nur von einem spaeteren UNC-Share-Namen ausgehen.
### Ablauf
#### Schritt 1: Eingaben und Zielkonflikte validieren
Vor der eigentlichen Anlage muessen folgende Punkte geprueft werden:
- ist der Server erreichbar
- ist der physische Zielpfad zulaessig
- existiert der physische Zielordner bereits
- existiert bereits ein Share mit demselben Namen
- ist der Share-Name nach Windows-Regeln gueltig
- ist die Ziel-OU fuer AD-Gruppen erreichbar
- sind die benoetigten Naming-Conventions und Tags vollstaendig
Abbruchkriterien:
- doppelter Share-Name
- ungueltiger Server- oder Zielpfad
- fehlende Berechtigungen fuer Dateisystem, AD oder Share-Verwaltung
#### Schritt 2: Physisches Zielverzeichnis bereitstellen
Falls der Root-Ordner noch nicht existiert, muss er zuerst erstellt werden.
Wichtig:
- Das darf nicht ueber den spaeteren Share-UNC-Pfad geschehen.
- Es braucht einen Zugriff auf den physischen Serverpfad.
Technische Moeglichkeiten:
- Zugriff ueber administrative Shares wie `\\server\\D$\\...`
- serverseitige API oder PowerShell-Remoting
- anderer dedizierter Remote-Mechanismus
Empfehlung:
- Die Share-Provisionierung sollte den physischen Serverpfad explizit kennen.
- Die spaetere DataArea sollte daraus den veroeffentlichten UNC-Pfad ableiten.
#### Schritt 3: Soll-Gruppen fuer den Share berechnen
Fachlich analog zu Ordnern werden die erwarteten Gruppen berechnet:
- Owner
- Write
- Read
- optional Traverse, falls fuer Share-Roots gewuenscht
- bei AGDLP zusaetzlich Domain-Local-Gruppen
Hier sollte dieselbe Template- und Tag-Logik wie bei Ordnern wiederverwendet werden.
Wichtig ist aber die Namensbasis:
- Bei einem Share sollte die Namensbildung stabil am Share-Root ausgerichtet sein.
- Die Namensbasis darf nicht davon abhaengen, ob spaeter unterhalb des Shares weitere Ordner existieren.
Empfehlung:
- zusaetzliche Platzhalter wie `SHARENAME`, `SHAREPATH`, `SHARESERVER`
- klare Regel, ob die bestehende Ordner-Relative-Path-Logik unveraendert ausreicht oder erweitert werden muss
#### Schritt 4: AD-Gruppen erzeugen oder wiederverwenden
Der bestehende Ensure-Ansatz kann weitgehend uebernommen werden:
- exakten Gruppennamen pruefen
- bei nicht-striktem Modus vorhandene passende Gruppen ueber ACL-/Wildcard-Logik wiederverwenden
- fehlende Gruppen anlegen
- Benutzer und verschachtelte Gruppen setzen
Fuer Share-Create gilt fachlich dieselbe Reihenfolge wie bei Ordnern:
1. alle benoetigten Gruppen aufloesen oder erzeugen
2. Mitgliedschaften setzen
3. SIDs und DNs final einlesen
#### Schritt 5: NTFS-ACL auf dem Share-Root setzen
Auf dem physischen Root-Ordner werden die passenden NTFS-Berechtigungen gesetzt.
Empfohlen:
- dieselbe Rechteabbildung wie bei Ordnern
- dieselbe Logik fuer AGP oder AGDLP
- explizite ACL-Pruefung vor dem Schreiben
- Ergebnis mit `addedAclEntries` und `skippedAclEntries` differenziert zurueckgeben
#### Schritt 6: SMB-Share veroeffentlichen
Nach erfolgreicher Dateisystem- und Gruppenanlage wird der Share selbst angelegt.
Dafuer wird eine neue technische Faehigkeit benoetigt, zum Beispiel:
- `CreateShare(server, shareName, localPath, description, ...)`
Technisch denkbar:
- Win32 `NetShareAdd`
- PowerShell `New-SmbShare`
- WMI/CIM
Empfehlung:
- moeglichst eine native, klar kapselbare Server-API nutzen
- Fehlercodes aus der Share-Anlage explizit in `ResultToken` uebernehmen
#### Schritt 7: SMB-Share-Berechtigungen setzen
Nach der Share-Erzeugung muessen die Share-Berechtigungen mit den erzeugten oder wiederverwendeten AD-Gruppen synchronisiert werden.
Empfohlene fachliche Abbildung:
- Owner-Gruppe: Full Control oder ein explizit definierter administrativer Satz
- Write-Gruppe: Change
- Read-Gruppe: Read
Wichtig:
- Share-Rechte sind fachlich nicht identisch zu NTFS-Rechten.
- Die Mapping-Regeln muessen explizit definiert werden.
#### Schritt 8: Read-back und Rueckgabe
Am Ende sollte der neu angelegte Share nochmals eingelesen werden:
- existiert der Share wirklich
- zeigt er auf den erwarteten Zielpfad
- stimmen NTFS-ACL und Share-Permissions
- stimmen aufgeloeste AD-Gruppen
Das Ergebnis sollte danach als `NtfsShare` inklusive Gruppenreferenzen zurueckgegeben werden.
## Fall 2: bestehender Share, aber fehlende AD-Gruppen
### Ziel
Fuer einen bereits publizierten Share sollen fehlende Gruppen und Berechtigungen analog zum bestehenden Ordner-Ensure-Pfad nachgezogen werden.
### Unterschied zum heutigen Ordner-Ensure
Der heutige `EnsureMissingPermissionGroupsAsync(...)` kann technisch gegen einen vorhandenen Root-Ordner laufen. Das reicht fuer Shares aber nur teilweise:
- NTFS-ACL am Share-Root kann damit sichergestellt werden
- die SMB-Share-Konfiguration selbst wird damit nicht gepflegt
- bestehende Share-Berechtigungen werden damit nicht ausgelesen oder korrigiert
Deshalb braucht es fuer Shares einen eigenen Ensure-Pfad.
### Eingaben
Mindestens benoetigt werden:
- `SharePath` als veroeffentlichter UNC-Pfad
- alternativ `ServerName + ShareName`
- optional physischer Zielpfad, falls nicht sicher auslesbar
- Owner-/Read-/Write-SIDs
- optionale CustomTags
### Ablauf
#### Schritt 1: Share aufloesen und klassifizieren
Der bestehende Share muss zunaechst technisch aufgeloest werden:
- existiert der Share
- auf welchem Server liegt er
- welcher physische Zielpfad ist hinterlegt
- ist er ein klassischer Share oder ein DFS-Link
Empfehlung:
- Nur klassische Shares im ersten Schritt unterstuetzen.
- DFS-Links spaeter separat betrachten, weil dort Share-Pfad und Zielpfad auseinanderlaufen koennen.
#### Schritt 2: Ist-Zustand laden
Fuer den bestehenden Share muessen drei Ist-Zustaende geladen werden:
- vorhandene AD-Gruppen
- vorhandene NTFS-ACL auf dem Root-Ordner
- vorhandene SMB-Share-Berechtigungen
Nur damit kann spaeter ein echtes Delta gebildet werden.
#### Schritt 3: Soll-Gruppen berechnen
Die Gruppen muessen fuer den Share auf dieselbe Weise berechnet werden wie bei der Neuanlage.
Dabei sollte dieselbe Namensbasis verwendet werden wie im Create-Pfad, damit Create und Ensure fachlich konsistent bleiben.
#### Schritt 4: Wiederverwendung oder Neuanlage von AD-Gruppen
Analog zum bestehenden Prinzip:
- exakten Namen pruefen
- bei nicht-striktem Modus vorhandene passende Gruppen wiederverwenden
- bei mehrdeutigen Treffern nicht still entscheiden
- fehlende Gruppen neu erzeugen
Empfehlung:
- die bisherige `ForceStrictAdGroupNames`-Logik fuer Shares unveraendert wiederverwenden
- Wiederverwendung gegen bestehende ACLs nur dann zulassen, wenn eindeutig und fachlich plausibel
#### Schritt 5: NTFS-ACL sicherstellen
Auf dem physischen Share-Root wird anschliessend die NTFS-ACL zum Sollzustand gebracht.
Dabei sollte dieselbe Logik wie im Ordner-Ensure verwendet werden:
- bereits vorhandene explizite Regel erkennen
- fehlende Regel setzen
- Ergebnismengen getrennt dokumentieren
#### Schritt 6: SMB-Share-Berechtigungen sicherstellen
Zusaetzlich muessen die Share-Berechtigungen ausgewertet und auf Soll gebracht werden.
Dabei sind dieselben Gruppen wie fuer die NTFS-Seite zu verwenden, aber mit einem expliziten Mapping auf Share-Rechte.
Empfehlung:
- Read-Gruppe -> Read
- Write-Gruppe -> Change
- Owner-Gruppe -> Full Control
Falls im Ist-Zustand breit vergebene Standardrechte vorhanden sind, muss fachlich entschieden werden:
- beibehalten und nur ergaenzen
- oder gezielt bereinigen und auf LIAM-Gruppen normieren
#### Schritt 7: Ergebnis und Warnungen
Der Ensure-Pfad fuer Shares sollte im Ergebnis klar trennen:
- neu angelegte Gruppen
- wiederverwendete Gruppen
- hinzugefuegte NTFS-ACLs
- uebersprungene NTFS-ACLs
- hinzugefuegte Share-Berechtigungen
- uebersprungene Share-Berechtigungen
- Warnungen bei Mehrdeutigkeiten, Altlasten oder nicht bereinigten Fremdeintraegen
## Notwendige technische Erweiterungen
### 1. Workflow- und Runtime-Oberflaeche
Empfohlen sind zusaetzliche dedizierte Aufrufe:
- `CreateNtfsShareAsync(...)`
- `EnsureNtfsSharePermissionGroupsAsync(...)`
Die bestehenden Ordner-Methoden sollten nicht still auf Shares umgebogen werden, weil die benoetigten Inputs und Resultate fachlich anders sind.
### 2. Provider-Ebene
Im NTFS-Provider braucht es einen expliziten Share-Schreibpfad:
- Eingaben validieren
- physischen Zielpfad und publizierten Share koordinieren
- AD-/NTFS-Engine aufrufen
- Share-Erzeugung oder Share-Update ausfuehren
### 3. Engine fuer Share-Operationen
Empfohlen ist eine neue technische Komponente, zum Beispiel:
- `ShareProvisioningEngine`
- oder eine klar getrennte Erweiterung von `cNetworkConnection`
Noetige Faehigkeiten:
- `ShareExists`
- `GetShareInfo`
- `CreateShare`
- `EnsureSharePermissions`
- optional `RemoveSharePermission`
### 4. ResultToken erweitern
Fuer Shares braucht es zusaetzliche Ergebnisfelder, zum Beispiel:
- `createdShares`
- `reusedShares`
- `addedShareAclEntries`
- `skippedShareAclEntries`
- `shareWarnings`
Alternativ kann ein eigener spezialisierter ResultToken fuer Share-Operationen eingefuehrt werden.
### 5. Logging
Fuer spaetere Betriebsdiagnose sollten diese Punkte explizit geloggt werden:
- Server, Share-Name, physischer Zielpfad
- exakter Create- oder Ensure-Modus
- Quelle einer Gruppenwiederverwendung
- Ergebnis der Share-Erzeugung
- Ergebnis der Share-Permission-Sicherung
- klare Trennung zwischen NTFS-ACL und Share-ACL
## Wichtige offene Entscheidungen
### 1. DFS-Unterstuetzung
Fuer einen ersten belastbaren Schritt sollte sich die Implementierung auf klassische Shares konzentrieren.
DFS-Links sind fachlich moeglich, aber deutlich komplexer:
- publizierter Pfad und physischer Zielpfad koennen auseinanderfallen
- Share- und DFS-Berechtigung sind nicht dasselbe
- die Zielserver koennen wechseln
Empfehlung:
- Phase 1 nur klassische Shares
- DFS spaeter bewusst als eigener Ausbau
### 2. Share-Permissions als Pflicht oder Optional
Wenn nur NTFS gesetzt wird, bleibt das Share-Modell unvollstaendig.
Empfehlung:
- Share-Permissions als Bestandteil des Zielbilds definieren
- optional ueber Flag abschaltbar machen, aber nicht dauerhaft weglassen
### 3. Namensbildung fuer Share-Roots
Es muss eindeutig festgelegt werden, worauf sich die Gruppennamen eines Shares beziehen:
- nur auf den Share-Namen
- auf den physischen Root-Pfad
- auf den publizierten UNC-Pfad
Empfehlung:
- Primaerschluessel fachlich am publizierten Share-Namen ausrichten
- physische Pfadbestandteile nur zusaetzlich als Tags oder Beschreibung verwenden
### 4. Altlasten bei bestehenden Shares
Bei bestehenden Shares koennen bereits andere Gruppen oder breite Standardrechte existieren.
Dafuer braucht es eine klare Strategie:
- nur ergaenzen
- normierend bereinigen
- oder nur warnen
Empfehlung:
- im ersten Schritt nur ergaenzen und warnen
- bereinigende Eingriffe erst in einem spaeteren expliziten Modus
## Empfohlene Umsetzung in Phasen
### Phase 1
- dedizierter Ensure-Pfad fuer bestehende klassische Shares
- AD-Gruppen sicherstellen
- NTFS-ACL auf Share-Root sicherstellen
- Share-Permissions lesen und setzen
### Phase 2
- vollstaendiger Create-Pfad fuer klassische Shares
- physisches Root-Verzeichnis anlegen
- Share publizieren
- Share read-back und Ergebnisobjekt liefern
### Phase 3
- DFS-Sonderfaelle
- bereinigender Modus fuer Altlasten
- Massenverarbeitung fuer viele Shares
## Kurzfazit
Fuer Shares ist keine reine Kopie des bestehenden Ordnerpfads ausreichend.
Noetig ist:
- Wiederverwendung der vorhandenen AD-Gruppen- und NTFS-Logik
- plus ein neuer Share-spezifischer Schreibpfad
- plus eine klare fachliche Behandlung von SMB-Share-Berechtigungen
Der sinnvollste erste Schritt ist ein eigener Ensure-Pfad fuer bestehende klassische Shares. Danach kann die vollstaendige Share-Neuanlage auf derselben Logik aufbauen.

View File

@@ -0,0 +1,452 @@
# LIAM WF GetDataAreas - Auffaelligkeiten und moegliche Fixes (initial)
## Ziel des Dokuments
Dieses Dokument betrachtet ausschliesslich den Stand `initial` (`f563d78417c8183ac0934beaa11d0f258b7537bf`).
Es bewertet die Rueckfrage, warum `WF Get DataAreas` in einer betroffenen Situation nicht mehr hunderte Datenbereiche, sondern nur noch sehr wenige Eintraege, beispielsweise `3`, zurueckliefern kann.
Ausdruecklich nicht im Fokus stehen hier:
- geaenderte `MaxDepth`
- geaenderte Naming-Conventions
- spaetere Aenderungen im aktuellen Hauptstand
Im Fokus stehen stattdessen nur moegliche Ursachen, die bereits im `initial`-Stand zu so einem Verhalten fuehren konnten:
- geaenderte Ordnerstruktur
- geaenderte Rechte auf Shares oder Ordnern
- geaenderte ACL-Situation
- geaenderte AD-Erreichbarkeit oder AD-Berechtigungen
- geaenderte DFS-Erreichbarkeit
- Logging-Luecken und stille Teilergebnisse
## Kurzfazit
Im `initial`-Stand ist das geschilderte Verhalten grundsaetzlich moeglich.
Die plausibelsten Ursachen fuer `frueher viele, jetzt nur noch wenige` sind im Altstand:
- geaenderte sichtbare Ordnerstruktur unterhalb des konfigurierten `RootPath`
- geaenderte Leserechte auf Teilbaeumen des Dateisystems
- Probleme beim rekursiven Traversieren, die im Code als Teilergebnis statt als klarer Fehler enden
- Probleme bei der Erreichbarkeit des Root-Pfads selbst, insbesondere bei UNC-/DFS-Pfaden
Reine AD-Probleme sind im `initial`-Stand fuer genau das Muster `es kommen noch 3 zurueck` deutlich weniger plausibel.
Wenn AD im `initial`-Stand wirklich nicht funktioniert, fuehrt das eher zu einem Fehlschlag des Logons und damit zu `null` oder zu einem kompletten Fehlerbild, nicht zu einer kleinen aber formal erfolgreichen Teilmenge.
Wichtig ist auch: Der `initial`-Stand verwendet fuer `GetDataAreas` noch keine explizite DFS-Klassifikation und keine Share-Enumeration. DFS-Probleme koennen also nur indirekt ueber den UNC-Pfad und dessen Dateisystemerreichbarkeit wirken, nicht ueber die spaetere Root-/DFS-Logik des heutigen Stands.
## Gepruefter Umfang
Fuer diese Einschaetzung wurden im `initial`-Stand insbesondere diese Dateien betrachtet:
- `LiamWorkflowActivities/C4IT.LIAM.WorkflowActivities.cs`
- `LiamWorkflowActivities/C4IT.LIAM.WorkflowactivityBase.cs`
- `LiamNtfs/C4IT.LIAM.Ntfs.cs`
- `LiamNtfs/cNtfsBase.cs`
- `LiamNtfs/cActiveDirectoryBase.cs`
- `LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs`
## Workflow-Verhalten im initial-Stand
Workflow-seitig ist der `initial`-Stand sehr duenn.
Die damalige Methode `getDataAreasFromProvider(Guid ProviderConfigClassID)` ruft den Provider direkt mit `ProviderEntry.Provider.getDataAreasAsync(ProviderEntry.Provider.MaxDepth)` auf und behandelt `null` oder `Count <= 0` pauschal als `No data areas found`.
Das bedeutet:
- Es gibt keine Unterscheidung zwischen technischem Fehler, Teilergebnis und wirklich leerem Bestand.
- Es gibt keine strukturierte Fehlerdiagnose fuer die Discovery.
- Es wird nicht protokolliert, wie viele Ordner theoretisch erreichbar gewesen waeren oder wo die Traversierung abgebrochen ist.
Damit ist bereits auf Workflow-Ebene erklaert, warum im Log oft nur wenig Aussagekraeftiges sichtbar ist.
## Discovery-Logik im initial-Stand
Die eigentliche Discovery sitzt in `cLiamProviderNtfs.getDataAreasAsync(int Depth = -1)`.
Die Logik ist im `initial`-Stand im Kern:
1. Lizenz pruefen
2. `LogonAsync()` ausfuehren
3. `RootPath` auf Leerwert pruefen
4. Root-Typ ueber die Anzahl der UNC-Segmente herleiten
5. `ntfsBase.RequestFoldersListAsync(this.RootPath, Depth)` aufrufen
6. alle zurueckgelieferten Ordner in `DataAreas` uebernehmen
Entscheidend ist:
- Es gibt im `initial`-Stand keine explizite DFS-Metadatenpruefung.
- Es gibt keine Share-Enumeration fuer den Root.
- Es wird direkt versucht, unterhalb des konfigurierten `RootPath` Dateisystemordner zu enumerieren.
Dadurch reagieren Ergebniszahl und Ergebnisqualitaet im `initial`-Stand primaer auf Dateisystemsichtbarkeit und Traversierungsrechte.
## Priorisierte Auffaelligkeiten im initial-Stand
### 1. Hoch: Zugriffsprobleme in der Ordnerrekursion werden als Teilergebnis zurueckgegeben
Das kritischste Verhalten fuer das geschilderte Fehlerbild steckt in `cNtfsBase.privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null)`.
Wenn waehrend `rootPath.GetDirectories()` oder in tieferen Rekursionsstufen ein Fehler auftritt, landet der Code in einem allgemeinen `catch` und gibt einfach die bis dahin gesammelte Liste `folders` zurueck.
Das bedeutet fachlich:
- Ein Zugriff auf den Root kann funktionieren.
- Die Traversierung kann auf tieferen Ebenen an einzelnen Ordnern scheitern.
- Statt eines klaren Fehlers wird aber nur die bis dahin gefundene Teilmenge zurueckgegeben.
Genau dieses Verhalten passt sehr gut zu:
- `frueher hunderte`
- `jetzt noch 3`
- `im Log sehe ich nichts`
Wenn sich zwischenzeitlich Leserechte oder effektive Traversierungsrechte auf Teilbaeumen geaendert haben, kann der `initial`-Stand damit sehr gut nur noch wenige erreichbare Ordner zurueckliefern.
Das ist im `initial`-Stand keine theoretische Ecke, sondern ein reales stilles Fehlerbild.
#### Abgleich zum aktuellen Stand
Status: Nicht gefixt, weiterhin offen.
Die kritische Rekursionsstelle in [`cNtfsBase.cs#L143`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L143) ist im aktuellen Stand weiterhin vorhanden. Das innere `catch` in [`cNtfsBase.cs#L174`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L174) liefert nach wie vor nur die bis dahin gefundenen Ordner zurueck.
Der aktuelle NTFS-Provider hat zwar die Root- und Pfadklassifikation stark geaendert, verwendet fuer Folder-Kinder aber weiterhin `RequestFoldersListAsync(...)`, siehe [`C4IT.LIAM.Ntfs.cs#L389`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L389). Damit bleibt das alte Problem der stillen Teilergebnisse erhalten.
Offenes ToDo:
- Traversierungsfehler pro Pfad explizit loggen
- Teilergebnis fachlich kennzeichnen statt still als Erfolg weiterzugeben
- im Workflow/API zwischen `vollstaendig`, `partial` und `failed` unterscheiden
### 2. Hoch: Aenderungen an der Ordnerstruktur wirken im initial-Stand direkt auf die Rueckgabemenge
Da der `initial`-Stand keine Root-Klassifikation ueber DFS-/Share-Metadaten macht, sondern schlicht rekursiv unterhalb von `RootPath` liest, haengt die Rueckgabemenge unmittelbar von der sichtbaren Ordnerstruktur ab.
Wenn beispielsweise:
- Unterordner entfernt wurden
- Verzeichnisstrukturen verschoben wurden
- Junctions oder Weiterleitungen anders wirken
- der konfigurierte Root heute auf einen kleineren Teilbaum zeigt
dann sinkt die Anzahl der gefundenen DataAreas direkt.
Im `initial`-Stand gibt es keine zusaetzliche semantische Schicht, die das noch abfedert oder klarer diagnostiziert.
#### Abgleich zum aktuellen Stand
Status: Nicht gefixt, aber fachlich veraendert.
Auch im aktuellen Stand wirkt sich die sichtbare Struktur weiterhin direkt auf die Rueckgabemenge aus. Allerdings passiert das nicht mehr nur ueber reine Ordnerrekursion, sondern ueber die neue Pfadklassifikation in [`C4IT.LIAM.Ntfs.cs#L285`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L285) und die Kindermittlung in [`C4IT.LIAM.Ntfs.cs#L363`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L363).
Das alte Finding ist damit nicht obsolet, sondern der Mechanismus ist heute breiter:
- Ordnerstruktur unterhalb eines Folder-/Share-Roots
- sichtbare Shares unterhalb eines Server-Roots
- sichtbare DFS-Objekte unterhalb eines DFS-Roots
Offenes ToDo:
- fachlich klar definieren, welche Root-Typen fuer `WF GetDataAreas` unterstuetzt und erwartet sind
- die ermittelte Root-Klassifikation und Kinderanzahl pro Call explizit loggen
### 3. Hoch: Der Root selbst muss erreichbar sein, sonst scheitert der gesamte Call
Vor dem eigentlichen Lesen fuehrt der Provider `LogonAsync()` aus.
Darin werden sowohl `ntfsBase.LogonAsync(LI)` als auch `activeDirectoryBase.LogonAsync(LI)` aufgerufen.
Das bedeutet:
- Der technische Benutzer muss den UNC-Root grundsaetzlich erreichen.
- Der AD-Login muss ebenfalls gelingen.
Wenn bereits der UNC-Zugriff auf `RootPath` scheitert, kommt es eher zu einem harten Fehlerbild.
Fuer das Muster `nur noch 3` ist deshalb weniger der komplette Ausfall des Root-Zugriffs interessant, sondern eher der Fall:
- Root erreichbar
- einige Unterordner oder Teilbaeume nicht mehr sauber lesbar
Genau dann greift wieder das stille Teilergebnis aus der Rekursion.
#### Abgleich zum aktuellen Stand
Status: Nicht gefixt, weiterhin offen.
Die Kopplung von NTFS- und AD-Logon besteht weiterhin. Der aktuelle Provider verwendet noch immer `await ntfsBase.LogonAsync(LI) && await activeDirectoryBase.LogonAsync(LI)`, siehe [`C4IT.LIAM.Ntfs.cs#L126`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L126) und [`C4IT.LIAM.Ntfs.cs#L140`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L140).
Fuer den aktuellen Stand gilt zusaetzlich: der Root selbst kann heute noch an mehr Infrastrukturhaengigkeiten gekoppelt sein als im `initial`-Stand, etwa an Share-Enumeration oder DFS-Metadaten.
Offenes ToDo:
- NTFS-Discovery fachlich von AD-Zusatzauflosung entkoppeln
- Root-Erreichbarkeit, AD-Login und spaetere Zusatzdatenaufloesung getrennt diagnostizieren
### 4. Mittel-Hoch: DFS-Probleme koennen im initial-Stand indirekt wirken
Im `initial`-Stand wird DFS fuer `GetDataAreas` nicht explizit ausgewertet.
Es gibt dort:
- keine DFS-Namespace-Klassifikation
- keine DFS-Link-Erkennung
- keine Nutzung von `NetDfsGetInfo()` im Discovery-Pfad
Das ist wichtig, weil dadurch spaetere Ursachen aus dem aktuellen Stand hier gerade nicht gelten.
Trotzdem kann DFS im `initial`-Stand indirekt relevant sein, wenn `RootPath` ein DFS-Pfad ist.
Dann wirken DFS-Probleme nicht ueber Metadatenklassifikation, sondern ueber die schlichte Tatsache, dass `DirectoryInfo(rootPath).GetDirectories()` auf diesem UNC-Pfad heute vielleicht nur noch teilweise oder gar nicht mehr funktioniert.
Moegliche Folgen:
- DFS-Ziel aktuell nicht sauber erreichbar
- Name oder Ziel nicht mehr korrekt aufloesbar
- technische Session landet auf anderem oder reduziertem Ziel
- Traversierung liefert nur eine kleine Teilmenge
Damit ist DFS im `initial`-Stand als indirekter Infrastrukturhebel durchaus plausibel, aber nicht ueber denselben Mechanismus wie im heutigen Stand.
#### Abgleich zum aktuellen Stand
Status: Nicht gefixt, sondern fachlich verlagert und erweitert.
Dieses Finding gilt 1:1 nur fuer `initial`. Im aktuellen Stand ist DFS nicht mehr nur indirekt relevant, sondern expliziter Teil der Discovery-Logik. Die DFS-Klassifikation laeuft ueber [`C4IT.LIAM.Ntfs.cs#L309`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L309), [`C4IT.LIAM.Ntfs.cs#L431`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L431) und [`cNetworkConnection.cs#L104`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L104).
Das bedeutet:
- Der konkrete `initial`-Mechanismus gilt nicht mehr unveraendert.
- Operativ ist DFS heute aber eher noch wichtiger als frueher.
- DFS-Fehler koennen im aktuellen Stand die Root-Klassifikation und damit die DataArea-Anzahl direkt beeinflussen.
Offenes ToDo:
- DFS-Aufloesungsfehler mit Rueckgabecode und betroffenem Pfad loggen
- im Diagnosepfad sichtbar machen, ob ein Pfad als `DfsNamespaceRoot`, `DfsLink`, `ClassicShare`, `ServerRoot` oder `Folder` klassifiziert wurde
### 5. Mittel: AD-Probleme erklaeren eher harte Fehler als kleine Treffermengen
Der `initial`-Stand koppelt NTFS- und AD-Logon eng zusammen.
Wenn `activeDirectoryBase.LogonAsync(LI)` scheitert, wird das gesamte `LogonAsync()` des Providers `false`, und `getDataAreasAsync()` gibt `null` zurueck.
Das spricht gegen AD als primaere Ursache fuer eine kleine, aber noch erfolgreiche Trefferliste.
Reine AD-Erreichbarkeitsprobleme passen im `initial`-Stand eher zu:
- kompletter Fehler
- keine DataAreas
- harter Ausfall beim Providerzugriff
Teilprobleme in AD koennen spaeter fuer Zusatzinformationen relevant sein, aber nicht besonders gut fuer `nur noch 3 DataAreas`.
#### Abgleich zum aktuellen Stand
Status: Im Kern weiterhin zutreffend, nicht grundsaetzlich gefixt.
Auch im aktuellen Stand spricht die gekoppelte Logon-Logik eher dafuer, dass echte AD-Logon-Probleme zu einem harten Fehler fuehren und nicht zu einer kleinen erfolgreichen Restliste. Die Kopplung ist weiterhin vorhanden, siehe [`C4IT.LIAM.Ntfs.cs#L140`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L140).
Verbessert wurde immerhin die Workflow-Rueckgabe: die neuere Runtime liefert bei `null`-Resultaten strukturierte Fehlercodes statt nur einer pauschalen `No data areas found`-Meldung, siehe [`LiamWorkflowRuntime.cs#L78`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/LiamWorkflowRuntime.cs#L78).
Offenes ToDo:
- AD-Fehler weiterhin von reiner NTFS-Discovery entkoppeln
- bei Discovery-Calls sauber zwischen `Logon failed`, `Traversal partial` und `No items found` trennen
### 6. Mittel: ACL-/Gruppenaufloesung beeinflusst eher Zusatzdaten als die Anzahl
Die ACL-Leselogik sitzt in `cActiveDirectoryBase.GetAccessControlList(string path)`.
Wenn dort auf einem Ordner die ACL nicht gelesen werden kann, wird `null` geliefert.
In der weiteren Verarbeitung fuehrt das eher dazu, dass zu einer bereits gefundenen DataArea Gruppeninformationen oder Owner-/Read-/Write-Zuordnungen fehlen.
Das reduziert im `initial`-Stand typischerweise nicht die Anzahl der gefundenen DataAreas selbst.
Darum gilt fuer die Fragestellung:
- geaenderte ACLs koennen die Zahl der DataAreas reduzieren, wenn sie die Ordner-Traversierung blockieren
- geaenderte ACLs erklaeren die reduzierte Zahl eher nicht, wenn nur die spaetere Permission-Group-Aufloesung betroffen ist
#### Abgleich zum aktuellen Stand
Status: Im Wesentlichen weiterhin zutreffend.
Auch im aktuellen Stand werden DataAreas zuerst gebaut und die Permission-Groups danach aufgeloest, siehe [`C4IT.LIAM.Ntfs.cs#L225`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L225). Die ACL-Leselogik in [`cActiveDirectoryBase.cs#L88`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L88) verhaelt sich weiterhin so, dass bei ACL-Lesefehlern eher Zusatzinformationen fehlen als ganze DataAreas verschwinden.
Der primaere Discovery-Effekt bleibt also auch heute:
- ACL-/Rechteprobleme waehrend der Ordner-Traversierung koennen die Anzahl reduzieren
- spaetere ACL-/Gruppenmapping-Probleme reduzieren meist nicht die Anzahl
Offenes ToDo:
- ACL-Lesefehler pro DataArea als Diagnosehinweis sichtbar machen
- Discovery und Permission-Mapping im Ergebnis klarer trennen
### 7. Mittel: `Depth = 0` ist im initial-Stand fachlich fehlerhaft
Im `initial`-Stand liefert `RequestFoldersListAsync(string rootPath, int depth)` bei `depth == 0` `null`.
Dadurch wird spaeter im Provider `DAL == null` als Fehler interpretiert und das bereits erzeugte Root-Objekt geht verloren.
Das ist ein echter Bug im Altstand.
Fuer die vorliegende Rueckfrage ist er aber nur dann relevant, wenn die betroffene Konfiguration wirklich `MaxDepth = 0` verwendet haette.
Da Konfigurationsaenderungen hier bewusst ausgeschlossen werden sollen, ist dieser Punkt eher nachrangig, aber als Code-Auffaelligkeit im `initial`-Stand trotzdem wichtig.
#### Abgleich zum aktuellen Stand
Status: Gefixt.
Im aktuellen Stand wird das Root-Objekt zuerst erzeugt und bei `Depth == 0` direkt zurueckgegeben, siehe [`C4IT.LIAM.Ntfs.cs#L171`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L171) bis [`C4IT.LIAM.Ntfs.cs#L180`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L180).
Das alte `initial`-Problem, dass das Root-Objekt bei `Depth = 0` verloren geht, besteht damit im aktuellen Stand nicht mehr.
## Bewertung der konkret genannten Umweltfaktoren fuer initial
### Aenderungen an der Ordnerstruktur
Sehr plausibel.
Im `initial`-Stand wird direkt rekursiv unterhalb von `RootPath` gelesen. Wenn sich die effektive Struktur des Teilbaums geaendert hat, aendert sich die Anzahl der DataAreas unmittelbar.
### Aenderungen an der Rechtesituation auf Ordnern oder Teilbaeumen
Sehr plausibel.
Das ist sogar einer der staerksten Kandidaten, weil die Rekursion bei Fehlern nicht klar scheitert, sondern mit Teilergebnissen weiterarbeitet.
Wenn der technische Benutzer bestimmte Unterordner nicht mehr listen kann, koennen ganze Teilbaeume verschwinden, ohne dass das Ergebnis als Fehler gekennzeichnet wird.
### Aenderungen an ACLs
Teilweise plausibel.
Wenn ACL-Aenderungen die Ordner-Traversierung selbst beeinflussen, dann ja, sehr plausibel.
Wenn ACL-Aenderungen nur die spaetere Permission-Group-Aufloesung betreffen, dann eher nicht als Erklaerung fuer `nur noch 3`.
### Aenderungen an AD-Rechten oder AD-Erreichbarkeit
Eher als Primaerursache unplausibel fuer dieses konkrete Muster.
Im `initial`-Stand fuehren ernsthafte AD-Probleme eher zum kompletten Logon-Fehler als zu einer kleinen Restliste.
### Aenderungen an DFS-Erreichbarkeit
Plausibel, aber indirekt.
Im `initial`-Stand wirkt DFS nur ueber den UNC-Pfad und dessen praktische Erreichbarkeit. Wenn ein DFS-Pfad nicht mehr stabil oder nicht mehr gleich aufgeloest wird, kann das Lesen des Dateisystembaums reduziert oder unvollstaendig werden.
## Warum man im initial-Log wenig sieht
Der `initial`-Stand hat mehrere Diagnoseprobleme gleichzeitig:
- Workflow-seitig gibt es nur pauschale Aussagen wie `No data areas found`
- in der Ordnerrekursion gibt es ein stilles `catch` ohne strukturierte Diagnose
- es gibt keine Statistik ueber erkannte, verworfene oder fehlgeschlagene Unterbaeume
- es gibt keine Kennzeichnung von Teilergebnissen
Dadurch kann ein fachlich stark reduziertes Ergebnis technisch wie ein normaler erfolgreicher Call aussehen.
## Moegliche Fixes fuer den initial-Stand
### 1. Rekursive Traversierung darf Fehler nicht still in Teilergebnisse umwandeln
Empfohlene Aenderung:
- Das innere `catch` in `privRequestFoldersListAsync()` sollte nicht kommentarlos `folders` zurueckgeben.
- Stattdessen muss mindestens geloggt werden, an welchem Pfad die Traversierung abgebrochen ist.
- Besser waere ein Ergebnisobjekt, das `Success`, `PartialResult` und `FailedPaths` sauber trennt.
Nutzen:
- Berechtigungs- oder Infrastrukturfehler auf Teilbaeumen werden endlich sichtbar.
- Der Unterschied zwischen `wirklich nur 3 vorhanden` und `nur 3 erreichbar` wird nachvollziehbar.
### 2. Teilergebnisse im Workflow explizit kennzeichnen
Empfohlene Aenderung:
- Workflow-seitig nicht nur `null` oder `Count <= 0` behandeln.
- Discovery-Ergebnis um Diagnosedaten erweitern, etwa:
- `IsPartial`
- `FailedPathCount`
- `LastTraversalError`
Nutzen:
- Die Workflow-Schicht bekommt eine belastbare Grundlage fuer Support und Betrieb.
### 3. RootPath und Traversierungsstatistik mitloggen
Empfohlene Aenderung:
Pro Call mindestens loggen:
- `RootPath`
- `Depth`
- Anzahl direkt gefundener Ordner
- Anzahl rekursiv gefundener Ordner
- Anzahl abgebrochener Teilbaeume
Nutzen:
- Schon ein einzelner Logauszug reicht spaeter fuer eine deutlich bessere Einordnung.
### 4. AD-Login fuer reine Discovery vom ACL-/Gruppenmapping entkoppeln
Empfohlene Aenderung:
- Fuer die reine Ordnerdiscovery sollte die NTFS-Erreichbarkeit ausschlaggebend sein.
- AD-Fehler sollten die spaetere Zusatzdatenaufloesung beeintraechtigen koennen, aber nicht zwingend die gesamte Discovery blockieren.
Nutzen:
- Reine AD-Stoerungen fuehren nicht mehr automatisch dazu, dass Discovery unnoetig komplett scheitert.
- Die Ursachenanalyse wird klarer getrennt.
### 5. Diagnosemodus fuer den Dateibaum einfuehren
Empfohlene Aenderung:
- Optionaler Diagnosemodus, der je Traversierungsebene protokolliert:
- welcher Pfad gelesen wurde
- wie viele Unterordner gefunden wurden
- an welchem Pfad ein Fehler auftrat
Nutzen:
- Gerade fuer sporadische Infrastrukturprobleme ist das deutlich hilfreicher als das bisherige allgemeine Fehlerbild.
## Praktische Pruefungen fuer den betroffenen initial-Fall
Wenn der Fehler im `initial`-Stand analysiert werden soll, sind diese Pruefungen am wahrscheinlichsten zielfuehrend:
1. Den exakten `RootPath` der betroffenen Konfiguration pruefen.
2. Mit dem technischen Benutzer denselben UNC-Pfad ausserhalb des Workflows rekursiv lesen.
3. Pruefen, ob auf tieferen Teilbaeumen Leserechte oder Traversierungsrechte geaendert wurden.
4. Falls der `RootPath` ein DFS-Pfad ist, pruefen, ob dieser im betroffenen Zeitraum noch auf dasselbe Ziel und mit derselben Erreichbarkeit aufgeloest wurde.
5. AD-Probleme nur dann priorisieren, wenn es Indizien fuer generelle Logon-Fehler oder komplett fehlgeschlagene Providerinitialisierung gibt.
## Schlussbewertung
Nur fuer den Stand `initial` betrachtet, sind die wahrscheinlichsten Ursachen fuer `statt hunderten nur noch 3 DataAreas`:
- geaenderte sichtbare Ordnerstruktur
- geaenderte Leserechte auf Unterordnern oder Teilbaeumen
- indirekte DFS-/UNC-Erreichbarkeitsprobleme auf dem verwendeten Root-Pfad
Deutlich weniger wahrscheinlich als primaere Ursache sind:
- reine AD-Rechteprobleme
- reine ACL-/Gruppenmapping-Probleme nach erfolgreicher Discovery
Die wichtigste technische Auffaelligkeit im `initial`-Stand ist, dass Fehler in der Ordnerrekursion still zu Teilergebnissen werden koennen. Genau das kann das beobachtete Verhalten sehr gut erklaeren, ohne dass im Log ein klarer Fehler sichtbar wird.

View File

@@ -0,0 +1,440 @@
# LIAM WF GetDataAreas - Auffaelligkeiten und moegliche Fixes
## Ziel des Dokuments
Dieses Dokument fasst die Untersuchung zum Verhalten von `WF Get DataAreas` zusammen, wenn in einer betroffenen NTFS-/DFS-Situation ploetzlich nur noch sehr wenige Datenbereiche, beispielsweise `3`, statt vorher hunderten Eintraegen zurueckgeliefert werden.
Der Fokus liegt bewusst nicht auf geaenderter `MaxDepth` oder geaenderten Naming-Conventions. Diese Faktoren wurden fuer die Bewertung zunaechst ausgeklammert. Stattdessen betrachtet dieses Dokument vor allem:
- geaenderte Ordnerstruktur
- geaenderte DFS- oder Share-Struktur
- geaenderte Rechte auf Shares, Ordnern oder ACLs
- geaenderte Erreichbarkeit von DFS-, Fileserver- oder AD-Systemen
- Logging-Luecken und stille Fehlerbilder
- moegliche fachliche und technische Fixes
## Kurzfazit
Das auffaelligste technische Risiko liegt im aktuellen NTFS-Provider und nicht in der Workflow-Huelle selbst.
Im Vergleich zum `initial`-Stand wurde das Verhalten des NTFS-Providers relevant geaendert. Der Provider klassifiziert den `RootPath` heute zuerst semantisch und traversiert danach unterschiedlich fuer Server-Roots, klassische Shares, DFS-Roots, DFS-Links und Folder.
Dadurch koennen sich Ergebnisanzahlen aendern, obwohl `WF Get DataAreas` selbst unveraendert aufgerufen wird.
Fuer das geschilderte Fehlerbild sind insbesondere drei Ursachen realistisch:
- Der konfigurierte oder effektiv aufgeloeste Root wird heute anders klassifiziert als frueher, beispielsweise als Server-Root oder DFS-Root.
- Die DFS-/Share-Ermittlung liefert aufgrund von Umgebungsaenderungen oder Erreichbarkeitsproblemen weniger Kinder.
- Die rekursive Ordnerermittlung laeuft bei Zugriffsproblemen nur noch teilweise durch und gibt still Teilergebnisse zurueck.
AD-Probleme koennen ebenfalls eine Rolle spielen, sind fuer genau das Muster `es kommen noch 3 Eintraege zurueck` aber eher nachrangig. Totale AD-Ausfaelle fuehren im aktuellen Code eher zu einem harten Fehlschlag oder `null`, nicht zu einer kleinen aber formal gueltigen Trefferliste.
## Umfang der Pruefung
Geprueft wurden insbesondere diese Komponenten:
- `LiamWorkflowActivities/C4IT.LIAM.WorkflowactivityBase.cs`
- `LiamWorkflowActivities/LiamWorkflowRuntime.cs`
- `LiamNtfs/C4IT.LIAM.Ntfs.cs`
- `LiamNtfs/cNtfsBase.cs`
- `LiamNtfs/cActiveDirectoryBase.cs`
- `LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs`
Zum Vergleich wurde auch der `initial`-Stand betrachtet, insbesondere das damalige Verhalten in `LiamNtfs/C4IT.LIAM.Ntfs.cs`.
## Ausgangsbeobachtung
Workflow-seitig wird fuer den Provideraufbau im Wesentlichen nur die Provider-Konfiguration aus Matrix42 gelesen und danach `provider.getDataAreasAsync(provider.MaxDepth)` aufgerufen.
Relevante Stellen:
- [`C4IT.LIAM.WorkflowactivityBase.cs#L252`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/C4IT.LIAM.WorkflowactivityBase.cs#L252)
- [`C4IT.LIAM.WorkflowactivityBase.cs#L260`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/C4IT.LIAM.WorkflowactivityBase.cs#L260)
- [`LiamWorkflowRuntime.cs#L66`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/LiamWorkflowRuntime.cs#L66)
- [`LiamWorkflowRuntime.cs#L78`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/LiamWorkflowRuntime.cs#L78)
Die Workflow-Schicht selbst loggt an dieser Stelle weder die Root-Klassifikation noch die Anzahl der gefundenen DataAreas noch Unterschiede zwischen Vollergebnis und Teilergebnis. Das erklaert bereits, warum im Workflow-Log wenig bis nichts Auffaelliges sichtbar sein kann.
## Relevante Verhaltensaenderung gegenueber `initial`
### Alter Stand
Im `initial`-Stand war die Logik im NTFS-Provider deutlich einfacher.
Der Provider hat:
- aus der UNC-Pfadtiefe abgeleitet, ob Root eher Share oder Folder ist
- danach direkt `RequestFoldersListAsync(this.RootPath, Depth)` aufgerufen
- die gelieferten Folder stumpf in `DataAreas` uebernommen
Es gab also keine explizite DFS-/Share-/Server-Root-Klassifikation.
### Aktueller Stand
Heute wird der Root-Pfad zuerst klassifiziert und danach je nach Typ unterschiedlich behandelt.
Relevante Stellen:
- [`C4IT.LIAM.Ntfs.cs#L155`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L155)
- [`C4IT.LIAM.Ntfs.cs#L172`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L172)
- [`C4IT.LIAM.Ntfs.cs#L225`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L225)
- [`C4IT.LIAM.Ntfs.cs#L285`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L285)
- [`C4IT.LIAM.Ntfs.cs#L363`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L363)
Dazu passen die Commits:
- `d28cfe0 Classify NTFS and DFS data areas`
- `f14d4ec Classify NTFS paths via DFS metadata`
- `9cfd266 Support NTFS server roots`
Diese Aenderungen sind fachlich sinnvoll, aber sie fuehren dazu, dass identische oder aehnliche Konfigurationen auf Veraenderungen in DFS-, Share- oder Zugriffsmetadaten viel staerker reagieren als frueher.
## Priorisierte Auffaelligkeiten
### 1. Hoch: DFS- oder Share-Struktur kann die Anzahl der DataAreas direkt veraendern
Der aktuelle Provider behandelt einen Server-Root anders als einen klassischen Share und anders als einen DFS-Root.
Bei einem Server-Root werden die Kinder nicht ueber Dateisystemordner gelesen, sondern ueber veroefentlichte Shares des Servers.
Relevante Stellen:
- [`C4IT.LIAM.Ntfs.cs#L369`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L369)
- [`C4IT.LIAM.Ntfs.cs#L411`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L411)
- [`C4IT.LIAM.Ntfs.cs#L530`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L530)
Bei einem DFS-Pfad wird zunaechst versucht, ueber DFS-Metadaten den Pfad semantisch einzuordnen.
Relevante Stellen:
- [`C4IT.LIAM.Ntfs.cs#L309`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L309)
- [`C4IT.LIAM.Ntfs.cs#L441`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L441)
- [`cNetworkConnection.cs#L104`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L104)
Fachliche Folge:
- Wenn sich DFS-Links, Namespace-Struktur oder veröffentlichte Shares geaendert haben, aendert sich die Ergebnisliste unmittelbar.
- Wenn heute beispielsweise nur noch zwei direkte Share-/Link-Kinder sichtbar sind, ist ein Ergebnis wie `3` plausibel: ein Root-Objekt plus zwei Kinder.
### 2. Hoch: Zugriffsprobleme auf Unterordner fuehren zu stillen Teilergebnissen
Die rekursive Ordnerermittlung in `cNtfsBase` hat ein besonders kritisches Verhalten.
Wenn waehrend der Rekursion eine Exception auftritt, wird im inneren `catch` einfach die bis dahin gesammelte Teilmenge zurueckgegeben. Diese Situation wird an genau der entscheidenden Stelle nicht geloggt.
Relevante Stellen:
- [`cNtfsBase.cs#L108`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L108)
- [`cNtfsBase.cs#L143`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L143)
- [`cNtfsBase.cs#L154`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L154)
- [`cNtfsBase.cs#L174`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L174)
Das ist fuer das geschilderte Fehlerbild besonders relevant.
Wenn sich Berechtigungen auf Teilbaeumen geaendert haben, kann die Traversierung bestimmte Aeste nicht mehr vollstaendig aufloesen. Der Call endet dann nicht zwangslaeufig mit einem Fehler, sondern mit einem kleineren, formal gueltigen Resultset.
Genau das passt zu Aussagen wie:
- frueher hunderte Eintraege
- jetzt noch wenige Eintraege
- im Log nichts Eindeutiges sichtbar
### 3. Hoch: Share-Ermittlung kann still auf wenige oder keine Treffer kollabieren
Die Share-Ermittlung erfolgt ueber `NetShareEnum`.
Relevante Stelle:
- [`cNetworkConnection.cs#L75`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L75)
Wenn `NetShareEnum` einen Fehlercode liefert, wird daraus kein normaler Exception-Pfad, sondern ein Dummy-Eintrag der Form `ERROR=<code>` erzeugt.
Relevante Stelle:
- [`cNetworkConnection.cs#L97`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L97)
Im Provider werden solche Eintraege spaeter als nicht sichtbare Disk-Shares weggefiltert.
Relevante Stelle:
- [`C4IT.LIAM.Ntfs.cs#L559`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L559)
Fachliche Folge:
- Fehler in der Share-Ermittlung koennen zu einer leeren oder sehr kleinen Kindliste fuehren.
- Das muss nicht als harter Fehler im Workflow auftauchen.
- Eine geaenderte Rechte- oder Netzsituation auf dem Fileserver kann also direkt die Anzahl der DataAreas beeinflussen.
### 4. Mittel-Hoch: DFS-Metadatenprobleme koennen still zu anderer Klassifikation fuehren
Die Methode `TryGetDfsEntryPath()` liefert bei ausbleibender DFS-Aufloesung schlicht `false`.
Relevante Stellen:
- [`C4IT.LIAM.Ntfs.cs#L455`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L455)
- [`cNetworkConnection.cs#L113`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L113)
Dabei wird ein normaler Fehlercode des Windows-APIs nicht als fachlicher Fehler an den Provideraufrufer weitergereicht. Der Provider interpretiert das schlicht als `kein DFS-Treffer` und faellt auf andere Heuristiken zurueck.
Moegliche Folge:
- derselbe UNC-Pfad wird je nach DFS-Verfuegbarkeit unterschiedlich klassifiziert
- dadurch aendert sich die Kinderermittlung
- dadurch aendert sich die Anzahl der DataAreas
### 5. Mittel: AD-Ausfaelle sind eher kein gutes Match fuer `nur noch 3`
Der NTFS-Provider-Login verlangt sowohl NTFS- als auch AD-Login.
Relevante Stelle:
- [`C4IT.LIAM.Ntfs.cs#L140`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L140)
Wenn AD komplett nicht erreichbar ist oder die AD-Anmeldung scheitert, passt das eher zu:
- kompletter Fehler
- `null`
- explizitem Logon-Fehler
und weniger zu einem kleinen aber erfolgreichen Ergebnis.
Das bedeutet nicht, dass AD nie beteiligt ist. AD kann in Folgeschritten relevant sein, insbesondere bei ACL- und Gruppenaufloesung. Aber fuer die reine Anzahl der gefundenen DataAreas ist AD nach aktuellem Code eher ein Sekundaerfaktor.
### 6. Mittel: ACL-/Gruppenaufloesung beeinflusst eher die Zusatzinformationen als die Anzahl
Bei der Aufloesung von Permission-Groups werden ACLs gelesen und SID-zu-Gruppe-Aufloesungen versucht.
Relevante Stellen:
- [`C4IT.LIAM.Ntfs.cs#L930`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L930)
- [`cActiveDirectoryBase.cs#L88`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L88)
- [`cActiveDirectoryBase.cs#L95`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cActiveDirectoryBase.cs#L95)
- [`C4IT.LIAM.Ntfs.cs#L945`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT.LIAM.Ntfs.cs#L945)
Wenn das Lesen der ACL fehlschlaegt, wird in `GetAccessControlList()` `null` zurueckgegeben und `ResolvePermissionGroupsAsync()` beendet sich einfach.
Fachliche Folge:
- Die DataArea bleibt im Ergebnis erhalten.
- Es fehlen eher Permission-Gruppen-Zuordnungen.
- Das reduziert typischerweise nicht die Anzahl der DataAreas.
Deshalb ist eine reine ACL-/AD-Gruppenproblematik fuer `nur noch 3` eher weniger passend als ein Struktur- oder Traversierungsproblem.
## Warum im Log oft nichts zu sehen ist
Das aktuelle Logging ist fuer diese Problemklasse zu schwach.
### Workflow-Ebene
`LiamWorkflowRuntime` uebergibt den Call und verarbeitet nur Erfolgs- oder Fehlerstatus.
Relevante Stelle:
- [`LiamWorkflowRuntime.cs#L76`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamWorkflowActivities/LiamWorkflowRuntime.cs#L76)
Es werden nicht geloggt:
- klassifizierter Root-Typ
- Anzahl der direkten Kinder
- Anzahl der rekursiv gefundenen Folder
- Unterschied zwischen Vollergebnis und Teilergebnis
- DFS-/Share-spezifische API-Rueckgaben
### NTFS-Ebene
Die problematischsten Stellen geben Teilergebnisse zurueck oder filtern Fehler indirekt weg.
Beispiele:
- stilles `catch` in der Ordnerrekursion: [`cNtfsBase.cs#L174`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/cNtfsBase.cs#L174)
- Fehlerpfad ueber `ERROR=<code>` bei `NetShareEnum`: [`cNetworkConnection.cs#L97`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L97)
- stille Rueckgabe `false` bei ausbleibender DFS-Aufloesung: [`cNetworkConnection.cs#L114`](/mnt/c/Workspace/C4IT%20DEV%20LIAM%20WEB%20Service_git/LiamNtfs/C4IT_IAM_SET/cNetworkConnection.cs#L114)
Das erklaert, warum ein auffaellig kleiner Rueckgabewert nicht automatisch einen klaren Fehler im Log erzeugt.
## Bewertung der vom Fachbereich genannten Umweltfaktoren
### Aenderungen an der Ordnerstruktur
Sehr plausibel.
Wenn sich die Struktur unterhalb des Roots oder die sichtbaren direkten Kinder geaendert haben, aendert sich die Zahl der gefundenen DataAreas direkt.
Besonders plausibel ist das, wenn der Root heute semantisch eher auf einen Server-Root, DFS-Root oder einen hoeheren Einstiegspunkt zeigt als frueher.
### Aenderungen an DFS-Systemen
Sehr plausibel.
Wenn DFS-Metadaten anders aufgeloest werden, Links entfernt wurden, Namespace-Targets geaendert wurden oder die DFS-Aufloesung zeitweise nicht funktioniert, kann die aktuelle Klassifikation und damit das Ergebnis sofort anders aussehen.
### Aenderungen an Share-Berechtigungen oder Share-Sichtbarkeit
Sehr plausibel.
Die Share-Ermittlung haengt von der Serverantwort auf `NetShareEnum` ab. Wenn der ausfuehrende Kontext dort heute weniger oder keine vernuenftig auswertbaren Eintraege erhaelt, schrumpft die Kindliste.
### Aenderungen an NTFS-ACLs auf Ordnern
Plausibel.
Wenn die rekursive Traversierung auf bestimmten Unterordnern scheitert, koennen Teilbaeume still aus dem Ergebnis herausfallen. Das ist eines der staerksten erklaerenden Muster fuer `frueher viele, heute wenige`.
### Aenderungen an AD-Rechten oder allgemeiner AD-Erreichbarkeit
Nur eingeschraenkt plausibel.
Totale AD-Ausfaelle wuerden eher zu einem harten Fehlerbild fuehren. Teilprobleme bei SID-/Gruppenaufloesung erklaeren eher fehlende Zusatzinformationen als eine drastisch reduzierte Anzahl von DataAreas.
## Moegliche Fixes
### 1. Traversierungsfehler nicht mehr still als Teilerfolg behandeln
Empfohlene Aenderung:
- Das innere `catch` in `cNtfsBase.privRequestFoldersListAsync()` darf nicht nur die bisherige Teilmenge zurueckgeben.
- Stattdessen sollte ein strukturierter Fehlerzustand gesetzt werden, beispielsweise `TraversalHadErrors`, `LastTraversalException` oder ein Provider-Error-Code.
- Optional kann es zwei Modi geben: `strict` fuer harten Fehler und `best-effort` fuer Teilergebnis mit expliziter Warnung.
Ziel:
Der Aufrufer muss unterscheiden koennen zwischen:
- vollstaendigem Ergebnis
- Teilergebnis wegen Zugriffsfehlern
- komplettem Fehler
### 2. Share-Ermittlungsfehler explizit loggen und propagieren
Empfohlene Aenderung:
- `NetShareEnum`-Fehler nicht ueber `ERROR=<code>` maskieren.
- Stattdessen Fehlercode und Servername explizit loggen.
- Provider-seitig muss ein leerer Share-Satz von einem Share-Ermittlungsfehler unterscheidbar sein.
Ziel:
Eine reduzierte Kindliste wegen echter Share-Abfragefehler soll im Log klar von `es existieren wirklich nur 3 Shares` unterscheidbar sein.
### 3. DFS-Aufloesungsfehler als Diagnoseinformation sichtbar machen
Empfohlene Aenderung:
- Rueckgabecode aus `NetDfsGetInfo` loggen, wenn kein DFS-Treffer aufgeloest werden kann.
- Im Provider zusaetzlich protokollieren, ob ein Pfad als `DfsNamespaceRoot`, `DfsLink`, `ClassicShare`, `ServerRoot` oder `Folder` klassifiziert wurde.
Ziel:
Wenn sich dieselbe Konfiguration in unterschiedlichen Umgebungszustand unterschiedlich verhaelt, muss diese Abzweigung sichtbar sein.
### 4. Ergebnisstatistik pro Call loggen
Empfohlene Aenderung:
Mindestens folgende Werte einmal pro `getDataAreasAsync()` loggen:
- `RootPath`
- klassifizierter Root-Typ
- `Depth`
- Anzahl direkter Kinder
- Anzahl rekursiv gefundener Kinder
- Anzahl verworfener Elemente wegen Fehlern
- Anzahl verworfener Elemente wegen Filterung
Ziel:
Ein einzelner Workflow-Lauf soll spaeter rekonstruierbar sein, ohne tief in Debugging oder Fremdsysteme zu muessen.
### 5. Optionalen Diagnosemodus fuer WF GetDataAreas einfuehren
Empfohlene Aenderung:
- Optionaler `AdditionalConfiguration`-Schalter wie `VerboseDataAreaDiscovery`.
- Wenn aktiviert, loggt der Provider jede Traversierungsstufe und jeden Klassifikationsschritt.
Ziel:
Probleme in produktionsnahen Umgebungen koennen gezielt untersucht werden, ohne permanent sehr viel Logvolumen zu erzeugen.
### 6. Trennung zwischen DataArea-Findung und Permission-Group-Aufloesung schaerfen
Empfohlene Aenderung:
- Fehler bei ACL-/Permission-Group-Aufloesung sollten fachlich getrennt von der eigentlichen DataArea-Findung behandelt werden.
- Im Ergebnis sollte sichtbar sein, ob DataAreas korrekt gefunden wurden, aber Zusatzdaten nur teilweise aufloesbar waren.
Ziel:
AD- und ACL-Probleme sollen nicht indirekt die Ursachenanalyse fuer reine Discovery-Probleme verschleiern.
## Konkrete Pruefungen fuer den betroffenen Fall
### 1. Exakten Root-Pfad des betroffenen Providers pruefen
Wichtig ist der genaue Wert von `GCCTarget`.
Zu unterscheiden ist insbesondere:
- `\\server`
- `\\server\share`
- `\\server\share\folder`
- `\\server\namespace`
- `\\server\namespace\link`
Mit dem aktuellen Provider ist das fachlich nicht gleichwertig.
### 2. Sichtbare Shares fuer den technischen Benutzer pruefen
Pruefen, ob der ausfuehrende Kontext auf dem Zielserver noch dieselben Shares enumerieren kann wie frueher.
Wenn nicht, ist die reduzierte Anzahl der DataAreas kein Workflow-Fehler, sondern ein Discovery-Effekt im NTFS-Provider.
### 3. DFS-Aufloesung im betroffenen Zeitraum pruefen
Pruefen, ob das betroffene System denselben Namespace und dieselben DFS-Links noch aufloesen konnte.
Bereits sporadische DFS-Probleme koennen zu anderer Klassifikation fuehren.
### 4. Rekursive Leserechte auf dem Root und auf tieferen Teilbaeumen pruefen
Pruefen, ob der technische Benutzer auf Unterordnern noch `GetDirectories()` ausfuehren kann.
Wenn das auf einzelnen Aesten nicht mehr geht, sind stille Teilergebnisse mit deutlich weniger DataAreas sehr plausibel.
### 5. Ergebnis gegen einen direkten Dateisystemtest spiegeln
Wenn moeglich, sollte derselbe Benutzerkontext ausserhalb des Workflows den Root-Pfad einmal rekursiv lesen.
Wenn dort ebenfalls nur noch wenige oder nur bestimmte Teilbaeume sichtbar sind, ist die Ursache sehr wahrscheinlich ausserhalb der Workflow-Schicht zu suchen.
## Empfohlene Reihenfolge fuer moegliche Umsetzungen
### Kurzfristig
- Logging fuer Root-Klassifikation und Ergebnisstatistik ergaenzen
- Traversierungsfehler sichtbar machen
- Share- und DFS-API-Fehler explizit loggen
### Mittelfristig
- saubere Provider-Fehlercodes fuer `partial result` einfuehren
- WF-Output um Diagnosehinweise erweitern
- Diagnosemodus fuer Discovery einbauen
### Langfristig
- strikte Trennung zwischen Discovery, ACL-Lesen und Gruppenaufloesung
- klarer fachlicher Vertrag, ob `best-effort` oder `strict discovery` gewuenscht ist
## Schlussbewertung
Wenn `MaxDepth` und Naming-Conventions bewusst ausgeklammert werden, sind fuer das beobachtete Verhalten am wahrscheinlichsten:
- geaenderte DFS-/Share-Struktur
- geaenderte Sichtbarkeit oder Erreichbarkeit von DFS-/Share-Metadaten
- geaenderte NTFS-Leserechte auf Teilbaeumen
Weniger wahrscheinlich als primaere Ursache sind reine AD- oder ACL-Gruppenprobleme. Diese erklaeren eher fehlende Zusatzinformationen als einen Rueckgang von hunderten auf wenige DataAreas.
Die wichtigste technische Auffaelligkeit ist, dass der aktuelle Code mehrere dieser Umweltprobleme nicht als klaren Fehler meldet, sondern in Teilergebnisse oder indirekt gefilterte Trefferlisten uebersetzt. Genau deshalb kann im Log wenig sichtbar sein, obwohl sich das Resultat drastisch geaendert hat.

View File

@@ -0,0 +1,133 @@
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Path
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Test-OrphanedSid {
param(
[Parameter(Mandatory = $true)]
[System.Security.Principal.SecurityIdentifier]$Sid
)
try {
[void]$Sid.Translate([System.Security.Principal.NTAccount])
return $false
}
catch [System.Security.Principal.IdentityNotMappedException] {
return $true
}
}
function Get-TargetDirectories {
param(
[Parameter(Mandatory = $true)]
[string]$RootPath
)
$rootItem = Get-Item -LiteralPath $RootPath
if (-not $rootItem.PSIsContainer) {
throw "Der Pfad '$RootPath' ist kein Ordner."
}
$directories = New-Object System.Collections.Generic.List[System.IO.DirectoryInfo]
$directories.Add([System.IO.DirectoryInfo]$rootItem)
foreach ($directory in Get-ChildItem -LiteralPath $RootPath -Directory -Recurse -Force) {
if (($directory.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
Write-Warning "Ueberspringe Reparse-Point: $($directory.FullName)"
continue
}
$directories.Add([System.IO.DirectoryInfo]$directory)
}
return $directories
}
function Remove-OrphanedAclEntriesFromDirectory {
param(
[Parameter(Mandatory = $true)]
[System.IO.DirectoryInfo]$Directory
)
$acl = Get-Acl -LiteralPath $Directory.FullName
$rules = $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
$orphanedRules = @()
foreach ($rule in $rules) {
$sid = [System.Security.Principal.SecurityIdentifier]$rule.IdentityReference
if (Test-OrphanedSid -Sid $sid) {
$orphanedRules += $rule
}
}
if ($orphanedRules.Count -eq 0) {
return [pscustomobject]@{
Path = $Directory.FullName
CheckedRuleCount = $rules.Count
RemovedRuleCount = 0
RemovedIdentities = @()
Changed = $false
}
}
$removedIdentities = New-Object System.Collections.Generic.List[string]
if ($PSCmdlet.ShouldProcess($Directory.FullName, "Remove $($orphanedRules.Count) orphaned ACL entries")) {
foreach ($rule in $orphanedRules) {
[void]$acl.RemoveAccessRuleSpecific($rule)
$removedIdentities.Add($rule.IdentityReference.Value)
}
Set-Acl -LiteralPath $Directory.FullName -AclObject $acl
}
return [pscustomobject]@{
Path = $Directory.FullName
CheckedRuleCount = $rules.Count
RemovedRuleCount = $orphanedRules.Count
RemovedIdentities = $removedIdentities.ToArray()
Changed = $orphanedRules.Count -gt 0
}
}
$resolvedPath = (Resolve-Path -LiteralPath $Path).Path
$results = New-Object System.Collections.Generic.List[object]
$errorCount = 0
foreach ($directory in Get-TargetDirectories -RootPath $resolvedPath) {
try {
$result = Remove-OrphanedAclEntriesFromDirectory -Directory $directory
$results.Add($result)
if ($result.RemovedRuleCount -gt 0) {
Write-Host ("[{0}] {1} verwaiste ACL-Eintraege in {2}" -f ($(if ($WhatIfPreference) { 'WHATIF' } else { 'OK' })), $result.RemovedRuleCount, $result.Path)
foreach ($identity in $result.RemovedIdentities) {
Write-Host (" - {0}" -f $identity)
}
}
}
catch {
$errorCount++
Write-Warning ("Fehler bei {0}: {1}" -f $directory.FullName, $_.Exception.Message)
}
}
$checkedDirectories = $results.Count
$changedDirectories = @($results | Where-Object { $_.Changed }).Count
$removedRules = ($results | Measure-Object -Property RemovedRuleCount -Sum).Sum
if ($null -eq $removedRules) {
$removedRules = 0
}
Write-Host ''
Write-Host 'Zusammenfassung'
Write-Host ('Gepruefte Ordner : {0}' -f $checkedDirectories)
Write-Host ('Geaenderte Ordner: {0}' -f $changedDirectories)
Write-Host ('Entfernte ACEs : {0}' -f $removedRules)
Write-Host ('Fehler : {0}' -f $errorCount)