Compare commits
43 Commits
d95f2385ac
...
Bruker-Dem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d07728b455 | ||
|
|
ee396e5259 | ||
|
|
1884469419 | ||
|
|
06381a4fa4 | ||
|
|
6ce1e70426 | ||
|
|
01fc0ba877 | ||
|
|
a3c741a50a | ||
|
|
ca15d635d4 | ||
|
|
0e95ddf53a | ||
|
|
dd02459851 | ||
|
|
9d9575c9ef | ||
|
|
eb6f23321d | ||
|
|
e087eb4197 | ||
|
|
b636f454cf | ||
|
|
a81c12e9b7 | ||
|
|
518087289e | ||
|
|
66ce92eadd | ||
|
|
cd3819c9bd | ||
|
|
61dd57cf0c | ||
|
|
3ec73817e8 | ||
|
|
24e10feffc | ||
|
|
2bda1010d1 | ||
|
|
8573698e33 | ||
|
|
837dd0b9ee | ||
|
|
663373092e | ||
|
|
c330627f0f | ||
|
|
7f3415c690 | ||
|
|
f2d1cbb3d8 | ||
|
|
537754f6bc | ||
|
|
ece7fd8e7c | ||
|
|
42f57ed7ba | ||
|
|
865fa577e3 | ||
|
|
9cfd266294 | ||
|
|
f14d4ec2e6 | ||
|
|
e7fc76bf5a | ||
|
|
4850fdb80d | ||
|
|
ba7d0fb600 | ||
|
|
b5981487d7 | ||
|
|
c12978ff5d | ||
|
|
4909c93bef | ||
|
|
55ff17c4b4 | ||
|
|
32021dcfd8 | ||
|
|
d28cfe008c |
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -30,8 +30,10 @@ namespace C4IT.LIAM
|
||||
public enum eLiamDataAreaTypes
|
||||
{
|
||||
Unknown = 0,
|
||||
NtfsServerRoot = 100,
|
||||
NtfsShare = 101,
|
||||
NtfsFolder = 102,
|
||||
DfsNamespaceRoot = 103,
|
||||
MsTeamsTeam = 401,
|
||||
MsTeamsChannel = 402,
|
||||
MsTeamsFolder = 403,
|
||||
|
||||
@@ -132,6 +132,6 @@
|
||||
<!--<Exec Command="xcopy.exe /S ..\LiamMsTeams\bin\Debug\LiamMsTeams.dll bin\Debug" ContinueOnError="false" WorkingDirectory="." />-->
|
||||
</Target>
|
||||
<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>
|
||||
</Project>
|
||||
@@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
||||
|
||||
using C4IT.Logging;
|
||||
using C4IT.Matrix42.ServerInfo;
|
||||
using C4IT_IAM;
|
||||
using C4IT_IAM_Engine;
|
||||
using C4IT_IAM_SET;
|
||||
using LiamNtfs;
|
||||
@@ -30,9 +31,32 @@ namespace C4IT.LIAM
|
||||
|
||||
public class cLiamProviderNtfs : cLiamProviderBase
|
||||
{
|
||||
private enum eNtfsPathKind
|
||||
{
|
||||
Unknown = 0,
|
||||
ServerRoot = 1,
|
||||
ClassicShare = 2,
|
||||
DfsNamespaceRoot = 3,
|
||||
DfsLink = 4,
|
||||
Folder = 5
|
||||
}
|
||||
|
||||
private sealed class cNtfsPathClassification
|
||||
{
|
||||
public string NormalizedPath { get; set; } = string.Empty;
|
||||
public eNtfsPathKind Kind { get; set; } = eNtfsPathKind.Unknown;
|
||||
public string BoundaryPath { 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 int Level { get; set; } = -1;
|
||||
}
|
||||
|
||||
public static Guid nftsModuleId = new Guid("77e213a1-6517-ea11-4881-000c2980fd94");
|
||||
public readonly cNtfsBase ntfsBase = new cNtfsBase();
|
||||
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
|
||||
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;
|
||||
|
||||
@@ -145,55 +169,17 @@ namespace C4IT.LIAM
|
||||
return null;
|
||||
|
||||
var DataAreas = new List<cLiamDataAreaBase>();
|
||||
|
||||
var rootpathSplit = this.RootPath.Split(new string[] { "\\" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
cLiamNtfsShare share = null;
|
||||
cLiamNtfsFolder NtfsRootFolder = null;
|
||||
switch (rootpathSplit.Length)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
return null;
|
||||
case 2:
|
||||
{
|
||||
share = new cLiamNtfsShare(this, new cNtfsResultShare()
|
||||
{
|
||||
DisplayName = rootpathSplit.Last(),
|
||||
Path = RootPath,
|
||||
Level = 0
|
||||
});
|
||||
await share.ResolvePermissionGroupsAsync(share.TechnicalName);
|
||||
DataAreas.Add(share);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
NtfsRootFolder = new cLiamNtfsFolder(this, null, null, new cNtfsResultFolder()
|
||||
{
|
||||
DisplayName = rootpathSplit.Last(),
|
||||
Path = RootPath,
|
||||
Level = 0
|
||||
});
|
||||
await NtfsRootFolder.ResolvePermissionGroupsAsync(NtfsRootFolder.TechnicalName);
|
||||
DataAreas.Add(NtfsRootFolder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var DAL = await ntfsBase.RequestFoldersListAsync(this.RootPath, Depth);
|
||||
if (DAL == null)
|
||||
var rootClassification = ClassifyPath(this.RootPath);
|
||||
var rootDataArea = await BuildDataAreaAsync(rootClassification);
|
||||
if (rootDataArea == null)
|
||||
return null;
|
||||
|
||||
foreach (var Entry in DAL)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Value.DisplayName, this.DataAreaRegEx).Success)
|
||||
continue;
|
||||
DataAreas.Add(rootDataArea);
|
||||
|
||||
if (Depth == 0)
|
||||
return DataAreas;
|
||||
|
||||
var Folder = new cLiamNtfsFolder(this, share, NtfsRootFolder, (cNtfsResultFolder)Entry.Value);
|
||||
await Folder.ResolvePermissionGroupsAsync(Folder.TechnicalName);
|
||||
DataAreas.Add(Folder);
|
||||
}
|
||||
DataAreas.AddRange(await GetChildDataAreasAsync(rootClassification, Depth));
|
||||
return DataAreas;
|
||||
}
|
||||
catch (Exception E)
|
||||
@@ -209,7 +195,6 @@ namespace C4IT.LIAM
|
||||
}
|
||||
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
|
||||
{
|
||||
//TODO implement LoadDataArea
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
@@ -222,36 +207,8 @@ namespace C4IT.LIAM
|
||||
}
|
||||
if (!await LogonAsync())
|
||||
return null;
|
||||
var splt = UID.Split(System.IO.Path.DirectorySeparatorChar);
|
||||
var name = Path.GetDirectoryName(UID);
|
||||
switch (splt.Length)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
return null;
|
||||
case 2:
|
||||
{
|
||||
var share = new cLiamNtfsShare(this, new cNtfsResultShare()
|
||||
{
|
||||
DisplayName = name,
|
||||
Path = UID,
|
||||
Level = getDepth(UID)
|
||||
});
|
||||
await share.ResolvePermissionGroupsAsync(share.TechnicalName);
|
||||
return share;
|
||||
}
|
||||
default:
|
||||
{
|
||||
var folder = new cLiamNtfsFolder(this, null, null, new cNtfsResultFolder()
|
||||
{
|
||||
DisplayName = name,
|
||||
Path = UID,
|
||||
Level = getDepth(UID)
|
||||
});
|
||||
await folder.ResolvePermissionGroupsAsync(folder.TechnicalName);
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
var classification = ClassifyPath(UID);
|
||||
return await BuildDataAreaAsync(classification);
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
@@ -265,6 +222,355 @@ namespace C4IT.LIAM
|
||||
|
||||
}
|
||||
|
||||
private async Task<cLiamDataAreaBase> BuildDataAreaAsync(cNtfsPathClassification classification, cNtfsResultFolder folderResult = null)
|
||||
{
|
||||
if (classification == null)
|
||||
return null;
|
||||
|
||||
switch (classification.Kind)
|
||||
{
|
||||
case eNtfsPathKind.ServerRoot:
|
||||
{
|
||||
return new cLiamNtfsServerRoot(this, classification.NormalizedPath, classification.Level);
|
||||
}
|
||||
case eNtfsPathKind.ClassicShare:
|
||||
case eNtfsPathKind.DfsLink:
|
||||
{
|
||||
var share = new cLiamNtfsShare(this, new cNtfsResultShare()
|
||||
{
|
||||
DisplayName = classification.DisplayName,
|
||||
Path = classification.NormalizedPath,
|
||||
Level = classification.Level
|
||||
}, classification.ParentBoundaryPath);
|
||||
await share.ResolvePermissionGroupsAsync(share.TechnicalName);
|
||||
return share;
|
||||
}
|
||||
case eNtfsPathKind.DfsNamespaceRoot:
|
||||
{
|
||||
var namespaceRoot = new cLiamNtfsDfsNamespaceRoot(this, new cNtfsResultShare()
|
||||
{
|
||||
DisplayName = classification.DisplayName,
|
||||
Path = classification.NormalizedPath,
|
||||
Level = classification.Level
|
||||
});
|
||||
await namespaceRoot.ResolvePermissionGroupsAsync(namespaceRoot.TechnicalName);
|
||||
return namespaceRoot;
|
||||
}
|
||||
case eNtfsPathKind.Folder:
|
||||
{
|
||||
var folderData = folderResult ?? new cNtfsResultFolder()
|
||||
{
|
||||
DisplayName = classification.DisplayName,
|
||||
Path = classification.NormalizedPath,
|
||||
Level = classification.Level,
|
||||
CreatedDate = Directory.Exists(classification.NormalizedPath)
|
||||
? new DirectoryInfo(classification.NormalizedPath).CreationTimeUtc.ToString("s")
|
||||
: DateTime.MinValue.ToString("s")
|
||||
};
|
||||
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);
|
||||
return folder;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private cNtfsPathClassification ClassifyPath(string path)
|
||||
{
|
||||
var normalizedPath = NormalizeUncPath(path);
|
||||
var segments = GetUncSegments(normalizedPath);
|
||||
var classification = new cNtfsPathClassification()
|
||||
{
|
||||
NormalizedPath = normalizedPath,
|
||||
DisplayName = GetDisplayName(normalizedPath),
|
||||
Level = getDepth(normalizedPath)
|
||||
};
|
||||
|
||||
if (segments.Length == 1)
|
||||
{
|
||||
classification.Kind = eNtfsPathKind.ServerRoot;
|
||||
return classification;
|
||||
}
|
||||
|
||||
if (segments.Length < 2)
|
||||
return classification;
|
||||
|
||||
classification.ParentPath = segments.Length > 2
|
||||
? BuildUncPath(segments, segments.Length - 1)
|
||||
: string.Empty;
|
||||
|
||||
var dfsPrefixes = GetDfsObjectPrefixes(normalizedPath);
|
||||
if (dfsPrefixes.Count > 0)
|
||||
{
|
||||
var namespaceRootPath = dfsPrefixes[0];
|
||||
var deepestDfsPath = dfsPrefixes[dfsPrefixes.Count - 1];
|
||||
|
||||
if (PathsEqual(normalizedPath, namespaceRootPath))
|
||||
{
|
||||
classification.Kind = eNtfsPathKind.DfsNamespaceRoot;
|
||||
classification.BoundaryPath = normalizedPath;
|
||||
return classification;
|
||||
}
|
||||
|
||||
if (PathsEqual(normalizedPath, deepestDfsPath))
|
||||
{
|
||||
classification.Kind = eNtfsPathKind.DfsLink;
|
||||
classification.BoundaryPath = deepestDfsPath;
|
||||
classification.ParentBoundaryPath = dfsPrefixes.Count > 1
|
||||
? dfsPrefixes[dfsPrefixes.Count - 2]
|
||||
: namespaceRootPath;
|
||||
return classification;
|
||||
}
|
||||
|
||||
classification.Kind = eNtfsPathKind.Folder;
|
||||
classification.BoundaryPath = deepestDfsPath;
|
||||
classification.ParentBoundaryPath = classification.ParentPath;
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return string.Empty;
|
||||
|
||||
var segments = path.Trim().Replace('/', '\\').Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
return @"\\" + string.Join("\\", segments);
|
||||
}
|
||||
|
||||
private string[] GetUncSegments(string path)
|
||||
{
|
||||
var normalized = NormalizeUncPath(path);
|
||||
return normalized.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
private string BuildUncPath(string[] segments, int segmentCount)
|
||||
{
|
||||
if (segments == null || segmentCount <= 0 || segments.Length < segmentCount)
|
||||
return string.Empty;
|
||||
|
||||
return @"\\" + string.Join("\\", segments.Take(segmentCount));
|
||||
}
|
||||
|
||||
private string GetDisplayName(string path)
|
||||
{
|
||||
var segments = GetUncSegments(path);
|
||||
if (segments.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
return segments.Last();
|
||||
}
|
||||
|
||||
private HashSet<string> GetPublishedShareNames(string serverName)
|
||||
{
|
||||
HashSet<string> shares;
|
||||
if (publishedShareCache.TryGetValue(serverName, out shares))
|
||||
return shares;
|
||||
|
||||
shares = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
using (var connection = new cNetworkConnection(this.RootPath, this.Credential?.Identification, this.Credential?.Secret))
|
||||
{
|
||||
foreach (var share in connection.EnumNetShares(serverName))
|
||||
{
|
||||
if (!IsVisibleDiskShare(share))
|
||||
continue;
|
||||
|
||||
shares.Add(share.shi1_netname);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogException(ex);
|
||||
}
|
||||
|
||||
publishedShareCache[serverName] = shares;
|
||||
return shares;
|
||||
}
|
||||
|
||||
private bool IsVisibleDiskShare(C4IT_IAM.SHARE_INFO_1 share)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(share.shi1_netname))
|
||||
return false;
|
||||
|
||||
if (share.shi1_netname.StartsWith("ERROR=", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (share.shi1_netname.EndsWith("$", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var shareType = share.shi1_type & 0xFF;
|
||||
return shareType == (uint)C4IT_IAM.SHARE_TYPE.STYPE_DISKTREE;
|
||||
}
|
||||
|
||||
public override async Task<List<cLiamDataAreaBase>> getSecurityGroupsAsync(string groupFilter)
|
||||
{
|
||||
|
||||
@@ -317,7 +623,8 @@ namespace C4IT.LIAM
|
||||
IDictionary<string, string> customTags,
|
||||
IEnumerable<string> ownerSids,
|
||||
IEnumerable<string> readerSids,
|
||||
IEnumerable<string> writerSids
|
||||
IEnumerable<string> writerSids,
|
||||
bool whatIf = false
|
||||
)
|
||||
{
|
||||
var engine = CreateFilesystemEngine(
|
||||
@@ -327,6 +634,7 @@ namespace C4IT.LIAM
|
||||
ownerSids,
|
||||
readerSids,
|
||||
writerSids);
|
||||
engine.WhatIf = whatIf;
|
||||
var result = engine.createDataArea();
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
@@ -337,8 +645,18 @@ namespace C4IT.LIAM
|
||||
IEnumerable<string> ownerSids,
|
||||
IEnumerable<string> readerSids,
|
||||
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 engine = CreateFilesystemEngine(
|
||||
folderPath,
|
||||
@@ -347,6 +665,7 @@ namespace C4IT.LIAM
|
||||
ownerSids,
|
||||
readerSids,
|
||||
writerSids);
|
||||
engine.WhatIf = whatIf;
|
||||
|
||||
return Task.FromResult(engine.ensureDataAreaPermissions(ensureTraverseGroups));
|
||||
}
|
||||
@@ -393,7 +712,9 @@ namespace C4IT.LIAM
|
||||
groupReadTag = GetRequiredCustomTag("Filesystem_GroupReadTag"),
|
||||
groupTraverseTag = GetRequiredCustomTag("Filesystem_GroupTraverseTag"),
|
||||
groupDLTag = requiresDomainLocalTag ? GetRequiredCustomTag("Filesystem_GroupDomainLocalTag") : string.Empty,
|
||||
groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag")
|
||||
groupGTag = GetRequiredCustomTag("Filesystem_GroupGlobalTag"),
|
||||
CanManagePermissionsForPath = IsPermissionManagedFolderPath,
|
||||
forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames")
|
||||
};
|
||||
|
||||
foreach (var template in BuildSecurityGroupTemplates())
|
||||
@@ -402,6 +723,25 @@ namespace C4IT.LIAM
|
||||
return engine;
|
||||
}
|
||||
|
||||
private bool IsAdditionalConfigurationEnabled(string key)
|
||||
{
|
||||
if (AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key))
|
||||
return false;
|
||||
|
||||
if (!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);
|
||||
}
|
||||
|
||||
public bool IsPermissionManagedFolderPath(string path)
|
||||
{
|
||||
var classification = ClassifyPath(path);
|
||||
return classification != null && classification.Kind == eNtfsPathKind.Folder;
|
||||
}
|
||||
|
||||
private IEnumerable<IAM_SecurityGroupTemplate> BuildSecurityGroupTemplates()
|
||||
{
|
||||
var templates = new List<IAM_SecurityGroupTemplate>();
|
||||
@@ -499,7 +839,12 @@ namespace C4IT.LIAM
|
||||
}
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@@ -689,7 +1034,7 @@ namespace C4IT.LIAM
|
||||
{
|
||||
private readonly cNtfsResultBase Share = null;
|
||||
|
||||
public cLiamNtfsShare(cLiamProviderNtfs Provider, cNtfsResultBase Share) :
|
||||
public cLiamNtfsShare(cLiamProviderNtfs Provider, cNtfsResultBase Share, string parentPath = null) :
|
||||
base(Provider)
|
||||
{
|
||||
this.Share = Share;
|
||||
@@ -701,6 +1046,8 @@ namespace C4IT.LIAM
|
||||
this.DataType = eLiamDataAreaTypes.NtfsShare;
|
||||
if (Directory.Exists(Share.Path))
|
||||
this.CreatedDate = new DirectoryInfo(Share.Path).CreationTimeUtc.ToString("s");
|
||||
if (!string.IsNullOrWhiteSpace(parentPath))
|
||||
this.ParentUID = cLiamNtfsFolder.GetUniqueDataAreaID(parentPath);
|
||||
}
|
||||
|
||||
internal async Task<List<cLiamDataAreaBase>> getFolders()
|
||||
@@ -746,6 +1093,50 @@ namespace C4IT.LIAM
|
||||
}
|
||||
|
||||
}
|
||||
public class cLiamNtfsDfsNamespaceRoot : cLiamNtfsPermissionDataAreaBase
|
||||
{
|
||||
private readonly cNtfsResultBase NamespaceRoot = null;
|
||||
|
||||
public cLiamNtfsDfsNamespaceRoot(cLiamProviderNtfs Provider, cNtfsResultBase NamespaceRoot) :
|
||||
base(Provider)
|
||||
{
|
||||
this.NamespaceRoot = NamespaceRoot;
|
||||
|
||||
this.DisplayName = NamespaceRoot.Path.Split('\\').Last();
|
||||
this.TechnicalName = NamespaceRoot.Path;
|
||||
this.UID = cLiamNtfsFolder.GetUniqueDataAreaID(NamespaceRoot.Path);
|
||||
this.Level = NamespaceRoot.Level;
|
||||
this.DataType = eLiamDataAreaTypes.DfsNamespaceRoot;
|
||||
if (Directory.Exists(NamespaceRoot.Path))
|
||||
this.CreatedDate = new DirectoryInfo(NamespaceRoot.Path).CreationTimeUtc.ToString("s");
|
||||
}
|
||||
|
||||
public override async Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
|
||||
{
|
||||
await Task.Delay(0);
|
||||
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 new readonly cLiamProviderNtfs Provider = null;
|
||||
@@ -768,7 +1159,7 @@ namespace C4IT.LIAM
|
||||
{
|
||||
public readonly cLiamNtfsShare Share = null;
|
||||
public readonly cLiamNtfsFolder NtfsRootFolder = null;
|
||||
public cLiamNtfsFolder(cLiamProviderNtfs Provider, cLiamNtfsShare share, cLiamNtfsFolder ntfsRootFolder, cNtfsResultFolder NtfsFolder) : base(Provider)
|
||||
public cLiamNtfsFolder(cLiamProviderNtfs Provider, cLiamNtfsShare share, cLiamNtfsFolder ntfsRootFolder, cNtfsResultFolder NtfsFolder, string parentPathOverride = null) : base(Provider)
|
||||
{
|
||||
var ntfsParent = NtfsFolder.Parent;
|
||||
this.NtfsRootFolder = ntfsRootFolder;
|
||||
@@ -779,7 +1170,11 @@ namespace C4IT.LIAM
|
||||
this.Level = NtfsFolder.Level;
|
||||
this.DataType = eLiamDataAreaTypes.NtfsFolder;
|
||||
this.CreatedDate = NtfsFolder.CreatedDate;
|
||||
if (ntfsParent != null)
|
||||
if (!string.IsNullOrWhiteSpace(parentPathOverride))
|
||||
{
|
||||
this.ParentUID = GetUniqueDataAreaID(parentPathOverride);
|
||||
}
|
||||
else if (ntfsParent != null)
|
||||
{
|
||||
this.ParentUID = GetUniqueDataAreaID(ntfsParent.Path);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using C4IT_IAM_Engine;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.DirectoryServices;
|
||||
using System.DirectoryServices.AccountManagement;
|
||||
using System.IO;
|
||||
@@ -51,6 +52,9 @@ namespace C4IT_IAM_SET
|
||||
public ICollection<string> ownerUserSids;
|
||||
public ICollection<string> readerUserSids;
|
||||
public ICollection<string> writerUserSids;
|
||||
public Func<string, bool> CanManagePermissionsForPath;
|
||||
public bool forceStrictAdGroupNames;
|
||||
public bool WhatIf;
|
||||
|
||||
public int ReadACLPermission = 0x200A9;
|
||||
public int WriteACLPermission = 0x301BF;
|
||||
@@ -132,18 +136,7 @@ namespace C4IT_IAM_SET
|
||||
resultToken.resultErrorId = 0;
|
||||
if (checkRequiredVariables().resultErrorId == 0)
|
||||
{
|
||||
newDataArea = new DataArea();
|
||||
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;
|
||||
InitializeFolderContext();
|
||||
try
|
||||
{
|
||||
// ImpersonationHelper.Impersonate(domainName, username, new NetworkCredential("", password).Password, delegate
|
||||
@@ -154,17 +147,20 @@ namespace C4IT_IAM_SET
|
||||
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))
|
||||
{
|
||||
if (checkFolder().resultErrorId == 0)
|
||||
var folderCheckResult = checkFolder();
|
||||
if (folderCheckResult.resultErrorId == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
createADGroups();
|
||||
createADGroups(resultToken);
|
||||
try
|
||||
{
|
||||
resultToken = createFolder();
|
||||
resultToken = MergeResultTokens(resultToken, createFolder());
|
||||
if (resultToken.resultErrorId == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
resultToken = SetTraversePermissions();
|
||||
resultToken = MergeResultTokens(resultToken, SetTraversePermissions());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -172,6 +168,7 @@ namespace C4IT_IAM_SET
|
||||
resultToken.resultMessage = "Fehler beim setzen der Traverserechte \n" + e.Message;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
resultToken.resultErrorId = 30200;
|
||||
@@ -188,7 +185,7 @@ namespace C4IT_IAM_SET
|
||||
}
|
||||
else
|
||||
{
|
||||
resultToken = checkFolder();
|
||||
resultToken = folderCheckResult;
|
||||
}
|
||||
/* },
|
||||
logonType,
|
||||
@@ -219,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()
|
||||
{
|
||||
ResultToken resultToken = new ResultToken(System.Reflection.MethodBase.GetCurrentMethod().ToString());
|
||||
@@ -274,7 +294,8 @@ namespace C4IT_IAM_SET
|
||||
{
|
||||
username = username,
|
||||
domainName = domainName,
|
||||
password = password
|
||||
password = password,
|
||||
ForceStrictAdGroupNames = forceStrictAdGroupNames
|
||||
};
|
||||
}
|
||||
|
||||
@@ -314,6 +335,13 @@ namespace C4IT_IAM_SET
|
||||
|
||||
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();
|
||||
if (traverseResult != null)
|
||||
{
|
||||
@@ -332,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;
|
||||
}
|
||||
}
|
||||
@@ -452,6 +482,22 @@ namespace C4IT_IAM_SET
|
||||
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}");
|
||||
AuthorizationRuleCollection ACLs = null;
|
||||
try
|
||||
@@ -484,13 +530,25 @@ namespace C4IT_IAM_SET
|
||||
var folderName = sanitizedSegments.Length > 0
|
||||
? sanitizedSegments[sanitizedSegments.Length - 1]
|
||||
: Helper.SanitizePathSegment(Path.GetFileName(parent.FullName.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)));
|
||||
var traverseNameTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.NamingTemplate, true, relativePath, sanitizedSegments, folderName);
|
||||
var traverseDescriptionTemplate = Helper.ApplyTemplatePlaceholders(traverseGroupTemplate.DescriptionTemplate, true, relativePath, sanitizedSegments, folderName);
|
||||
var boundedTraverseContext = Helper.GetBoundedAdGroupTemplateContext(
|
||||
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;
|
||||
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}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -588,6 +646,16 @@ namespace C4IT_IAM_SET
|
||||
if (parent.Parent != null)
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist nicht null. Erstelle AD-Gruppe.");
|
||||
if (WhatIf)
|
||||
{
|
||||
resultToken.createdGroups.Add(newTraverseGroup.Name);
|
||||
resultToken.ensuredTraverseGroups.Add(newTraverseGroup.Name);
|
||||
resultToken.warnings.Add($"Traverse-Gruppe würde angelegt werden: {newTraverseGroup.Name}");
|
||||
resultToken.addedAclEntries.Add(newTraverseGroup.Name);
|
||||
parentTraverseAclExists = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
newSecurityGroups.CreateADGroup(groupOUPath, newTraverseGroup, null);
|
||||
@@ -625,6 +693,7 @@ namespace C4IT_IAM_SET
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, "Parent.Parent ist null. Traverse-ACL kann nicht gesetzt werden.");
|
||||
@@ -648,11 +717,14 @@ namespace C4IT_IAM_SET
|
||||
resultToken.skippedAclEntries.Add(parentTraverseGroup.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!WhatIf)
|
||||
{
|
||||
accessControl.AddAccessRule(new FileSystemAccessRule(parentTraverseGroup.Sid,
|
||||
FileSystemRights.Read, InheritanceFlags.None, PropagationFlags.None,
|
||||
AccessControlType.Allow));
|
||||
parent.SetAccessControl(accessControl);
|
||||
}
|
||||
resultToken.addedAclEntries.Add(parentTraverseGroup.Name);
|
||||
}
|
||||
}
|
||||
@@ -668,8 +740,6 @@ namespace C4IT_IAM_SET
|
||||
if (i == lvl)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (currentSecGroup == null)
|
||||
@@ -677,33 +747,18 @@ namespace C4IT_IAM_SET
|
||||
DefaultLogger.LogEntry(LogLevels.Error, "currentSecGroup ist null.");
|
||||
continue;
|
||||
}
|
||||
using (GroupPrincipal groupPrincipal = GroupPrincipal.FindByIdentity(domainContext, currentSecGroup.UID))
|
||||
if (currentSecGroup.Scope != GroupScope.Global)
|
||||
continue;
|
||||
|
||||
if (WhatIf)
|
||||
{
|
||||
if (groupPrincipal == null)
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"GroupPrincipal nicht gefunden für UID: {currentSecGroup.UID}");
|
||||
resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde Mitglied '{currentSecGroup.Name}' erhalten.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentSecGroup.Scope == GroupScope.Global)
|
||||
{
|
||||
try
|
||||
{
|
||||
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}");
|
||||
if (!TryEnsureGlobalGroupMembershipWithRetry(domainContext, parentTraverseGroup, currentSecGroup))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
traverseGroup = parentTraverseGroup;
|
||||
}
|
||||
else
|
||||
@@ -714,9 +769,15 @@ namespace C4IT_IAM_SET
|
||||
{
|
||||
if (!parentTraverseGroup.Members.Contains(traverseGroup))
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"Füge {traverseGroup.DistinguishedName} zur Traverse-Gruppe {parentTraverseGroup.DistinguishedName} hinzu");
|
||||
parentTraverseGroup.Members.Add(traverseGroup);
|
||||
parentTraverseGroup.Save();
|
||||
if (WhatIf)
|
||||
{
|
||||
resultToken.warnings.Add($"Traverse-Gruppe '{parentTraverseGroup.Name}' würde verschachtelte Gruppe '{traverseGroup.Name}' erhalten.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryEnsureNestedTraverseGroupMembershipWithRetry(parentTraverseGroup, traverseGroup))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -727,10 +788,13 @@ namespace C4IT_IAM_SET
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!WhatIf)
|
||||
{
|
||||
parentTraverseGroup.Save();
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"parentTraverseGroup gespeichert: {parentTraverseGroup.Name}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Error, $"Fehler beim Speichern der parentTraverseGroup: {ex.Message}");
|
||||
@@ -770,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()
|
||||
{
|
||||
@@ -832,6 +985,12 @@ namespace C4IT_IAM_SET
|
||||
var directory = new DirectoryInfo(newDataArea.IAM_Folders[0].technicalName);
|
||||
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))
|
||||
{
|
||||
resultToken.warnings.Add($"Keine SID für Gruppe '{currentSecGroup?.Name}' verfügbar.");
|
||||
@@ -851,7 +1010,9 @@ namespace C4IT_IAM_SET
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!WhatIf)
|
||||
DataArea.AddDirectorySecurity(newDataArea.IAM_Folders[0].baseFolder, newDataArea.IAM_Folders[0].technicalName, sid, currentSecGroup.rights, AccessControlType.Allow);
|
||||
|
||||
resultToken.addedAclEntries.Add(currentSecGroup.Name);
|
||||
}
|
||||
|
||||
@@ -909,12 +1070,20 @@ namespace C4IT_IAM_SET
|
||||
else
|
||||
users = null;
|
||||
|
||||
var groupAlreadyExists = newSecurityGroups.GroupAllreadyExisting(newSecurityGroups.IAM_SecurityGroups[i].Name.ToUpper());
|
||||
newSecurityGroups.EnsureADGroup(groupOUPath, newSecurityGroups.IAM_SecurityGroups[i], users);
|
||||
if (groupAlreadyExists)
|
||||
resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
|
||||
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.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);
|
||||
else
|
||||
resultToken.reusedGroups.Add(newSecurityGroups.IAM_SecurityGroups[i].Name);
|
||||
}
|
||||
}
|
||||
catch (Exception E)
|
||||
@@ -945,6 +1114,26 @@ namespace C4IT_IAM_SET
|
||||
}
|
||||
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}");
|
||||
DirectoryInfo newDir = Directory.CreateDirectory(newDataArea.IAM_Folders[0].technicalName);
|
||||
newDataArea.IAM_Folders[0].UID = DataArea.GetUniqueDataAreaID(newDir.FullName);
|
||||
@@ -997,7 +1186,7 @@ namespace C4IT_IAM_SET
|
||||
LogMethodEnd(MethodBase.GetCurrentMethod());
|
||||
}
|
||||
|
||||
private void createADGroups()
|
||||
private void createADGroups(ResultToken resultToken)
|
||||
{
|
||||
LogMethodBegin(MethodBase.GetCurrentMethod());
|
||||
|
||||
@@ -1056,8 +1245,25 @@ namespace C4IT_IAM_SET
|
||||
users = readers;
|
||||
else
|
||||
users = null;
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using C4IT.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -10,6 +11,22 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
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)
|
||||
{
|
||||
return Regex.Replace(str, @"(?<loopTag>{{(?<prefix>[^}]*)(?<loop>LOOP)(?<postfix>[^{]*)}})", loop <= 0 ? "" : "${prefix}" + loop + "${postfix}");
|
||||
@@ -49,6 +66,84 @@ namespace C4IT_IAM_Engine
|
||||
|
||||
return result;
|
||||
}
|
||||
public static BoundedTemplateContext GetBoundedAdGroupTemplateContext(
|
||||
string templateValue,
|
||||
bool allowRelativePath,
|
||||
string defaultRelativePath,
|
||||
string[] sanitizedSegments,
|
||||
string folderName,
|
||||
IDictionary<string, string> replacementTags,
|
||||
int maxLength,
|
||||
string logContext)
|
||||
{
|
||||
var effectiveSegments = (sanitizedSegments ?? Array.Empty<string>()).Where(i => i != null).ToArray();
|
||||
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))
|
||||
@@ -77,5 +172,146 @@ namespace C4IT_IAM_Engine
|
||||
else
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace C4IT_IAM_Engine
|
||||
public string domainName;
|
||||
public string username;
|
||||
public SecureString password;
|
||||
public bool ForceStrictAdGroupNames;
|
||||
|
||||
public List<IAM_SecurityGroup> IAM_SecurityGroups;
|
||||
public string rootUID;
|
||||
@@ -188,16 +189,39 @@ namespace C4IT_IAM_Engine
|
||||
tags.Add("GROUPTYPEPOSTFIX", GroupTypeTag);
|
||||
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)
|
||||
.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)
|
||||
.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)
|
||||
.ToUpper();
|
||||
|
||||
@@ -208,6 +232,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = ownerGlobal.NamingTemplate,
|
||||
description = ownerGlobal.DescriptionTemplate,
|
||||
WildcardPattern = ownerGlobal.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + ownerGlobal.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -221,6 +246,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = writeGlobal.NamingTemplate,
|
||||
description = writeGlobal.DescriptionTemplate,
|
||||
WildcardPattern = writeGlobal.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + writeGlobal.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -234,6 +260,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = readGlobal.NamingTemplate,
|
||||
description = readGlobal.DescriptionTemplate,
|
||||
WildcardPattern = readGlobal.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + readGlobal.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -251,6 +278,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = ownerDL.NamingTemplate,
|
||||
description = ownerDL.DescriptionTemplate,
|
||||
WildcardPattern = ownerDL.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + ownerDL.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -265,6 +293,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = writeDL.NamingTemplate,
|
||||
description = writeDL.DescriptionTemplate,
|
||||
WildcardPattern = writeDL.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + writeDL.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -279,6 +308,7 @@ namespace C4IT_IAM_Engine
|
||||
{
|
||||
Name = readDL.NamingTemplate,
|
||||
description = readDL.DescriptionTemplate,
|
||||
WildcardPattern = readDL.WildcardTemplate,
|
||||
|
||||
technicalName = "CN=" + readDL.NamingTemplate + "," + ouPath,
|
||||
targetTyp = (int)IAM_TargetType.FileSystem,
|
||||
@@ -293,6 +323,7 @@ namespace C4IT_IAM_Engine
|
||||
secGroup.description = secGroup.description.ReplaceLoopTag(0);
|
||||
secGroup.Name = secGroup.Name.ReplaceLoopTag(loop);
|
||||
secGroup.technicalName = secGroup.technicalName.ReplaceLoopTag(loop);
|
||||
secGroup.WildcardPattern = secGroup.WildcardPattern.ReplaceLoopTag(loop);
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group generated: {secGroup.technicalName}");
|
||||
}
|
||||
}
|
||||
@@ -398,6 +429,157 @@ namespace C4IT_IAM_Engine
|
||||
return groupEntry;
|
||||
}
|
||||
|
||||
private DirectoryEntry FindGroupEntryByWildcard(string ouPath, string wildcardPattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(wildcardPattern))
|
||||
return null;
|
||||
|
||||
Regex wildcardRegex;
|
||||
try
|
||||
{
|
||||
wildcardRegex = new Regex(wildcardPattern, RegexOptions.IgnoreCase);
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
cLogManager.DefaultLogger.LogException(E);
|
||||
return null;
|
||||
}
|
||||
|
||||
var basePath = "LDAP://" + domainName;
|
||||
if (!string.IsNullOrWhiteSpace(ouPath))
|
||||
basePath += "/" + ouPath;
|
||||
|
||||
DirectoryEntry entry = new DirectoryEntry
|
||||
{
|
||||
Path = basePath,
|
||||
Username = username,
|
||||
Password = new NetworkCredential("", password).Password,
|
||||
AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
|
||||
};
|
||||
|
||||
DirectorySearcher search = new DirectorySearcher(entry)
|
||||
{
|
||||
Filter = "(objectClass=group)"
|
||||
};
|
||||
search.PageSize = 100000;
|
||||
search.PropertiesToLoad.Add("sAMAccountName");
|
||||
search.PropertiesToLoad.Add("distinguishedName");
|
||||
|
||||
string matchedName = null;
|
||||
string matchedDistinguishedName = null;
|
||||
var matchCount = 0;
|
||||
|
||||
foreach (SearchResult result in search.FindAll())
|
||||
{
|
||||
if (!result.Properties.Contains("sAMAccountName") || result.Properties["sAMAccountName"].Count == 0)
|
||||
continue;
|
||||
|
||||
var samAccountName = result.Properties["sAMAccountName"][0]?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(samAccountName) || !wildcardRegex.IsMatch(samAccountName))
|
||||
continue;
|
||||
|
||||
matchCount++;
|
||||
if (matchCount > 1)
|
||||
{
|
||||
DefaultLogger.LogEntry(LogLevels.Warning, $"Multiple AD groups matched wildcard '{wildcardPattern}' in '{basePath}'. Regex-based reuse is skipped.");
|
||||
search.Dispose();
|
||||
entry.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
matchedName = samAccountName;
|
||||
matchedDistinguishedName = result.Properties.Contains("distinguishedName") && result.Properties["distinguishedName"].Count > 0
|
||||
? result.Properties["distinguishedName"][0]?.ToString()
|
||||
: null;
|
||||
}
|
||||
|
||||
search.Dispose();
|
||||
entry.Dispose();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(matchedDistinguishedName))
|
||||
return null;
|
||||
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"Reusing existing AD group '{matchedName}' via wildcard '{wildcardPattern}'.");
|
||||
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)
|
||||
{
|
||||
secGroup.CreatedNewEntry = false;
|
||||
secGroup.UID = getSID(existingGroup);
|
||||
|
||||
if (existingGroup.Properties.Contains("sAMAccountName") && existingGroup.Properties["sAMAccountName"].Count > 0)
|
||||
secGroup.Name = existingGroup.Properties["sAMAccountName"][0]?.ToString();
|
||||
|
||||
if (existingGroup.Properties.Contains("distinguishedName") && existingGroup.Properties["distinguishedName"].Count > 0)
|
||||
secGroup.technicalName = existingGroup.Properties["distinguishedName"][0]?.ToString();
|
||||
}
|
||||
|
||||
private static bool HasMember(PropertyValueCollection members, string distinguishedName)
|
||||
{
|
||||
foreach (var member in members)
|
||||
@@ -445,18 +627,60 @@ namespace C4IT_IAM_Engine
|
||||
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());
|
||||
try
|
||||
{
|
||||
var existingGroup = FindGroupEntry(secGroup.Name);
|
||||
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 CreateADGroup(ouPath, secGroup, users);
|
||||
|
||||
AddMissingMembers(existingGroup, secGroup, users);
|
||||
var objectid = getSID(existingGroup);
|
||||
secGroup.UID = objectid;
|
||||
ApplyExistingGroup(secGroup, existingGroup);
|
||||
return existingGroup;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
cLogManager.DefaultLogger.LogException(E);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogMethodEnd(MethodBase.GetCurrentMethod());
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -475,6 +699,7 @@ namespace C4IT_IAM_Engine
|
||||
LogMethodBegin(MethodBase.GetCurrentMethod());
|
||||
try
|
||||
{
|
||||
secGroup.CreatedNewEntry = false;
|
||||
if (!GroupAllreadyExisting(secGroup.Name.ToUpper()))
|
||||
{
|
||||
|
||||
@@ -511,6 +736,7 @@ namespace C4IT_IAM_Engine
|
||||
var objectid = SecurityGroups.getSID(ent);
|
||||
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}");
|
||||
secGroup.UID = objectid;
|
||||
secGroup.CreatedNewEntry = true;
|
||||
return ent;
|
||||
}
|
||||
else
|
||||
@@ -518,9 +744,8 @@ namespace C4IT_IAM_Engine
|
||||
DirectoryEntry e = FindGroupEntry(secGroup.Name);
|
||||
if (e == null)
|
||||
return null;
|
||||
var objectid = getSID(e);
|
||||
secGroup.UID = objectid;
|
||||
AddMissingMembers(e, secGroup, users);
|
||||
ApplyExistingGroup(secGroup, e);
|
||||
return e;
|
||||
}
|
||||
return null;
|
||||
@@ -588,6 +813,8 @@ namespace C4IT_IAM_Engine
|
||||
public string UID;
|
||||
public string Parent = "";
|
||||
public string description;
|
||||
public string WildcardPattern;
|
||||
public bool CreatedNewEntry;
|
||||
public List<IAM_SecurityGroup> memberGroups;
|
||||
public string Name;
|
||||
public string technicalName;
|
||||
|
||||
@@ -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")]
|
||||
private static extern int WNetAddConnection2(NetResource netResource,
|
||||
string password, string username, int flags);
|
||||
@@ -123,6 +152,15 @@ namespace C4IT_IAM
|
||||
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)]
|
||||
@@ -205,4 +243,10 @@ namespace C4IT_IAM
|
||||
return shi1_netname;
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct DFS_INFO_1
|
||||
{
|
||||
public string EntryPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace LiamNtfs
|
||||
{
|
||||
public class cNtfsBase
|
||||
{
|
||||
private const int ErrorSessionCredentialConflict = 1219;
|
||||
private const int ErrorNotConnected = 2250;
|
||||
|
||||
private cNtfsLogonInfo privLogonInfo = null;
|
||||
private int scanningDepth;
|
||||
public PrincipalContext adContext = null;
|
||||
@@ -58,15 +61,40 @@ namespace LiamNtfs
|
||||
LogonInfo.UserSecret,
|
||||
LogonInfo.User,
|
||||
0);
|
||||
if(result == 1219)
|
||||
|
||||
if (result == ErrorSessionCredentialConflict)
|
||||
{
|
||||
result = WNetCancelConnection2(LogonInfo.TargetNetworkName,0,true);
|
||||
var originalResult = result;
|
||||
var cancelResult = WNetCancelConnection2(LogonInfo.TargetNetworkName, 0, true);
|
||||
|
||||
if (cancelResult == 0 || cancelResult == ErrorNotConnected)
|
||||
{
|
||||
result = WNetAddConnection2(
|
||||
netResource,
|
||||
LogonInfo.UserSecret,
|
||||
LogonInfo.User,
|
||||
0);
|
||||
|
||||
if (result == 0)
|
||||
return await privLogonAsync(LogonInfo);
|
||||
{
|
||||
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)
|
||||
{
|
||||
throw new Win32Exception(result);
|
||||
throw CreateNtfsLoginException(LogonInfo, result, null, null);
|
||||
}
|
||||
var FSLogon = true;
|
||||
|
||||
@@ -83,6 +111,39 @@ namespace LiamNtfs
|
||||
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()
|
||||
{
|
||||
if (privLogonInfo == null)
|
||||
@@ -143,40 +204,64 @@ namespace LiamNtfs
|
||||
private List<cNtfsResultBase> privRequestFoldersListAsync(DirectoryInfo rootPath, int depth, cNtfsResultFolder parent = null)
|
||||
{
|
||||
ResetError();
|
||||
List<cNtfsResultBase> folders = new List<cNtfsResultBase>();
|
||||
var folders = new List<cNtfsResultBase>();
|
||||
try
|
||||
{
|
||||
var res = new List<cNtfsResultBase>();
|
||||
if (depth == 0)
|
||||
return res;
|
||||
return folders;
|
||||
|
||||
DirectoryInfo[] directories;
|
||||
try
|
||||
{
|
||||
foreach (var directory in rootPath.GetDirectories())
|
||||
directories = rootPath.GetDirectories();
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
cNtfsResultFolder folder = new cNtfsResultFolder()
|
||||
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
|
||||
{
|
||||
folder = new cNtfsResultFolder()
|
||||
{
|
||||
ID = generateUniquId(directory.FullName),
|
||||
Path = directory.FullName,
|
||||
CreatedDate = directory.CreationTimeUtc.ToString("s"),
|
||||
DisplayName = directory.Name,
|
||||
Level = scanningDepth - depth+1,
|
||||
Level = scanningDepth - depth + 1,
|
||||
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);
|
||||
if (depth > 0)
|
||||
if (depth <= 0)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var result = privRequestFoldersListAsync(directory, depth - 1, folder);
|
||||
if (result != null && result.Count > 0)
|
||||
folders.AddRange(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception E)
|
||||
{
|
||||
return new List<cNtfsResultBase>(folders);
|
||||
cLogManager.LogEntry($"Could not scan subtree '{directory.FullName}': {E.Message}", LogLevels.Warning);
|
||||
cLogManager.LogException(E, LogLevels.Debug);
|
||||
}
|
||||
}
|
||||
|
||||
return new List<cNtfsResultBase>(folders);
|
||||
return folders;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>C4IT.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(MSBuildRuntimeType)' == 'Core' ">
|
||||
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
@@ -61,6 +64,11 @@
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
</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>
|
||||
<Compile Include="..\..\Common Code\Configuration\C4IT.Configuration.ConfigHelper.cs">
|
||||
<Link>Common\C4IT.Configuration.ConfigHelper.cs</Link>
|
||||
|
||||
@@ -449,23 +449,18 @@ namespace C4IT.LIAM.Activities
|
||||
|
||||
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();
|
||||
Success.Set(context, result != null);
|
||||
|
||||
if (result?.Result?.targetResourceId != null)
|
||||
{
|
||||
string idString = result.Result.targetResourceId.ToString();
|
||||
if (Guid.TryParse(idString, out Guid teamGuid))
|
||||
{
|
||||
CreatedTeamId.Set(context, teamGuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
var providerEntry = getDataProvider(ConfigID.Get(context));
|
||||
var result = LiamWorkflowRuntime.CloneTeamAsync(
|
||||
providerEntry?.Provider,
|
||||
TeamId.Get(context),
|
||||
Name.Get(context),
|
||||
Description.Get(context),
|
||||
Visibility.Get(context),
|
||||
PartsToClone.Get(context),
|
||||
AdditionalMembers.Get(context),
|
||||
AdditionalOwners.Get(context)).GetAwaiter().GetResult();
|
||||
Success.Set(context, result != null && result.Success);
|
||||
CreatedTeamId.Set(context, result?.CreatedTeamId ?? Guid.Empty);
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
@@ -606,45 +601,17 @@ namespace C4IT.LIAM.Activities
|
||||
ErrorMessage.Set(context, string.Empty);
|
||||
|
||||
var entry = getDataProvider(ConfigID.Get(context));
|
||||
if (entry != null && entry.Provider is cLiamProviderExchange ex)
|
||||
{
|
||||
var result = ex.exchangeManager.CreateDistributionGroupWithOwnershipGroups(
|
||||
var result = LiamWorkflowRuntime.CreateDistributionGroup(
|
||||
entry?.Provider,
|
||||
Name.Get(context),
|
||||
Alias.Get(context),
|
||||
DistributionListDisplayName.Get(context),
|
||||
PrimarySmtpAddress.Get(context),
|
||||
out string errorCode,
|
||||
out string errorMessage
|
||||
);
|
||||
ErrorCode.Set(context, errorCode);
|
||||
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);
|
||||
}
|
||||
PrimarySmtpAddress.Get(context));
|
||||
Success.Set(context, result.Success);
|
||||
ObjectGuid.Set(context, result.ObjectGuid);
|
||||
CreatedGroups.Set(context, result.CreatedGroups);
|
||||
ErrorCode.Set(context, result.ErrorCode);
|
||||
ErrorMessage.Set(context, result.ErrorMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -729,45 +696,17 @@ namespace C4IT.LIAM.Activities
|
||||
ErrorMessage.Set(context, string.Empty);
|
||||
|
||||
var entry = getDataProvider(ConfigID.Get(context));
|
||||
if (entry != null && entry.Provider is cLiamProviderExchange ex)
|
||||
{
|
||||
var result = ex.exchangeManager.CreateSharedMailboxWithOwnershipGroups(
|
||||
var result = LiamWorkflowRuntime.CreateSharedMailbox(
|
||||
entry?.Provider,
|
||||
Name.Get(context),
|
||||
Alias.Get(context),
|
||||
MailboxDisplayName.Get(context),
|
||||
PrimarySmtpAddress.Get(context),
|
||||
out string errorCode,
|
||||
out string errorMessage
|
||||
);
|
||||
ErrorCode.Set(context, errorCode);
|
||||
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);
|
||||
}
|
||||
PrimarySmtpAddress.Get(context));
|
||||
Success.Set(context, result.Success);
|
||||
ObjectGuid.Set(context, result.ObjectGuid);
|
||||
CreatedGroups.Set(context, result.CreatedGroups);
|
||||
ErrorCode.Set(context, result.ErrorCode);
|
||||
ErrorMessage.Set(context, result.ErrorMessage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -892,15 +831,16 @@ namespace C4IT.LIAM.Activities
|
||||
var ownerList = OwnerSids.Expression != null ? OwnerSids.Get(context) : null;
|
||||
var memberList = MemberSids.Expression != null ? MemberSids.Get(context) : null;
|
||||
|
||||
var groups = adProv.CreateServiceGroups(
|
||||
var result = LiamWorkflowRuntime.CreateAdServiceGroups(
|
||||
adProv,
|
||||
svcName,
|
||||
desc,
|
||||
scopeEnum,
|
||||
typeEnum,
|
||||
ownerList,
|
||||
memberList);
|
||||
Success.Set(context, groups != null);
|
||||
CreatedGroups.Set(context, groups);
|
||||
Success.Set(context, result.Success);
|
||||
CreatedGroups.Set(context, result.CreatedGroups);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -937,9 +877,9 @@ namespace C4IT.LIAM.Activities
|
||||
{
|
||||
EnsureDataProviders(context);
|
||||
var cfgId = ConfigID.Get(context);
|
||||
var provider = getDataProvider(cfgId).Provider as cLiamProviderNtfs;
|
||||
// evtl. CustomTags, OwnerSIDs etc. aus Activity-Inputs holen
|
||||
var res = provider.CreateDataAreaAsync(
|
||||
var provider = getDataProvider(cfgId)?.Provider;
|
||||
var result = LiamWorkflowRuntime.CreateDataAreaAsync(
|
||||
provider,
|
||||
NewFolderPath.Get(context),
|
||||
ParentFolderPath.Get(context),
|
||||
/*customTags*/null,
|
||||
@@ -947,7 +887,7 @@ namespace C4IT.LIAM.Activities
|
||||
/*readerSids*/null,
|
||||
/*writerSids*/null
|
||||
).GetAwaiter().GetResult();
|
||||
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(res)));
|
||||
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result.ResultToken)));
|
||||
}
|
||||
private void EnsureDataProviders(NativeActivityContext context)
|
||||
{
|
||||
@@ -1002,45 +942,22 @@ namespace C4IT.LIAM.Activities
|
||||
EnsureDataProviders(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 readerSids = ReaderSids.Expression != null ? ReaderSids.Get(context) : null;
|
||||
var writerSids = WriterSids.Expression != null ? WriterSids.Get(context) : null;
|
||||
|
||||
var result = provider.EnsureMissingPermissionGroupsAsync(
|
||||
folderPath,
|
||||
var providerEntry = getDataProvider(cfgId);
|
||||
var result = LiamWorkflowRuntime.EnsureNtfsPermissionGroupsAsync(
|
||||
providerEntry?.Provider,
|
||||
FolderPath.Get(context),
|
||||
null,
|
||||
NormalizeSidList(ownerSids),
|
||||
NormalizeSidList(readerSids),
|
||||
NormalizeSidList(writerSids),
|
||||
ownerSids,
|
||||
readerSids,
|
||||
writerSids,
|
||||
EnsureTraverse.Get(context)).GetAwaiter().GetResult();
|
||||
|
||||
Success.Set(context, result != null && result.resultErrorId == 0);
|
||||
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result)));
|
||||
}
|
||||
|
||||
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);
|
||||
Success.Set(context, result.Success);
|
||||
ResultToken.Set(context, JsonValue.Parse(JsonConvert.SerializeObject(result.ResultToken)));
|
||||
}
|
||||
|
||||
private void EnsureDataProviders(NativeActivityContext context)
|
||||
|
||||
@@ -435,52 +435,20 @@ namespace LiamWorkflowActivities
|
||||
return null;
|
||||
}
|
||||
|
||||
var lstSecurityGroups = await ProviderEntry.Provider.getSecurityGroupsAsync(ProviderEntry.Provider.GroupFilter);
|
||||
if (lstSecurityGroups == null)
|
||||
var result = await LiamWorkflowRuntime.GetSecurityGroupsFromProviderAsync(ProviderEntry.Provider);
|
||||
if (!result.Success)
|
||||
{
|
||||
SetOperationErrorFromProvider(
|
||||
ProviderEntry.Provider,
|
||||
"WF_GET_SECURITYGROUPS_PROVIDER_CALL_FAILED",
|
||||
"Provider returned null while reading security groups.");
|
||||
SetOperationError(result.ErrorCode, result.ErrorMessage);
|
||||
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);
|
||||
return new List<SecurityGroupEntry>();
|
||||
}
|
||||
|
||||
var SGs = new List<SecurityGroupEntry>();
|
||||
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;
|
||||
return result.SecurityGroups;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
@@ -518,95 +486,22 @@ namespace LiamWorkflowActivities
|
||||
return null;
|
||||
}
|
||||
|
||||
var lstDataAreas = await ProviderEntry.Provider.getDataAreasAsync(ProviderEntry.Provider.MaxDepth);
|
||||
if (lstDataAreas == null)
|
||||
{
|
||||
SetOperationErrorFromProvider(
|
||||
var result = await LiamWorkflowRuntime.GetDataAreasFromProviderAsync(
|
||||
ProviderEntry.Provider,
|
||||
"WF_GET_DATAAREAS_PROVIDER_CALL_FAILED",
|
||||
"Provider returned null while reading data areas.");
|
||||
ProviderEntry.ObjectID.ToString());
|
||||
if (!result.Success)
|
||||
{
|
||||
SetOperationError(result.ErrorCode, result.ErrorMessage);
|
||||
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);
|
||||
return new List<DataAreaEntry>();
|
||||
}
|
||||
|
||||
if (!await EnsureNtfsPermissionGroupsIfConfiguredAsync(ProviderEntry, lstDataAreas))
|
||||
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) Write‑SID
|
||||
// - 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) Read‑SID
|
||||
// - 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();
|
||||
return result.DataAreas;
|
||||
}
|
||||
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<cLiamNtfsPermissionDataAreaBase>())
|
||||
{
|
||||
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)
|
||||
{
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
</Compile>
|
||||
<Compile Include="C4IT.LIAM.WorkflowactivityBase.cs" />
|
||||
<Compile Include="C4IT.LIAM.WorkflowActivities.cs" />
|
||||
<Compile Include="LiamWorkflowRuntime.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
694
LiamWorkflowActivities/LiamWorkflowRuntime.cs
Normal file
694
LiamWorkflowActivities/LiamWorkflowRuntime.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
LiamWorkflowDiagnostics/AppIcon.ico
Normal file
BIN
LiamWorkflowDiagnostics/AppIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
@@ -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>
|
||||
@@ -18,6 +18,9 @@
|
||||
<SccAuxPath>SAK</SccAuxPath>
|
||||
<SccProvider>SAK</SccProvider>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>AppIcon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -95,6 +98,7 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
</ApplicationDefinition>
|
||||
<Resource Include="AppIcon.ico" />
|
||||
<Page Include="MainWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<SubType>Designer</SubType>
|
||||
@@ -116,5 +120,6 @@
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" Condition=" '$(OS)' == 'Windows_NT' " />
|
||||
<Import Project="LiamWorkflowDiagnostics.NonWindows.targets" Condition=" '$(OS)' != 'Windows_NT' " />
|
||||
</Project>
|
||||
@@ -2,6 +2,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="LIAM Workflow Diagnostics" Width="1100"
|
||||
Icon="/AppIcon.ico"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<Grid Margin="12">
|
||||
@@ -454,14 +455,32 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<StackPanel Grid.Row="5" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,0,8">
|
||||
<Button x:Name="LoadJsonButton" Content="Load JSON" Width="110" Margin="0,0,8,0" Click="LoadJsonButton_Click"/>
|
||||
<Button x:Name="ExportJsonButton" Content="Export Sanitized JSON" Width="170" Margin="0,0,8,0" Click="ExportJsonButton_Click"/>
|
||||
<Button x:Name="InitializeButton" Content="Initialize Provider" Width="160" Margin="0,0,8,0" Click="InitializeButton_Click"/>
|
||||
<Button x:Name="FetchDataAreasButton" Content="Fetch Data Areas" Width="160" Margin="0,0,8,0" Click="FetchDataAreasButton_Click"/>
|
||||
<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"/>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="5" Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox x:Name="WhatIfCheckBox"
|
||||
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">
|
||||
<Grid Margin="10">
|
||||
|
||||
@@ -113,6 +113,7 @@ namespace LiamWorkflowDiagnostics
|
||||
MsTeamsVisibilityComboBox.SelectedValue = MsTeamsVisibilityPrivate;
|
||||
MsTeamsCloneSettingsCheckBox.IsChecked = true;
|
||||
MsTeamsCloneChannelsCheckBox.IsChecked = true;
|
||||
WhatIfCheckBox.IsChecked = true;
|
||||
|
||||
FetchDataAreasButton.IsEnabled = false;
|
||||
FetchSecurityGroupsButton.IsEnabled = false;
|
||||
@@ -179,12 +180,13 @@ namespace LiamWorkflowDiagnostics
|
||||
ApplyMatrix42Environment(ServerNameTextBox.Text, UseHttpsCheckBox.IsChecked ?? false);
|
||||
ApplyLicense(LicenseTextBox.Text);
|
||||
|
||||
_session = new ProviderTestSession(msg => AppendLog(msg));
|
||||
var success = await _session.InitializeAsync(providerData, maskToken, CreateProviderInstance, providerConfigClassId, providerConfigObjectId);
|
||||
var session = new ProviderTestSession(msg => AppendLog(msg));
|
||||
_session = session;
|
||||
var success = await Task.Run(() => session.InitializeAsync(providerData, maskToken, CreateProviderInstance, providerConfigClassId, providerConfigObjectId));
|
||||
if (success)
|
||||
{
|
||||
AppendLog("Provider initialisiert und authentifiziert.", LogLevels.Info);
|
||||
ResultTextBox.Text = _session.SanitizedConfigJson;
|
||||
ResultTextBox.Text = session.SanitizedConfigJson;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -240,6 +242,18 @@ namespace LiamWorkflowDiagnostics
|
||||
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)
|
||||
{
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.Control)
|
||||
@@ -327,6 +341,7 @@ namespace LiamWorkflowDiagnostics
|
||||
ServerNameTextBox.Text = settings.ServerName ?? string.Empty;
|
||||
UseHttpsCheckBox.IsChecked = settings.UseHttps;
|
||||
LicenseTextBox.Text = settings.License ?? string.Empty;
|
||||
WhatIfCheckBox.IsChecked = settings.EnableWhatIf;
|
||||
NtfsCreateFolderPathTextBox.Text = settings.NtfsCreateFolderPath ?? string.Empty;
|
||||
NtfsCreateParentPathTextBox.Text = settings.NtfsCreateParentPath ?? string.Empty;
|
||||
NtfsCreateOwnerSidsTextBox.Text = settings.NtfsCreateOwnerSids ?? string.Empty;
|
||||
@@ -405,6 +420,7 @@ namespace LiamWorkflowDiagnostics
|
||||
ServerName = ServerNameTextBox.Text ?? string.Empty,
|
||||
UseHttps = UseHttpsCheckBox.IsChecked ?? false,
|
||||
License = LicenseTextBox.Text ?? string.Empty,
|
||||
EnableWhatIf = WhatIfCheckBox.IsChecked ?? true,
|
||||
GroupStrategy = GroupStrategyCombo.SelectedItem is eLiamGroupStrategies gs ? (int)gs : (int)eLiamGroupStrategies.Ntfs_AGDLP,
|
||||
NtfsCreateFolderPath = NtfsCreateFolderPathTextBox.Text ?? string.Empty,
|
||||
NtfsCreateParentPath = NtfsCreateParentPathTextBox.Text ?? string.Empty,
|
||||
@@ -522,9 +538,13 @@ namespace LiamWorkflowDiagnostics
|
||||
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
|
||||
? providerHint
|
||||
: $"Initialisiere zuerst einen Provider. {providerHint}";
|
||||
? providerHint + modeHint
|
||||
: $"Initialisiere zuerst einen Provider. {providerHint}{modeHint}";
|
||||
}
|
||||
|
||||
private async void FetchDataAreasButton_Click(object sender, RoutedEventArgs e)
|
||||
@@ -537,39 +557,37 @@ namespace LiamWorkflowDiagnostics
|
||||
|
||||
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}) ...");
|
||||
var areas = await _session.Provider.getDataAreasAsync(maxDepth);
|
||||
if (areas == null)
|
||||
{
|
||||
var providerMessage = _session.Provider.GetLastErrorMessage();
|
||||
if (_session.Provider is cLiamProviderExchange exchangeProvider)
|
||||
{
|
||||
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);
|
||||
}
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.GetDataAreasFromProviderAsync(
|
||||
provider,
|
||||
configurationId,
|
||||
runWhatIf));
|
||||
ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
|
||||
|
||||
ResultTextBox.Text = "[]";
|
||||
if (!result.Success)
|
||||
{
|
||||
AppendLog($"DataAreas-Call fehlgeschlagen [{result.ErrorCode}]: {result.ErrorMessage}", LogLevels.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (areas.Count == 0)
|
||||
if (result.DataAreas.Count == 0)
|
||||
{
|
||||
AppendLog("Keine DataAreas gefunden.", LogLevels.Warning);
|
||||
ResultTextBox.Text = "[]";
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = ConvertDataAreas(areas);
|
||||
var json = JsonConvert.SerializeObject(entries, Formatting.Indented);
|
||||
ResultTextBox.Text = json;
|
||||
AppendLog($"DataAreas erhalten: {entries.Count}");
|
||||
if (runWhatIf && result.AutomaticEnsurePreview != null && result.AutomaticEnsurePreview.Count > 0)
|
||||
{
|
||||
AppendLog($"EnsureNtfsPermissionGroups wurde nur simuliert fuer {result.AutomaticEnsurePreview.Count} Ordner. Details stehen im Result-JSON.", LogLevels.Warning);
|
||||
}
|
||||
|
||||
AppendLog($"DataAreas erhalten: {result.DataAreas.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -580,7 +598,7 @@ namespace LiamWorkflowDiagnostics
|
||||
|
||||
private async void ExecuteNtfsCreateButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await ExecuteProviderActionAsync("NTFS Folder Create", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS");
|
||||
var folderPath = GetRequiredText(NtfsCreateFolderPathTextBox.Text, "New Folder Path");
|
||||
@@ -594,41 +612,101 @@ namespace LiamWorkflowDiagnostics
|
||||
var ownerSids = ParseIdentifierList(NtfsCreateOwnerSidsTextBox.Text, "Owner SIDs");
|
||||
if (ownerSids.Count == 0)
|
||||
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 () =>
|
||||
{
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.CreateDataAreaAsync(
|
||||
provider,
|
||||
folderPath,
|
||||
parentPath,
|
||||
ParseKeyValueLines(CustomTagsTextBox.Text, "Custom Tags"),
|
||||
null,
|
||||
ownerSids,
|
||||
ParseIdentifierList(NtfsCreateReaderSidsTextBox.Text, "Reader SIDs"),
|
||||
ParseIdentifierList(NtfsCreateWriterSidsTextBox.Text, "Writer SIDs")),
|
||||
"NTFS Folder Create");
|
||||
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)
|
||||
{
|
||||
await ExecuteProviderActionAsync("NTFS Ensure Groups / ACLs", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderNtfs>("NTFS");
|
||||
var folderPath = GetRequiredText(NtfsEnsureFolderPathTextBox.Text, "Folder Path");
|
||||
var result = await provider.EnsureMissingPermissionGroupsAsync(
|
||||
folderPath,
|
||||
ParseKeyValueLines(CustomTagsTextBox.Text, "Custom Tags"),
|
||||
ParseIdentifierList(NtfsEnsureOwnerSidsTextBox.Text, "Owner SIDs"),
|
||||
ParseIdentifierList(NtfsEnsureReaderSidsTextBox.Text, "Reader SIDs"),
|
||||
ParseIdentifierList(NtfsEnsureWriterSidsTextBox.Text, "Writer SIDs"),
|
||||
NtfsEnsureTraverseCheckBox.IsChecked ?? false);
|
||||
var ownerSids = ParseIdentifierList(NtfsEnsureOwnerSidsTextBox.Text, "Owner SIDs");
|
||||
var readerSids = ParseIdentifierList(NtfsEnsureReaderSidsTextBox.Text, "Reader SIDs");
|
||||
var writerSids = ParseIdentifierList(NtfsEnsureWriterSidsTextBox.Text, "Writer SIDs");
|
||||
var ensureTraverse = 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)
|
||||
{
|
||||
await ExecuteProviderActionAsync("AD Ensure Service Groups", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderAD>("Active Directory");
|
||||
var serviceName = GetRequiredText(AdServiceNameTextBox.Text, "Service Name");
|
||||
@@ -642,7 +720,10 @@ namespace LiamWorkflowDiagnostics
|
||||
var ownerSids = ParseIdentifierList(AdOwnerSidsTextBox.Text, "Owner SIDs");
|
||||
var memberSids = ParseIdentifierList(AdMemberSidsTextBox.Text, "Member SIDs");
|
||||
|
||||
var result = await Task.Run(() => provider.CreateServiceGroups(
|
||||
await ExecuteProviderActionAsync("AD Ensure Service Groups", async () =>
|
||||
{
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.CreateAdServiceGroups(
|
||||
provider,
|
||||
serviceName,
|
||||
description,
|
||||
scope,
|
||||
@@ -650,76 +731,160 @@ namespace LiamWorkflowDiagnostics
|
||||
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)
|
||||
{
|
||||
await ExecuteProviderActionAsync("MsTeams Clone Team", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderMsTeams>("MsTeams");
|
||||
var sourceTeamId = GetRequiredText(MsTeamsSourceTeamIdTextBox.Text, "Source Team ID");
|
||||
var newTeamName = GetRequiredText(MsTeamsNewNameTextBox.Text, "New Team Name");
|
||||
var description = NormalizeOptionalText(MsTeamsDescriptionTextBox.Text);
|
||||
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 () =>
|
||||
{
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.CloneTeamAsync(
|
||||
provider,
|
||||
sourceTeamId,
|
||||
newTeamName,
|
||||
NormalizeOptionalText(MsTeamsDescriptionTextBox.Text),
|
||||
description,
|
||||
visibility,
|
||||
GetSelectedCloneParts(),
|
||||
string.Join(";", ParseIdentifierList(MsTeamsAdditionalMembersTextBox.Text, "Additional Members")),
|
||||
string.Join(";", ParseIdentifierList(MsTeamsAdditionalOwnersTextBox.Text, "Additional Owners")));
|
||||
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)
|
||||
{
|
||||
await ExecuteProviderActionAsync("Exchange Create Shared Mailbox", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange");
|
||||
var name = GetRequiredText(ExchangeMailboxNameTextBox.Text, "Name");
|
||||
var alias = GetRequiredText(ExchangeMailboxAliasTextBox.Text, "Alias");
|
||||
var displayName = NormalizeOptionalText(ExchangeMailboxDisplayNameTextBox.Text);
|
||||
var primarySmtp = NormalizeOptionalText(ExchangeMailboxPrimarySmtpTextBox.Text);
|
||||
var result = await Task.Run(() => provider.exchangeManager.CreateSharedMailboxWithOwnershipGroups(
|
||||
|
||||
await ExecuteProviderActionAsync("Exchange Create Shared Mailbox", async () =>
|
||||
{
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.CreateSharedMailbox(
|
||||
provider,
|
||||
name,
|
||||
alias,
|
||||
displayName,
|
||||
primarySmtp));
|
||||
|
||||
return new
|
||||
return result;
|
||||
}, () =>
|
||||
{
|
||||
ObjectGuid = result.Item1,
|
||||
Groups = MapSecurityGroupResults(result.Item2)
|
||||
};
|
||||
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)
|
||||
{
|
||||
await ExecuteProviderActionAsync("Exchange Create Distribution Group", async () =>
|
||||
try
|
||||
{
|
||||
var provider = EnsureInitializedProvider<cLiamProviderExchange>("Exchange");
|
||||
var name = GetRequiredText(ExchangeDistributionNameTextBox.Text, "Name");
|
||||
var alias = GetRequiredText(ExchangeDistributionAliasTextBox.Text, "Alias");
|
||||
var displayName = NormalizeOptionalText(ExchangeDistributionDisplayNameTextBox.Text);
|
||||
var primarySmtp = NormalizeOptionalText(ExchangeDistributionPrimarySmtpTextBox.Text);
|
||||
var result = await Task.Run(() => provider.exchangeManager.CreateDistributionGroupWithOwnershipGroups(
|
||||
|
||||
await ExecuteProviderActionAsync("Exchange Create Distribution Group", async () =>
|
||||
{
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.CreateDistributionGroup(
|
||||
provider,
|
||||
name,
|
||||
alias,
|
||||
displayName,
|
||||
primarySmtp));
|
||||
|
||||
return new
|
||||
return result;
|
||||
}, () =>
|
||||
{
|
||||
ObjectGuid = result.Item1,
|
||||
Groups = MapSecurityGroupResults(result.Item2)
|
||||
};
|
||||
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)
|
||||
@@ -732,38 +897,25 @@ namespace LiamWorkflowDiagnostics
|
||||
|
||||
try
|
||||
{
|
||||
AppendLog($"Lese SecurityGroups (Filter='{_session.Provider.GroupFilter}') ...");
|
||||
var groups = await _session.Provider.getSecurityGroupsAsync(_session.Provider.GroupFilter);
|
||||
if (groups == null)
|
||||
{
|
||||
var providerMessage = _session.Provider.GetLastErrorMessage();
|
||||
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);
|
||||
}
|
||||
var provider = _session.Provider;
|
||||
var filter = provider.GroupFilter;
|
||||
AppendLog($"Lese SecurityGroups (Filter='{filter}') ...");
|
||||
var result = await Task.Run(() => LiamWorkflowRuntime.GetSecurityGroupsFromProviderAsync(provider));
|
||||
ResultTextBox.Text = JsonConvert.SerializeObject(result, Formatting.Indented);
|
||||
|
||||
ResultTextBox.Text = "[]";
|
||||
if (!result.Success)
|
||||
{
|
||||
AppendLog($"SecurityGroups-Call fehlgeschlagen [{result.ErrorCode}]: {result.ErrorMessage}", LogLevels.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (groups.Count == 0)
|
||||
if (result.SecurityGroups.Count == 0)
|
||||
{
|
||||
AppendLog("Keine SecurityGroups gefunden.", LogLevels.Warning);
|
||||
ResultTextBox.Text = "[]";
|
||||
return;
|
||||
}
|
||||
|
||||
var entries = ConvertSecurityGroups(groups);
|
||||
var json = JsonConvert.SerializeObject(entries, Formatting.Indented);
|
||||
ResultTextBox.Text = json;
|
||||
AppendLog($"SecurityGroups erhalten: {entries.Count}");
|
||||
AppendLog($"SecurityGroups erhalten: {result.SecurityGroups.Count}");
|
||||
}
|
||||
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)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
@@ -807,9 +959,19 @@ namespace LiamWorkflowDiagnostics
|
||||
try
|
||||
{
|
||||
SaveSettings();
|
||||
AppendLog($"{actionName} gestartet.");
|
||||
var result = await action();
|
||||
var runInWhatIfMode = IsWhatIfEnabled && whatIfAction != null;
|
||||
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);
|
||||
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)
|
||||
@@ -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
|
||||
{
|
||||
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()
|
||||
{
|
||||
var selectedValue = MsTeamsVisibilityComboBox.SelectedValue;
|
||||
@@ -978,22 +1128,6 @@ namespace LiamWorkflowDiagnostics
|
||||
|| 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (data == null)
|
||||
@@ -1489,6 +1529,7 @@ namespace LiamWorkflowDiagnostics
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
public bool UseHttps { get; set; } = false;
|
||||
public string License { get; set; } = string.Empty;
|
||||
public bool EnableWhatIf { get; set; } = true;
|
||||
public string NtfsCreateFolderPath { get; set; } = string.Empty;
|
||||
public string NtfsCreateParentPath { get; set; } = string.Empty;
|
||||
public string NtfsCreateOwnerSids { get; set; } = string.Empty;
|
||||
|
||||
172
Sonstiges/Get-NtfsStableFolderId.ps1
Normal file
172
Sonstiges/Get-NtfsStableFolderId.ps1
Normal 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.
|
||||
#>
|
||||
@@ -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#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
|
||||
|
||||
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#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.
|
||||
|
||||
@@ -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#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.
|
||||
|
||||
@@ -207,7 +268,7 @@ Betroffene Stellen:
|
||||
- [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)
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -221,7 +282,7 @@ Betroffene Stellen:
|
||||
- [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)
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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)
|
||||
- [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.
|
||||
|
||||
@@ -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#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.
|
||||
|
||||
@@ -269,7 +330,7 @@ Betroffene Stellen:
|
||||
- [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)
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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#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.
|
||||
|
||||
|
||||
396
Sonstiges/LIAM_NTFS_DFS_Metadaten_Klassifikation_Konzept.md
Normal file
396
Sonstiges/LIAM_NTFS_DFS_Metadaten_Klassifikation_Konzept.md
Normal 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.
|
||||
421
Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md
Normal file
421
Sonstiges/LIAM_NTFS_Massenverarbeitung_ADGruppen_Konzept.md
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
133
Sonstiges/Remove-OrphanedFolderAclEntries.ps1
Normal file
133
Sonstiges/Remove-OrphanedFolderAclEntries.ps1
Normal 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)
|
||||
Reference in New Issue
Block a user