Safeguard NTFS AD group reuse

This commit is contained in:
Meik
2026-07-16 15:43:00 +02:00
parent c4efc41c4b
commit c1e26ba615
4 changed files with 266 additions and 54 deletions

View File

@@ -144,6 +144,8 @@ namespace C4IT.LIAM
private const string AdditionalConfigurationGroupNameSanitizeReplacementKey = "NtfsGroupNameSanitizeReplacement";
private const string AdditionalConfigurationPreserveAdGroupNameCaseKey = "PreserveNtfsAdGroupNameCase";
private const string AdditionalConfigurationAdDomainControllersKey = "NtfsAdDomainControllers";
private const string AdditionalConfigurationAdGroupReuseModeKey = "NtfsAdGroupReuseMode";
private const string AdditionalConfigurationAdGroupMarkerBackfillKey = "NtfsAdGroupMarkerBackfill";
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);
@@ -1394,6 +1396,8 @@ namespace C4IT.LIAM
engine.CanManagePermissionsForPath = IsPermissionManagedFolderPath;
engine.CanManageTraversePermissionsForPath = IsTraversePermissionManagedPath;
engine.forceStrictAdGroupNames = IsAdditionalConfigurationEnabled("ForceStrictAdGroupNames");
engine.adGroupReuseMode = GetAdGroupReuseMode();
engine.adGroupMarkerBackfill = IsAdditionalConfigurationEnabled(AdditionalConfigurationAdGroupMarkerBackfillKey);
engine.groupNameSanitizeReplacement = GetAdditionalConfigurationValueOrDefault(
AdditionalConfigurationGroupNameSanitizeReplacementKey,
Helper.DefaultGroupNameSanitizeReplacement);
@@ -1458,6 +1462,25 @@ namespace C4IT.LIAM
return rawValue == null ? string.Empty : rawValue.Trim();
}
private SecurityGroupReuseMode GetAdGroupReuseMode()
{
var value = GetAdditionalConfigurationValue(AdditionalConfigurationAdGroupReuseModeKey);
if (string.IsNullOrWhiteSpace(value))
return SecurityGroupReuseMode.Safe;
if (value.Equals("Safe", StringComparison.OrdinalIgnoreCase))
return SecurityGroupReuseMode.Safe;
if (value.Equals("Name", StringComparison.OrdinalIgnoreCase))
{
LogEntry("NtfsAdGroupReuseMode=Name is active. Existing AD groups may be reused by name without ACL/marker validation.", LogLevels.Warning);
return SecurityGroupReuseMode.Name;
}
LogEntry($"AdditionalConfiguration '{AdditionalConfigurationAdGroupReuseModeKey}' has invalid value '{value}'. Defaulting to Safe.", LogLevels.Warning);
return SecurityGroupReuseMode.Safe;
}
private string GetEffectiveTraverseBoundaryPath()
{
var configuredBoundaryPath = GetAdditionalConfigurationValue(AdditionalConfigurationTraverseBoundaryPathKey);

View File

@@ -57,6 +57,8 @@ namespace C4IT_IAM_SET
public Func<string, bool> CanManageTraversePermissionsForPath;
public string traverseBoundaryPath;
public bool forceStrictAdGroupNames;
public SecurityGroupReuseMode adGroupReuseMode = SecurityGroupReuseMode.Safe;
public bool adGroupMarkerBackfill;
public string groupNameSanitizeReplacement = Helper.DefaultGroupNameSanitizeReplacement;
public bool preserveAdGroupNameCase;
public bool WhatIf;
@@ -323,7 +325,9 @@ namespace C4IT_IAM_SET
effectiveDomainController = effectiveDomainController,
password = password,
ForceStrictAdGroupNames = forceStrictAdGroupNames,
PreserveAdGroupNameCase = preserveAdGroupNameCase
PreserveAdGroupNameCase = preserveAdGroupNameCase,
ReuseMode = adGroupReuseMode,
MarkerBackfill = adGroupMarkerBackfill
};
}
@@ -934,7 +938,9 @@ namespace C4IT_IAM_SET
Name = traverseNameTemplate.ReplaceLoopTag(loop),
description = traverseDescriptionTemplate.ReplaceLoopTag(loop),
technicalName = "CN=" + traverseNameTemplate.ReplaceLoopTag(loop) + "," + groupOUPath,
Scope = traverseGroupTemplate.Scope
securityGroupType = SecurityGroupType.Traverse,
Scope = traverseGroupTemplate.Scope,
MarkerPath = parent.FullName
};
DefaultLogger.LogEntry(LogLevels.Debug, $"Erstellte TraverseGroup: {newTraverseGroup.Name} (Loop: {loop})");
loop++;

View File

@@ -26,9 +26,13 @@ namespace C4IT_IAM_Engine
public SecureString password;
public bool ForceStrictAdGroupNames;
public bool PreserveAdGroupNameCase;
public SecurityGroupReuseMode ReuseMode = SecurityGroupReuseMode.Safe;
public bool MarkerBackfill;
public List<IAM_SecurityGroup> IAM_SecurityGroups;
public string rootUID;
private const string MarkerAttributeName = "info";
private const string MarkerPrefix = "LIAM;Provider=Ntfs;";
public SecurityGroups()
{
IAM_SecurityGroups = new List<IAM_SecurityGroup>();
@@ -269,9 +273,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = ownerGlobal.WildcardTemplate,
technicalName = "CN=" + ownerGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Owner,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)ownerACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(osecGroup);
@@ -283,9 +289,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = writeGlobal.WildcardTemplate,
technicalName = "CN=" + writeGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Write,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)writeACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(wsecGroup);
@@ -297,9 +305,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = readGlobal.WildcardTemplate,
technicalName = "CN=" + readGlobal.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Read,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)readACLPermission,
Scope = GroupScope.Global
Scope = GroupScope.Global,
MarkerPath = newFolderPath
};
IAM_SecurityGroups.Add(rsecGroup);
@@ -315,9 +325,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = ownerDL.WildcardTemplate,
technicalName = "CN=" + ownerDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Owner,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)ownerACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
osecDLGroup.memberGroups.Add(osecGroup);
IAM_SecurityGroups.Add(osecDLGroup);
@@ -330,9 +342,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = writeDL.WildcardTemplate,
technicalName = "CN=" + writeDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Write,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)writeACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
wsecDLGroup.memberGroups.Add(wsecGroup);
IAM_SecurityGroups.Add(wsecDLGroup);
@@ -345,9 +359,11 @@ namespace C4IT_IAM_Engine
WildcardPattern = readDL.WildcardTemplate,
technicalName = "CN=" + readDL.NamingTemplate + "," + ouPath,
securityGroupType = SecurityGroupType.Read,
targetTyp = (int)IAM_TargetType.FileSystem,
rights = (FileSystemRights)readACLPermission,
Scope = GroupScope.Local
Scope = GroupScope.Local,
MarkerPath = newFolderPath
};
rsecDLGroup.memberGroups.Add(rsecGroup);
IAM_SecurityGroups.Add(rsecDLGroup);
@@ -661,6 +677,168 @@ namespace C4IT_IAM_Engine
group.CommitChanges();
}
private static string NormalizeMarkerPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
return string.Empty;
return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar);
}
private static string GetMarkerScope(GroupScope scope)
{
return scope == GroupScope.Local ? "DomainLocal" : "Global";
}
private static string BuildMarker(IAM_SecurityGroup secGroup)
{
return $"{MarkerPrefix}Path={NormalizeMarkerPath(secGroup.MarkerPath)};Role={secGroup.securityGroupType};Scope={GetMarkerScope(secGroup.Scope)}";
}
private static Dictionary<string, string> ParseMarker(string marker)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(marker) || !marker.StartsWith(MarkerPrefix, StringComparison.OrdinalIgnoreCase))
return result;
foreach (var part in marker.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
var separatorIndex = part.IndexOf('=');
if (separatorIndex <= 0)
continue;
result[part.Substring(0, separatorIndex).Trim()] = part.Substring(separatorIndex + 1).Trim();
}
return result;
}
private static IEnumerable<string> GetLiamMarkers(string info)
{
if (string.IsNullOrWhiteSpace(info))
return Enumerable.Empty<string>();
return info
.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)
.Select(i => i.Trim())
.Where(i => i.StartsWith(MarkerPrefix, StringComparison.OrdinalIgnoreCase));
}
private static bool IsMatchingMarker(string marker, IAM_SecurityGroup secGroup)
{
var values = ParseMarker(marker);
if (values.Count == 0)
return false;
return values.TryGetValue("Provider", out var provider)
&& string.Equals(provider, "Ntfs", StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Path", out var markerPath)
&& string.Equals(NormalizeMarkerPath(markerPath), NormalizeMarkerPath(secGroup.MarkerPath), StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Role", out var role)
&& string.Equals(role, secGroup.securityGroupType.ToString(), StringComparison.OrdinalIgnoreCase)
&& values.TryGetValue("Scope", out var scope)
&& string.Equals(scope, GetMarkerScope(secGroup.Scope), StringComparison.OrdinalIgnoreCase);
}
private bool TryReadInfoAttribute(DirectoryEntry group, out string info)
{
info = string.Empty;
if (group == null)
return false;
try
{
group.RefreshCache(new[] { MarkerAttributeName });
if (group.Properties.Contains(MarkerAttributeName) && group.Properties[MarkerAttributeName].Count > 0)
info = group.Properties[MarkerAttributeName].Value?.ToString() ?? string.Empty;
return true;
}
catch (Exception E)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group marker attribute '{MarkerAttributeName}' cannot be read for '{group.Path}'. Marker-based reuse is not available. {E.Message}");
return false;
}
}
private bool HasMatchingMarker(DirectoryEntry group, IAM_SecurityGroup secGroup)
{
if (!TryReadInfoAttribute(group, out var info))
return false;
return GetLiamMarkers(info).Any(marker => IsMatchingMarker(marker, secGroup));
}
private bool TryWriteMarker(DirectoryEntry group, IAM_SecurityGroup secGroup, bool onlyIfNoLiamMarker)
{
if (group == null)
return false;
if (!TryReadInfoAttribute(group, out var info))
return false;
var existingMarkers = GetLiamMarkers(info).ToList();
if (existingMarkers.Any(marker => IsMatchingMarker(marker, secGroup)))
return true;
if (onlyIfNoLiamMarker && existingMarkers.Count > 0)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group '{secGroup.Name}' already has a LIAM marker, but it does not match path '{secGroup.MarkerPath}' and role '{secGroup.securityGroupType}'. Marker backfill is skipped.");
return false;
}
try
{
var marker = BuildMarker(secGroup);
var newInfo = string.IsNullOrWhiteSpace(info)
? marker
: info.TrimEnd('\r', '\n') + Environment.NewLine + marker;
group.Properties[MarkerAttributeName].Value = newInfo;
group.CommitChanges();
DefaultLogger.LogEntry(LogLevels.Debug, $"AD group marker written to '{secGroup.Name}': {marker}");
return true;
}
catch (Exception E)
{
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group marker attribute '{MarkerAttributeName}' cannot be written for '{secGroup.Name}'. {E.Message}");
return false;
}
}
private DirectoryEntry ValidateExistingGroupReuse(DirectoryEntry existingGroup, IAM_SecurityGroup secGroup, string folderPath, bool linkedFromFolderAcl)
{
if (existingGroup == null)
return null;
if (ReuseMode == SecurityGroupReuseMode.Name)
{
if (!linkedFromFolderAcl && !HasMatchingMarker(existingGroup, secGroup))
DefaultLogger.LogEntry(LogLevels.Warning, $"AD group '{secGroup.Name}' is reused by name because NtfsAdGroupReuseMode=Name is active. This bypasses ACL/marker validation for '{secGroup.MarkerPath}'.");
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
if (linkedFromFolderAcl)
{
if (MarkerBackfill)
TryWriteMarker(existingGroup, secGroup, true);
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
if (HasMatchingMarker(existingGroup, secGroup))
{
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
var message = $"Existing AD group '{secGroup.Name}' was found by name for '{folderPath ?? secGroup.MarkerPath}', but it is not linked on the folder ACL and has no matching LIAM marker. Reuse is blocked in NtfsAdGroupReuseMode=Safe. Set NtfsAdGroupReuseMode=Name to allow legacy name-based reuse.";
DefaultLogger.LogEntry(LogLevels.Warning, message);
throw new InvalidOperationException(message);
}
public DirectoryEntry EnsureADGroup(string ouPath, IAM_SecurityGroup secGroup, List<UserPrincipal> users, string folderPath = null)
{
LogMethodBegin(MethodBase.GetCurrentMethod());
@@ -668,8 +846,12 @@ namespace C4IT_IAM_Engine
{
secGroup.CreatedNewEntry = false;
DirectoryEntry existingGroup = null;
var linkedFromFolderAcl = false;
if (!ForceStrictAdGroupNames)
{
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
linkedFromFolderAcl = existingGroup != null;
}
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
@@ -680,8 +862,8 @@ namespace C4IT_IAM_Engine
if (existingGroup == null)
return CreateADGroup(ouPath, secGroup, users);
existingGroup = ValidateExistingGroupReuse(existingGroup, secGroup, folderPath, linkedFromFolderAcl);
AddMissingMembers(existingGroup, secGroup, users);
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
}
catch (Exception E)
@@ -702,8 +884,12 @@ namespace C4IT_IAM_Engine
{
secGroup.CreatedNewEntry = false;
DirectoryEntry existingGroup = null;
var linkedFromFolderAcl = false;
if (!ForceStrictAdGroupNames)
{
existingGroup = FindGroupEntryFromFolderAcl(folderPath, secGroup.WildcardPattern);
linkedFromFolderAcl = existingGroup != null;
}
if (existingGroup == null)
existingGroup = FindGroupEntry(secGroup.Name);
@@ -714,8 +900,7 @@ namespace C4IT_IAM_Engine
if (existingGroup == null)
return null;
ApplyExistingGroup(secGroup, existingGroup);
return existingGroup;
return ValidateExistingGroupReuse(existingGroup, secGroup, folderPath, linkedFromFolderAcl);
}
catch (Exception E)
{
@@ -738,55 +923,45 @@ namespace C4IT_IAM_Engine
secGroup.Name = groupName;
secGroup.technicalName = "CN=" + groupName + "," + ouPath;
if (!GroupAllreadyExisting(groupName))
if (GroupAllreadyExisting(groupName))
throw new InvalidOperationException($"AD group '{groupName}' already exists. Create mode does not reuse existing groups.");
DirectoryEntry entry = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {groupName}");
DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group");
group.Properties["sAmAccountName"].Value = groupName;
if (users != null && secGroup.Scope == GroupScope.Global)
{
DirectoryEntry entry = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
DefaultLogger.LogEntry(LogLevels.Debug, $"Creating ad entry with CN / sAmAccountName: {groupName}");
DirectoryEntry group = entry.Children.Add("CN=" + groupName, "group");
group.Properties["sAmAccountName"].Value = groupName;
if (users != null && secGroup.Scope == GroupScope.Global)
foreach (var user in users)
{
foreach (var user in users)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {user.DistinguishedName}");
group.Properties["member"].Add(user.DistinguishedName);
}
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {user.DistinguishedName}");
group.Properties["member"].Add(user.DistinguishedName);
}
if(!String.IsNullOrEmpty(secGroup.description))
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting description: {secGroup.description}");
group.Properties["description"].Value = secGroup.description;
}
var groupType = secGroup.Scope == GroupScope.Global ? GroupScopeValues.Global : GroupScopeValues.Local;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting groupType to: {groupType}");
group.Properties["groupType"].Value = groupType;
if (secGroup.Scope == GroupScope.Local)
foreach (var iGroup in secGroup.memberGroups)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {iGroup.technicalName}");
group.Properties["member"].Add(iGroup.technicalName);
}
group.CommitChanges();
DirectoryEntry ent = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + "CN=" + groupName + "," + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
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
if(!String.IsNullOrEmpty(secGroup.description))
{
DirectoryEntry e = FindGroupEntry(secGroup.Name);
if (e == null)
return null;
AddMissingMembers(e, secGroup, users);
ApplyExistingGroup(secGroup, e);
return e;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting description: {secGroup.description}");
group.Properties["description"].Value = secGroup.description;
}
return null;
var groupType = secGroup.Scope == GroupScope.Global ? GroupScopeValues.Global : GroupScopeValues.Local;
DefaultLogger.LogEntry(LogLevels.Debug, $"Setting groupType to: {groupType}");
group.Properties["groupType"].Value = groupType;
if (secGroup.Scope == GroupScope.Local)
foreach (var iGroup in secGroup.memberGroups)
{
DefaultLogger.LogEntry(LogLevels.Debug, $"Adding member: {iGroup.technicalName}");
group.Properties["member"].Add(iGroup.technicalName);
}
group.CommitChanges();
DirectoryEntry ent = new DirectoryEntry("LDAP://" + GetLdapServer() + "/" + "CN=" + groupName + "," + ouPath, username, new NetworkCredential("", password).Password, AuthenticationTypes.Secure | AuthenticationTypes.Sealing);
var objectid = SecurityGroups.getSID(ent);
DefaultLogger.LogEntry(LogLevels.Debug, $"Security group created in ad: {secGroup.technicalName}");
secGroup.UID = objectid;
secGroup.CreatedNewEntry = true;
TryWriteMarker(ent, secGroup, false);
return ent;
}
catch (Exception E)
{
@@ -865,11 +1040,17 @@ namespace C4IT_IAM_Engine
public int targetTyp;
public GroupScope Scope;
public FileSystemRights rights;
public string MarkerPath;
public IAM_SecurityGroup()
{
memberGroups = new List<IAM_SecurityGroup>();
}
}
public enum SecurityGroupReuseMode
{
Safe,
Name
}
public enum SecurityGroupType
{
[XmlEnum(Name = "0")]

View File

@@ -215,6 +215,8 @@ Im Diagnose-JSON erscheinen diese Werte unter `AdditionalConfiguration`. Paramet
| `NtfsGroupNameSanitizeReplacement` | Zeichenfolge, z. B. `_`, `.`, leer, `none`, `remove`, `<empty>` | Legt fest, womit ungueltige Zeichen in dynamischen gruppennamenrelevanten Pfadbestandteilen ersetzt werden. Standard ist `_`. Mit leerem Wert oder `none`/`remove`/`<empty>` werden ungueltige Zeichen entfernt und Pfadsegmente ohne Trennzeichen verbunden. |
| `PreserveNtfsAdGroupNameCase` | `true`, `1`, `yes` | Unterbindet das automatische Uppercase fuer generierte NTFS-AD-Gruppennamen. Ohne diesen Parameter werden generierte Gruppennamen wie bisher in Grossbuchstaben erzeugt. |
| `ForceStrictAdGroupNames` | `true`, `1`, `yes` | Erzwingt strikte AD-Gruppennamen. Wildcard-/ACL-basierte Wiederverwendung abweichender bestehender Gruppen wird damit eingeschraenkt; es werden nur exakt passende konfigurierte oder generierte Namen verwendet. |
| `NtfsAdGroupReuseMode` | `Safe`, `Name` | Steuert die Wiederverwendung vorhandener AD-Gruppen im automatischen Ensure. Standard ist `Safe`: vorhandene Gruppen werden nur wiederverwendet, wenn sie bereits passend auf der Ordner-ACL liegen oder einen passenden LIAM-Marker im AD-Attribut `info` besitzen. `Name` erlaubt die Legacy-Wiederverwendung nur anhand des Namens und wird als unsicherer Modus geloggt. |
| `NtfsAdGroupMarkerBackfill` | `true`, `1`, `yes` | Ergaenzt bei sicher wiederverwendeten, ACL-verknuepften Gruppen einen LIAM-Marker im AD-Attribut `info`, sofern das Attribut les- und schreibbar ist. |
| `NtfsAdDomainControllers` | Kommagetrennte DC-Liste, z. B. `dc01.contoso.local,dc02.contoso.local` | Pinnt NTFS-AD-Operationen auf einen Domain Controller. Der erste erreichbare DC wird verwendet. Wenn kein Eintrag erreichbar ist oder der Parameter fehlt, wird der PDC Emulator verwendet; danach faellt der Code auf die normale Domain-Locator-Logik zurueck. Der ausgewaehlte DC wird im Debug-Log protokolliert. |
Beispiele: