chore: sync LIAM solution snapshot incl. diagnostics tooling

- update multiple LIAM projects and solution/config files

- add LiamWorkflowDiagnostics app sources and generated outputs

- include current workspace state (dependencies and build outputs)
This commit is contained in:
Meik
2026-02-27 09:12:34 +01:00
parent f563d78417
commit 3d4f60d83e
721 changed files with 936335 additions and 653393 deletions

View File

@@ -1,371 +1,371 @@
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using C4IT.Logging;
using C4IT.Matrix42.ServerInfo;
using LiamAD;
using static C4IT.Logging.cLogManager;
using static LiamAD.ADServiceGroupCreator;
using static LiamAD.cActiveDirectoryBase;
namespace C4IT.LIAM
{
public static class LiamInitializer
{
static public cLiamProviderBase CreateInstance(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData)
{
return new cLiamProviderAD(LiamConfiguration, ProviderData);
}
}
public class cLiamProviderAD : cLiamProviderBase
{
public static Guid adModuleId = new Guid("e820a625-0653-ee11-b886-00155d300101");
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
private readonly ADServiceGroupCreator _serviceGroupCreator;
public cLiamProviderAD(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) :
base(LiamConfiguration, ProviderData)
{
_serviceGroupCreator = new ADServiceGroupCreator(this);
}
public List<Tuple<string, string, string, string>> CreateServiceGroups(
string serviceName,
string description = null,
eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
ADGroupType gruppentyp = ADGroupType.Distribution,
IEnumerable<string> ownerSidList = null,
IEnumerable<string> memberSidList = null)
{
return _serviceGroupCreator.EnsureServiceGroups(
serviceName,
description,
gruppenbereich,
gruppentyp,
ownerSidList,
memberSidList);
}
public override async Task<bool> LogonAsync()
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return false;
}
return await LogonAsync(true);
}
public async Task<bool> LogonAsync(bool force = false)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var LI = new cADLogonInfo()
{
Domain = Domain,
User = Credential?.Identification,
UserSecret = Credential?.Secret,
TargetGroupPath = this.GroupPath
};
var RetVal = await activeDirectoryBase.LogonAsync(LI);
return RetVal;
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogMethodEnd(CM);
}
return false;
}
public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return new List<cLiamDataAreaBase>();
}
if (!await LogonAsync())
return null;
if (string.IsNullOrEmpty(this.GroupPath))
return null;
// 1. Alle Roh-Resultate einlesen
var rawList = await activeDirectoryBase.RequestSecurityGroupsListAsync(this.GroupFilter);
if (rawList == null)
return null;
// 2. Nur die, die dem RegEx entsprechen und deren Wert extrahieren
var allResults = rawList
.Where(entry =>
string.IsNullOrEmpty(this.GroupRegEx)
|| Regex.Match(entry.Value.DisplayName, this.GroupRegEx).Success)
.Select(entry => (cSecurityGroupResult)entry.Value)
.ToList();
// 3. ManagedBySID-Werte sammeln (ohne Null-/Leereinträge)
var managedBySids = new HashSet<string>(
allResults
.Select(r => r.ManagedBySID)
.Where(m => !string.IsNullOrEmpty(m))
);
// 4. Nur die Gruppen, deren ID nicht in managedBySids enthalten ist
var filteredResults = allResults
.Where(r => !managedBySids.Contains(r.ID))
.ToList();
// 5. In DataArea-Objekte umwandeln
var SecurityGroups = new List<cLiamDataAreaBase>();
foreach (var secGroup in filteredResults)
{
SecurityGroups.Add(new cLiamAdGroupAsDataArea(this, secGroup));
}
return SecurityGroups;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
public override async Task<List<cLiamDataAreaBase>> getSecurityGroupsAsync(string groupFilter)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return new List<cLiamDataAreaBase>();
}
if (!await LogonAsync())
return null;
if (string.IsNullOrEmpty(this.GroupPath))
return null;
var SecurityGroups = new List<cLiamDataAreaBase>();
var SGL = await activeDirectoryBase.RequestSecurityGroupsListAsync(groupFilter);
if (SGL == null)
return null;
foreach (var Entry in SGL)
{
if (!string.IsNullOrEmpty(this.GroupRegEx) && !Regex.Match(Entry.Value.DisplayName, this.GroupRegEx).Success)
continue;
var SecurityGroup = new cLiamAdGroup2(this, (cSecurityGroupResult)Entry.Value);
SecurityGroups.Add(SecurityGroup);
}
return SecurityGroups;
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogMethodEnd(CM);
}
return null;
}
public int getDepth(string path)
{
return getDepth(this.RootPath, path);
}
public static int getDepth(DirectoryInfo root, DirectoryInfo folder)
{
var rootDepth = root.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Length;
var folderDepth = folder.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Length;
return folderDepth - rootDepth;
}
public static int getDepth(string root, string folder)
{
return getDepth(new DirectoryInfo(root), new DirectoryInfo(folder));
}
public override string GetLastErrorMessage()
{
throw new NotImplementedException();
}
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
{
//TODO implement LoadDataArea
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
await Task.Delay(0);
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return null;
}
var res = new cLiamAdGroupAsDataArea(this, new cSecurityGroupResult()
{
Path = UID
});
return res;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
}
public class cLiamAdGroupAsDataArea : cLiamDataAreaBase
{
public new readonly cLiamProviderAD Provider = null;
public readonly string dn = null;
public readonly string scope = null;
public readonly string ManagedBySID;
public override Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
{
throw new NotImplementedException();
}
public cLiamAdGroupAsDataArea(cLiamProviderAD Provider, cSecurityGroupResult secGroup) : base(Provider)
{
this.UID = secGroup.ID;
this.TechnicalName = secGroup.Path;
this.DisplayName = secGroup.DisplayName;
this.Description = secGroup.Description;
this.Provider = Provider;
this.dn = secGroup.Path;
this.scope = secGroup.Scope.ToString();
this.ManagedBySID = secGroup.ManagedBySID;
}
public override async Task<List<cLiamUserInfo>> GetOwnersAsync()
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
return await GetMembersAsync(true);
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
private async Task<List<cLiamUserInfo>> GetMembersAsync(bool owners)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var AD = this.Provider?.activeDirectoryBase;
if (AD == null)
{
LogEntry($"Could not get ad class from Provider for folder '{this.TechnicalName}'", LogLevels.Warning);
return null;
}
cADCollectionBase lstMembers;
if (owners && !string.IsNullOrEmpty(OwnerRef))
lstMembers = await AD.GetMembersAsync(OwnerRef);
else if (owners && !string.IsNullOrEmpty(dn))
lstMembers = await AD.GetManagedByMembersAsync(this.dn);
else
lstMembers = null;
if (lstMembers == null)
{
LogEntry($"Could not get owner list for folder '{this.TechnicalName}'", LogLevels.Warning);
return null;
}
var RetVal = new List<cLiamUserInfo>(lstMembers.Count);
LogEntry($"Owners for folder found: {lstMembers.Count}", LogLevels.Debug);
foreach (var MemberEntry in lstMembers.Values)
{
var User = new cLiamUserInfo()
{
DisplayName = MemberEntry.DisplayName,
UserPrincipalName = (MemberEntry as cADUserResult).UserPrincipalName,
SID = MemberEntry.ID
};
RetVal.Add(User);
}
return RetVal;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
}
public class cLiamAdGroup2 : cLiamDataAreaBase
{
public new readonly cLiamProviderAD Provider = null;
public readonly string dn = null;
public readonly string scope = null;
public override Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
{
throw new NotImplementedException();
}
public cLiamAdGroup2(cLiamProviderAD Provider, cSecurityGroupResult secGroup) : base(Provider)
{
this.DisplayName = secGroup.DisplayName;
this.UID = secGroup.ID;
this.TechnicalName = secGroup.DisplayName;
this.Provider = Provider;
this.dn = secGroup.Path;
this.scope = secGroup.Scope.ToString();
}
}
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using C4IT.Logging;
using C4IT.Matrix42.ServerInfo;
using LiamAD;
using static C4IT.Logging.cLogManager;
using static LiamAD.ADServiceGroupCreator;
using static LiamAD.cActiveDirectoryBase;
namespace C4IT.LIAM
{
public static class LiamInitializer
{
static public cLiamProviderBase CreateInstance(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData)
{
return new cLiamProviderAD(LiamConfiguration, ProviderData);
}
}
public class cLiamProviderAD : cLiamProviderBase
{
public static Guid adModuleId = new Guid("e820a625-0653-ee11-b886-00155d300101");
public readonly cActiveDirectoryBase activeDirectoryBase = new cActiveDirectoryBase();
private readonly ADServiceGroupCreator _serviceGroupCreator;
public cLiamProviderAD(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) :
base(LiamConfiguration, ProviderData)
{
_serviceGroupCreator = new ADServiceGroupCreator(this);
}
public List<Tuple<string, string, string, string>> CreateServiceGroups(
string serviceName,
string description = null,
eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
ADGroupType gruppentyp = ADGroupType.Distribution,
IEnumerable<string> ownerSidList = null,
IEnumerable<string> memberSidList = null)
{
return _serviceGroupCreator.EnsureServiceGroups(
serviceName,
description,
gruppenbereich,
gruppentyp,
ownerSidList,
memberSidList);
}
public override async Task<bool> LogonAsync()
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return false;
}
return await LogonAsync(true);
}
public async Task<bool> LogonAsync(bool force = false)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var LI = new cADLogonInfo()
{
Domain = Domain,
User = Credential?.Identification,
UserSecret = Credential?.Secret,
TargetGroupPath = this.GroupPath
};
var RetVal = await activeDirectoryBase.LogonAsync(LI);
return RetVal;
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogMethodEnd(CM);
}
return false;
}
public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return new List<cLiamDataAreaBase>();
}
if (!await LogonAsync())
return null;
if (string.IsNullOrEmpty(this.GroupPath))
return null;
// 1. Alle Roh-Resultate einlesen
var rawList = await activeDirectoryBase.RequestSecurityGroupsListAsync(this.GroupFilter);
if (rawList == null)
return null;
// 2. Nur die, die dem RegEx entsprechen und deren Wert extrahieren
var allResults = rawList
.Where(entry =>
string.IsNullOrEmpty(this.GroupRegEx)
|| Regex.Match(entry.Value.DisplayName, this.GroupRegEx).Success)
.Select(entry => (cSecurityGroupResult)entry.Value)
.ToList();
// 3. ManagedBySID-Werte sammeln (ohne Null-/Leereinträge)
var managedBySids = new HashSet<string>(
allResults
.Select(r => r.ManagedBySID)
.Where(m => !string.IsNullOrEmpty(m))
);
// 4. Nur die Gruppen, deren ID nicht in managedBySids enthalten ist
var filteredResults = allResults
.Where(r => !managedBySids.Contains(r.ID))
.ToList();
// 5. In DataArea-Objekte umwandeln
var SecurityGroups = new List<cLiamDataAreaBase>();
foreach (var secGroup in filteredResults)
{
SecurityGroups.Add(new cLiamAdGroupAsDataArea(this, secGroup));
}
return SecurityGroups;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
public override async Task<List<cLiamDataAreaBase>> getSecurityGroupsAsync(string groupFilter)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return new List<cLiamDataAreaBase>();
}
if (!await LogonAsync())
return null;
if (string.IsNullOrEmpty(this.GroupPath))
return null;
var SecurityGroups = new List<cLiamDataAreaBase>();
var SGL = await activeDirectoryBase.RequestSecurityGroupsListAsync(groupFilter);
if (SGL == null)
return null;
foreach (var Entry in SGL)
{
if (!string.IsNullOrEmpty(this.GroupRegEx) && !Regex.Match(Entry.Value.DisplayName, this.GroupRegEx).Success)
continue;
var SecurityGroup = new cLiamAdGroup2(this, (cSecurityGroupResult)Entry.Value);
SecurityGroups.Add(SecurityGroup);
}
return SecurityGroups;
}
catch (Exception E)
{
LogException(E);
}
finally
{
LogMethodEnd(CM);
}
return null;
}
public int getDepth(string path)
{
return getDepth(this.RootPath, path);
}
public static int getDepth(DirectoryInfo root, DirectoryInfo folder)
{
var rootDepth = root.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Length;
var folderDepth = folder.FullName.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Length;
return folderDepth - rootDepth;
}
public static int getDepth(string root, string folder)
{
return getDepth(new DirectoryInfo(root), new DirectoryInfo(folder));
}
public override string GetLastErrorMessage()
{
throw new NotImplementedException();
}
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
{
//TODO implement LoadDataArea
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
await Task.Delay(0);
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(adModuleId))
{
LogEntry($"Error: License not valid", LogLevels.Error);
return null;
}
var res = new cLiamAdGroupAsDataArea(this, new cSecurityGroupResult()
{
Path = UID
});
return res;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
}
public class cLiamAdGroupAsDataArea : cLiamDataAreaBase
{
public new readonly cLiamProviderAD Provider = null;
public readonly string dn = null;
public readonly string scope = null;
public readonly string ManagedBySID;
public override Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
{
throw new NotImplementedException();
}
public cLiamAdGroupAsDataArea(cLiamProviderAD Provider, cSecurityGroupResult secGroup) : base(Provider)
{
this.UID = secGroup.ID;
this.TechnicalName = secGroup.Path;
this.DisplayName = secGroup.DisplayName;
this.Description = secGroup.Description;
this.Provider = Provider;
this.dn = secGroup.Path;
this.scope = secGroup.Scope.ToString();
this.ManagedBySID = secGroup.ManagedBySID;
}
public override async Task<List<cLiamUserInfo>> GetOwnersAsync()
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
return await GetMembersAsync(true);
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
private async Task<List<cLiamUserInfo>> GetMembersAsync(bool owners)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
var AD = this.Provider?.activeDirectoryBase;
if (AD == null)
{
LogEntry($"Could not get ad class from Provider for folder '{this.TechnicalName}'", LogLevels.Warning);
return null;
}
cADCollectionBase lstMembers;
if (owners && !string.IsNullOrEmpty(OwnerRef))
lstMembers = await AD.GetMembersAsync(OwnerRef);
else if (owners && !string.IsNullOrEmpty(dn))
lstMembers = await AD.GetManagedByMembersAsync(this.dn);
else
lstMembers = null;
if (lstMembers == null)
{
LogEntry($"Could not get owner list for folder '{this.TechnicalName}'", LogLevels.Warning);
return null;
}
var RetVal = new List<cLiamUserInfo>(lstMembers.Count);
LogEntry($"Owners for folder found: {lstMembers.Count}", LogLevels.Debug);
foreach (var MemberEntry in lstMembers.Values)
{
var User = new cLiamUserInfo()
{
DisplayName = MemberEntry.DisplayName,
UserPrincipalName = (MemberEntry as cADUserResult).UserPrincipalName,
SID = MemberEntry.ID
};
RetVal.Add(User);
}
return RetVal;
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
}
public class cLiamAdGroup2 : cLiamDataAreaBase
{
public new readonly cLiamProviderAD Provider = null;
public readonly string dn = null;
public readonly string scope = null;
public override Task<List<cLiamDataAreaBase>> getChildrenAsync(int Depth = -1)
{
throw new NotImplementedException();
}
public cLiamAdGroup2(cLiamProviderAD Provider, cSecurityGroupResult secGroup) : base(Provider)
{
this.DisplayName = secGroup.DisplayName;
this.UID = secGroup.ID;
this.TechnicalName = secGroup.DisplayName;
this.Provider = Provider;
this.dn = secGroup.Path;
this.scope = secGroup.Scope.ToString();
}
}
}

View File

@@ -1,10 +1,10 @@
""
{
"FILE_VERSION" = "9237"
"ENLISTMENT_CHOICE" = "NEVER"
"PROJECT_FILE_RELATIVE_PATH" = ""
"NUMBER_OF_EXCLUDED_FILES" = "0"
"ORIGINAL_PROJECT_FILE_PATH" = ""
"NUMBER_OF_NESTED_PROJECTS" = "0"
"SOURCE_CONTROL_SETTINGS_PROVIDER" = "PROVIDER"
}
""
{
"FILE_VERSION" = "9237"
"ENLISTMENT_CHOICE" = "NEVER"
"PROJECT_FILE_RELATIVE_PATH" = ""
"NUMBER_OF_EXCLUDED_FILES" = "0"
"ORIGINAL_PROJECT_FILE_PATH" = ""
"NUMBER_OF_NESTED_PROJECTS" = "0"
"SOURCE_CONTROL_SETTINGS_PROVIDER" = "PROVIDER"
}

View File

@@ -1,74 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{AECA0AD2-8B91-4767-9AFA-E160F6662DBE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LIAMActiveDirectory</RootNamespace>
<AssemblyName>LiamActiveDirectory</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
<SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.DirectoryServices" />
<Reference Include="System.DirectoryServices.AccountManagement" />
<Reference Include="System.ValueTuple, Version=4.0.5.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.ValueTuple.4.6.1\lib\net462\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="cActiveDirectoryBase.cs" />
<Compile Include="C4IT.LIAM.AD.cs" />
<Compile Include="cADBase.cs" />
<Compile Include="cADServiceGroupCreator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiamBaseClasses\LiamBaseClasses.csproj">
<Project>{3531c9e6-cf6e-458e-b604-4a5a8d1c7ab0}</Project>
<Name>LiamBaseClasses</Name>
</ProjectReference>
<ProjectReference Include="..\LiamHelper\LiamHelper.csproj">
<Project>{6b0e73a6-f918-42d5-9525-d59d4d16283d}</Project>
<Name>LiamHelper</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{AECA0AD2-8B91-4767-9AFA-E160F6662DBE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>LIAMActiveDirectory</RootNamespace>
<AssemblyName>LiamActiveDirectory</AssemblyName>
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
<SccProjectName>SAK</SccProjectName>
<SccLocalPath>SAK</SccLocalPath>
<SccAuxPath>SAK</SccAuxPath>
<SccProvider>SAK</SccProvider>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.DirectoryServices" />
<Reference Include="System.DirectoryServices.AccountManagement" />
<Reference Include="System.ValueTuple, Version=4.0.5.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
<HintPath>..\packages\System.ValueTuple.4.6.1\lib\net462\System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="cActiveDirectoryBase.cs" />
<Compile Include="C4IT.LIAM.AD.cs" />
<Compile Include="cADBase.cs" />
<Compile Include="cADServiceGroupCreator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiamBaseClasses\LiamBaseClasses.csproj">
<Project>{3531c9e6-cf6e-458e-b604-4a5a8d1c7ab0}</Project>
<Name>LiamBaseClasses</Name>
</ProjectReference>
<ProjectReference Include="..\LiamHelper\LiamHelper.csproj">
<Project>{6b0e73a6-f918-42d5-9525-d59d4d16283d}</Project>
<Name>LiamHelper</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -1,36 +1,19 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("LIAMActiveDirectory")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("LIAMActiveDirectory")]
[assembly: AssemblyCopyright("Copyright © 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("aeca0ad2-8b91-4767-9afa-e160f6662dbe")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("LIAM AD Module")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("aeca0ad2-8b91-4767-9afa-e160f6662dbe")]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,142 +1,142 @@
using C4IT.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LiamAD
{
public class cADBase
{
private cADLogonInfo privLogonInfo = null;
private int scanningDepth;
public PrincipalContext adContext = null;
public Exception LastException { get; private set; } = null;
public string LastErrorMessage { get; private set; } = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetError()
{
LastException = null;
LastErrorMessage = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
{
LastException = E;
LastErrorMessage = Action + ": " + E.Message;
cLogManager.LogEntry(Action, lev);
}
[DllImport("mpr.dll")]
private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags);
[DllImport("mpr.dll")]
private static extern int WNetCancelConnection2(string name, int flags,
bool force);
}
public class cADLogonInfo
{
public string Domain;
public string User;
public string UserSecret;
public string TargetGroupPath;
}
[StructLayout(LayoutKind.Sequential)]
public class NetResource
{
public ResourceScope Scope;
public ResourceType ResourceType;
public ResourceDisplaytype DisplayType;
public int Usage;
public string LocalName;
public string RemoteName;
public string Comment;
public string Provider;
}
public enum ResourceScope : int
{
Connected = 1,
GlobalNetwork,
Remembered,
Recent,
Context
};
public enum ResourceType : int
{
Any = 0,
Disk = 1,
Print = 2,
Reserved = 8,
}
public enum ResourceDisplaytype : int
{
Generic = 0x0,
Domain = 0x01,
Server = 0x02,
Share = 0x03,
File = 0x04,
Group = 0x05,
Network = 0x06,
Root = 0x07,
Shareadmin = 0x08,
Directory = 0x09,
Tree = 0x0a,
Ndscontainer = 0x0b
}
public enum NetError : uint
{
NERR_Success = 0,
NERR_BASE = 2100,
NERR_UnknownDevDir = (NERR_BASE + 16),
NERR_DuplicateShare = (NERR_BASE + 18),
NERR_BufTooSmall = (NERR_BASE + 23),
}
public enum SHARE_TYPE : uint
{
STYPE_DISKTREE = 0,
STYPE_PRINTQ = 1,
STYPE_DEVICE = 2,
STYPE_IPC = 3,
STYPE_SPECIAL = 0x80000000,
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHARE_INFO_1
{
public string shi1_netname;
public uint shi1_type;
public string shi1_remark;
public SHARE_INFO_1(string sharename, uint sharetype, string remark)
{
this.shi1_netname = sharename;
this.shi1_type = sharetype;
this.shi1_remark = remark;
}
public override string ToString()
{
return shi1_netname;
}
}
}
using C4IT.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace LiamAD
{
public class cADBase
{
private cADLogonInfo privLogonInfo = null;
private int scanningDepth;
public PrincipalContext adContext = null;
public Exception LastException { get; private set; } = null;
public string LastErrorMessage { get; private set; } = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetError()
{
LastException = null;
LastErrorMessage = null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
{
LastException = E;
LastErrorMessage = Action + ": " + E.Message;
cLogManager.LogEntry(Action, lev);
}
[DllImport("mpr.dll")]
private static extern int WNetAddConnection2(NetResource netResource, string password, string username, int flags);
[DllImport("mpr.dll")]
private static extern int WNetCancelConnection2(string name, int flags,
bool force);
}
public class cADLogonInfo
{
public string Domain;
public string User;
public string UserSecret;
public string TargetGroupPath;
}
[StructLayout(LayoutKind.Sequential)]
public class NetResource
{
public ResourceScope Scope;
public ResourceType ResourceType;
public ResourceDisplaytype DisplayType;
public int Usage;
public string LocalName;
public string RemoteName;
public string Comment;
public string Provider;
}
public enum ResourceScope : int
{
Connected = 1,
GlobalNetwork,
Remembered,
Recent,
Context
};
public enum ResourceType : int
{
Any = 0,
Disk = 1,
Print = 2,
Reserved = 8,
}
public enum ResourceDisplaytype : int
{
Generic = 0x0,
Domain = 0x01,
Server = 0x02,
Share = 0x03,
File = 0x04,
Group = 0x05,
Network = 0x06,
Root = 0x07,
Shareadmin = 0x08,
Directory = 0x09,
Tree = 0x0a,
Ndscontainer = 0x0b
}
public enum NetError : uint
{
NERR_Success = 0,
NERR_BASE = 2100,
NERR_UnknownDevDir = (NERR_BASE + 16),
NERR_DuplicateShare = (NERR_BASE + 18),
NERR_BufTooSmall = (NERR_BASE + 23),
}
public enum SHARE_TYPE : uint
{
STYPE_DISKTREE = 0,
STYPE_PRINTQ = 1,
STYPE_DEVICE = 2,
STYPE_IPC = 3,
STYPE_SPECIAL = 0x80000000,
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHARE_INFO_1
{
public string shi1_netname;
public uint shi1_type;
public string shi1_remark;
public SHARE_INFO_1(string sharename, uint sharetype, string remark)
{
this.shi1_netname = sharename;
this.shi1_type = sharetype;
this.shi1_remark = remark;
}
public override string ToString()
{
return shi1_netname;
}
}
}

View File

@@ -1,288 +1,288 @@
using System;
using System.DirectoryServices;
using System.Linq;
using System.Threading;
using C4IT.Logging;
using C4IT.LIAM;
using LiamAD;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;
namespace LiamAD
{
/// <summary>
/// Helfer für cLiamProviderAD: Erstellt AD Member- und Owner-Gruppen für Services
/// nach konfigurierter Namenskonvention und setzt ManagedBy.
/// </summary>
public class ADServiceGroupCreator
{
private readonly cLiamProviderAD _provider;
private readonly cActiveDirectoryBase _adBase;
private readonly string _ldapRoot;
private readonly string _user;
private readonly string _password;
public enum ADGroupType
{
Security, // Sicherheit
Distribution // Verteiler
}
public ADServiceGroupCreator(cLiamProviderAD provider)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_adBase = provider.activeDirectoryBase;
_ldapRoot = $"LDAP://{provider.Domain}/{provider.GroupPath}";
_user = provider.Credential.Identification;
_password = new System.Net.NetworkCredential(_user, provider.Credential.Secret).Password;
}
/// <summary>
/// Erstellt oder findet beide AD-Gruppen (Member & Owner) für einen Service.
/// Neu mit: gruppenbereich (Scope) und gruppentyp (für Member-Gruppe).
/// Owner-Gruppe ist immer Security.
/// </summary>
public List<Tuple<string, string, string, string>> EnsureServiceGroups(
string serviceName,
string description = null,
eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
ADGroupType gruppentyp = ADGroupType.Distribution,
IEnumerable<string> ownerSidList = null,
IEnumerable<string> memberSidList = null)
{
const int MaxLoop = 50;
var result = new List<Tuple<string, string, string, string>>();
// Konventionen für Member und Owner
var ownerConv = _provider.NamingConventions
.FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADOwner);
var memberConv = _provider.NamingConventions
.FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADMember);
if (ownerConv == null || memberConv == null)
throw new InvalidOperationException("Namenskonvention für ADMember oder ADOwner fehlt.");
// Tags
_provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix);
_provider.CustomTags.TryGetValue("ADOwner", out var ownerPostfix);
_provider.CustomTags.TryGetValue("ADMember", out var memberPostfix);
// 1) Owner-Gruppe (immer Security)
string ownerName = null;
for (int loop = 0; loop <= MaxLoop; loop++)
{
string loopPart = loop > 0 ? "_" + loop : string.Empty;
ownerName = ownerConv.NamingTemplate
.Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
.Replace("{{NAME}}", serviceName)
.Replace("{{_LOOP}}", loopPart)
.Replace("{{GROUPTYPEPOSTFIX}}", ownerPostfix);
if (!GroupExists(ownerName)) break;
if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Owner-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
}
EnsureGroup(ownerName, ownerConv, description, managedByDn: null, gruppenbereich, ADGroupType.Security);
AddMembersBySid(ownerName, ownerSidList); // NEU: SIDs als Owner hinzufügen
var ownerDn = GetDistinguishedName(ownerName);
var ownerSid = GetSid(ownerName);
result.Add(Tuple.Create(eLiamAccessRoles.ADOwner.ToString(), ownerSid, ownerName, ownerDn));
// 2) Member-Gruppe (Gruppentyp nach Parameter)
string memberName = null;
for (int loop = 0; loop <= MaxLoop; loop++)
{
string loopPart = loop > 0 ? "_" + loop : string.Empty;
memberName = memberConv.NamingTemplate
.Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
.Replace("{{NAME}}", serviceName)
.Replace("{{_LOOP}}", loopPart)
.Replace("{{GROUPTYPEPOSTFIX}}", memberPostfix);
if (!GroupExists(memberName)) break;
if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Member-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
}
EnsureGroup(memberName, memberConv, description, managedByDn: ownerDn, gruppenbereich, gruppentyp);
AddMembersBySid(memberName, memberSidList); // NEU: SIDs als Member hinzufügen
var memberDn = GetDistinguishedName(memberName);
var memberSid = GetSid(memberName);
result.Add(Tuple.Create(eLiamAccessRoles.ADMember.ToString(), memberSid, memberName, memberDn));
return result;
}
/// <summary>
/// Fügt einer bestehenden Gruppe per SID die entsprechenden AD-Objekte hinzu.
/// </summary>
private void AddMembersBySid(string groupName, IEnumerable<string> sidList)
{
if (sidList == null) return;
// Basis für die Suche: komplette Domäne, nicht nur der OU-Pfad
string domainRoot = $"LDAP://{_provider.Domain}";
using (var root = new DirectoryEntry(domainRoot, _user, _password, AuthenticationTypes.Secure))
using (var grpSearch = new DirectorySearcher(root))
{
grpSearch.Filter = $"(&(objectCategory=group)(sAMAccountName={groupName}))";
var grpRes = grpSearch.FindOne();
if (grpRes == null) return;
var grpEntry = grpRes.GetDirectoryEntry();
foreach (var sidStr in sidList)
{
// Leere oder null überspringen
if (string.IsNullOrWhiteSpace(sidStr))
continue;
SecurityIdentifier sid;
try
{
sid = new SecurityIdentifier(sidStr);
}
catch (Exception)
{
// Ungültige SID-String-Darstellung überspringen
continue;
}
// In LDAP-Filter-Notation umwandeln
var bytes = new byte[sid.BinaryLength];
sid.GetBinaryForm(bytes, 0);
var sb = new StringBuilder();
foreach (var b in bytes)
sb.AppendFormat("\\{0:X2}", b);
string octetSid = sb.ToString();
// Suche nach dem Objekt in der Domäne
using (var usrSearch = new DirectorySearcher(root))
{
usrSearch.Filter = $"(objectSid={octetSid})";
var usrRes = usrSearch.FindOne();
if (usrRes == null)
continue;
var userDn = usrRes.Properties["distinguishedName"][0].ToString();
// Doppelteinträge vermeiden
if (!grpEntry.Properties["member"].Contains(userDn))
grpEntry.Properties["member"].Add(userDn);
}
}
grpEntry.CommitChanges();
}
}
/// <summary>
/// Wandelt eine SID (String-Form) in das für LDAP nötige Oktet-String-Format um.
/// </summary>
private string SidStringToLdapFilter(string sidString)
{
var sid = new SecurityIdentifier(sidString);
var bytes = new byte[sid.BinaryLength];
sid.GetBinaryForm(bytes, 0);
var sb = new StringBuilder();
foreach (var b in bytes)
sb.AppendFormat("\\{0:X2}", b);
return sb.ToString();
}
private string GetSid(string name)
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
using (var ds = new DirectorySearcher(root))
{
ds.Filter = $"(&(objectCategory=group)(sAMAccountName={name}))";
var r = ds.FindOne();
if (r == null) return null;
var de = r.GetDirectoryEntry();
var sidBytes = (byte[])de.Properties["objectSid"][0];
return new SecurityIdentifier(sidBytes, 0).Value;
}
}
private string FormatName(cLiamNamingConvention conv, string serviceName, System.Collections.Generic.IDictionary<string, string> tags)
{
string tmpl = conv.NamingTemplate.Replace("{{NAME}}", serviceName);
foreach (var kv in tags)
tmpl = tmpl.Replace("{{" + kv.Key + "}}", kv.Value);
return tmpl;
}
/// <summary>
/// Stellt sicher, dass die Gruppe existiert neu mit Scope & Type.
/// </summary>
private void EnsureGroup(
string groupName,
cLiamNamingConvention conv,
string description,
string managedByDn,
eLiamAccessRoleScopes groupScope,
ADGroupType groupType)
{
if (!GroupExists(groupName))
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
{
var grp = root.Children.Add("CN=" + groupName, "group");
grp.Properties["sAMAccountName"].Value = groupName;
grp.Properties["displayName"].Value = groupName;
// Hier: Security-Bit (0x80000000) nur, wenn Security, sonst 0
int typeBit = (groupType == ADGroupType.Security)
? unchecked((int)0x80000000)
: 0;
// Scope-Bit aus Param
grp.Properties["groupType"].Value = unchecked(typeBit | GetScopeBit(groupScope));
if (!string.IsNullOrEmpty(description))
grp.Properties["description"].Value = description;
if (managedByDn != null)
grp.Properties["managedBy"].Value = managedByDn;
grp.CommitChanges();
}
WaitReplication(groupName, TimeSpan.FromMinutes(2));
}
}
private bool GroupExists(string name)
{
return _adBase.directoryEntry.Children.Cast<DirectoryEntry>()
.Any(c => string.Equals(
c.Properties["sAMAccountName"]?.Value?.ToString(), name, StringComparison.OrdinalIgnoreCase));
}
private void WaitReplication(string groupName, TimeSpan timeout)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.Elapsed < timeout)
{
if (GroupExists(groupName))
return;
Thread.Sleep(2000);
}
}
private string GetDistinguishedName(string name)
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
using (var ds = new DirectorySearcher(root))
{
ds.Filter = "(&(objectClass=group)(sAMAccountName=" + name + "))";
var res = ds.FindOne();
return res?.Properties["distinguishedName"]?[0]?.ToString();
}
}
private int GetScopeBit(eLiamAccessRoleScopes scope)
{
switch (scope)
{
case eLiamAccessRoleScopes.Universal:
return 0x8;
case eLiamAccessRoleScopes.Global:
return 0x2;
case eLiamAccessRoleScopes.DomainLocal:
return 0x4;
default:
return 0x8;
}
}
}
using System;
using System.DirectoryServices;
using System.Linq;
using System.Threading;
using C4IT.Logging;
using C4IT.LIAM;
using LiamAD;
using System.Collections.Generic;
using System.Security.Principal;
using System.Text;
namespace LiamAD
{
/// <summary>
/// Helfer für cLiamProviderAD: Erstellt AD Member- und Owner-Gruppen für Services
/// nach konfigurierter Namenskonvention und setzt ManagedBy.
/// </summary>
public class ADServiceGroupCreator
{
private readonly cLiamProviderAD _provider;
private readonly cActiveDirectoryBase _adBase;
private readonly string _ldapRoot;
private readonly string _user;
private readonly string _password;
public enum ADGroupType
{
Security, // Sicherheit
Distribution // Verteiler
}
public ADServiceGroupCreator(cLiamProviderAD provider)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
_adBase = provider.activeDirectoryBase;
_ldapRoot = $"LDAP://{provider.Domain}/{provider.GroupPath}";
_user = provider.Credential.Identification;
_password = new System.Net.NetworkCredential(_user, provider.Credential.Secret).Password;
}
/// <summary>
/// Erstellt oder findet beide AD-Gruppen (Member & Owner) für einen Service.
/// Neu mit: gruppenbereich (Scope) und gruppentyp (für Member-Gruppe).
/// Owner-Gruppe ist immer Security.
/// </summary>
public List<Tuple<string, string, string, string>> EnsureServiceGroups(
string serviceName,
string description = null,
eLiamAccessRoleScopes gruppenbereich = eLiamAccessRoleScopes.Universal,
ADGroupType gruppentyp = ADGroupType.Distribution,
IEnumerable<string> ownerSidList = null,
IEnumerable<string> memberSidList = null)
{
const int MaxLoop = 50;
var result = new List<Tuple<string, string, string, string>>();
// Konventionen für Member und Owner
var ownerConv = _provider.NamingConventions
.FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADOwner);
var memberConv = _provider.NamingConventions
.FirstOrDefault(nc => nc.AccessRole == eLiamAccessRoles.ADMember);
if (ownerConv == null || memberConv == null)
throw new InvalidOperationException("Namenskonvention für ADMember oder ADOwner fehlt.");
// Tags
_provider.CustomTags.TryGetValue("ADGroupPrefix", out var prefix);
_provider.CustomTags.TryGetValue("ADOwner", out var ownerPostfix);
_provider.CustomTags.TryGetValue("ADMember", out var memberPostfix);
// 1) Owner-Gruppe (immer Security)
string ownerName = null;
for (int loop = 0; loop <= MaxLoop; loop++)
{
string loopPart = loop > 0 ? "_" + loop : string.Empty;
ownerName = ownerConv.NamingTemplate
.Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
.Replace("{{NAME}}", serviceName)
.Replace("{{_LOOP}}", loopPart)
.Replace("{{GROUPTYPEPOSTFIX}}", ownerPostfix);
if (!GroupExists(ownerName)) break;
if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Owner-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
}
EnsureGroup(ownerName, ownerConv, description, managedByDn: null, gruppenbereich, ADGroupType.Security);
AddMembersBySid(ownerName, ownerSidList); // NEU: SIDs als Owner hinzufügen
var ownerDn = GetDistinguishedName(ownerName);
var ownerSid = GetSid(ownerName);
result.Add(Tuple.Create(eLiamAccessRoles.ADOwner.ToString(), ownerSid, ownerName, ownerDn));
// 2) Member-Gruppe (Gruppentyp nach Parameter)
string memberName = null;
for (int loop = 0; loop <= MaxLoop; loop++)
{
string loopPart = loop > 0 ? "_" + loop : string.Empty;
memberName = memberConv.NamingTemplate
.Replace("{{ADGroupPrefix}}", prefix ?? string.Empty)
.Replace("{{NAME}}", serviceName)
.Replace("{{_LOOP}}", loopPart)
.Replace("{{GROUPTYPEPOSTFIX}}", memberPostfix);
if (!GroupExists(memberName)) break;
if (loop == MaxLoop) throw new InvalidOperationException($"Kein eindeutiger Member-Name für '{serviceName}' nach {MaxLoop} Versuchen.");
}
EnsureGroup(memberName, memberConv, description, managedByDn: ownerDn, gruppenbereich, gruppentyp);
AddMembersBySid(memberName, memberSidList); // NEU: SIDs als Member hinzufügen
var memberDn = GetDistinguishedName(memberName);
var memberSid = GetSid(memberName);
result.Add(Tuple.Create(eLiamAccessRoles.ADMember.ToString(), memberSid, memberName, memberDn));
return result;
}
/// <summary>
/// Fügt einer bestehenden Gruppe per SID die entsprechenden AD-Objekte hinzu.
/// </summary>
private void AddMembersBySid(string groupName, IEnumerable<string> sidList)
{
if (sidList == null) return;
// Basis für die Suche: komplette Domäne, nicht nur der OU-Pfad
string domainRoot = $"LDAP://{_provider.Domain}";
using (var root = new DirectoryEntry(domainRoot, _user, _password, AuthenticationTypes.Secure))
using (var grpSearch = new DirectorySearcher(root))
{
grpSearch.Filter = $"(&(objectCategory=group)(sAMAccountName={groupName}))";
var grpRes = grpSearch.FindOne();
if (grpRes == null) return;
var grpEntry = grpRes.GetDirectoryEntry();
foreach (var sidStr in sidList)
{
// Leere oder null überspringen
if (string.IsNullOrWhiteSpace(sidStr))
continue;
SecurityIdentifier sid;
try
{
sid = new SecurityIdentifier(sidStr);
}
catch (Exception)
{
// Ungültige SID-String-Darstellung überspringen
continue;
}
// In LDAP-Filter-Notation umwandeln
var bytes = new byte[sid.BinaryLength];
sid.GetBinaryForm(bytes, 0);
var sb = new StringBuilder();
foreach (var b in bytes)
sb.AppendFormat("\\{0:X2}", b);
string octetSid = sb.ToString();
// Suche nach dem Objekt in der Domäne
using (var usrSearch = new DirectorySearcher(root))
{
usrSearch.Filter = $"(objectSid={octetSid})";
var usrRes = usrSearch.FindOne();
if (usrRes == null)
continue;
var userDn = usrRes.Properties["distinguishedName"][0].ToString();
// Doppelteinträge vermeiden
if (!grpEntry.Properties["member"].Contains(userDn))
grpEntry.Properties["member"].Add(userDn);
}
}
grpEntry.CommitChanges();
}
}
/// <summary>
/// Wandelt eine SID (String-Form) in das für LDAP nötige Oktet-String-Format um.
/// </summary>
private string SidStringToLdapFilter(string sidString)
{
var sid = new SecurityIdentifier(sidString);
var bytes = new byte[sid.BinaryLength];
sid.GetBinaryForm(bytes, 0);
var sb = new StringBuilder();
foreach (var b in bytes)
sb.AppendFormat("\\{0:X2}", b);
return sb.ToString();
}
private string GetSid(string name)
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
using (var ds = new DirectorySearcher(root))
{
ds.Filter = $"(&(objectCategory=group)(sAMAccountName={name}))";
var r = ds.FindOne();
if (r == null) return null;
var de = r.GetDirectoryEntry();
var sidBytes = (byte[])de.Properties["objectSid"][0];
return new SecurityIdentifier(sidBytes, 0).Value;
}
}
private string FormatName(cLiamNamingConvention conv, string serviceName, System.Collections.Generic.IDictionary<string, string> tags)
{
string tmpl = conv.NamingTemplate.Replace("{{NAME}}", serviceName);
foreach (var kv in tags)
tmpl = tmpl.Replace("{{" + kv.Key + "}}", kv.Value);
return tmpl;
}
/// <summary>
/// Stellt sicher, dass die Gruppe existiert neu mit Scope & Type.
/// </summary>
private void EnsureGroup(
string groupName,
cLiamNamingConvention conv,
string description,
string managedByDn,
eLiamAccessRoleScopes groupScope,
ADGroupType groupType)
{
if (!GroupExists(groupName))
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
{
var grp = root.Children.Add("CN=" + groupName, "group");
grp.Properties["sAMAccountName"].Value = groupName;
grp.Properties["displayName"].Value = groupName;
// Hier: Security-Bit (0x80000000) nur, wenn Security, sonst 0
int typeBit = (groupType == ADGroupType.Security)
? unchecked((int)0x80000000)
: 0;
// Scope-Bit aus Param
grp.Properties["groupType"].Value = unchecked(typeBit | GetScopeBit(groupScope));
if (!string.IsNullOrEmpty(description))
grp.Properties["description"].Value = description;
if (managedByDn != null)
grp.Properties["managedBy"].Value = managedByDn;
grp.CommitChanges();
}
WaitReplication(groupName, TimeSpan.FromMinutes(2));
}
}
private bool GroupExists(string name)
{
return _adBase.directoryEntry.Children.Cast<DirectoryEntry>()
.Any(c => string.Equals(
c.Properties["sAMAccountName"]?.Value?.ToString(), name, StringComparison.OrdinalIgnoreCase));
}
private void WaitReplication(string groupName, TimeSpan timeout)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
while (sw.Elapsed < timeout)
{
if (GroupExists(groupName))
return;
Thread.Sleep(2000);
}
}
private string GetDistinguishedName(string name)
{
using (var root = new DirectoryEntry(_ldapRoot, _user, _password, AuthenticationTypes.Secure))
using (var ds = new DirectorySearcher(root))
{
ds.Filter = "(&(objectClass=group)(sAMAccountName=" + name + "))";
var res = ds.FindOne();
return res?.Properties["distinguishedName"]?[0]?.ToString();
}
}
private int GetScopeBit(eLiamAccessRoleScopes scope)
{
switch (scope)
{
case eLiamAccessRoleScopes.Universal:
return 0x8;
case eLiamAccessRoleScopes.Global:
return 0x2;
case eLiamAccessRoleScopes.DomainLocal:
return 0x4;
default:
return 0x8;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]

View File

@@ -1,4 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]

View File

@@ -1,14 +1,28 @@
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActi.8091FDFC.Up2Date
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActi.8091FDFC.Up2Date
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Debug\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Debug\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Debug\LiamActi.8091FDFC.Up2Date
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Debug\LiamActiveDirectory.pdb

View File

@@ -1 +1 @@
88ca960765b758a5c4edcddedf6dc811f2a49d840cd7ade015d7644911a99c77
0f439131d442d0ee4d6473d1fe79783624433c39a44374a60e15e079e468a4be

View File

@@ -1,4 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")]

View File

@@ -1,14 +1,28 @@
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LIAMActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LIAMActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LiamActi.8091FDFC.Up2Date
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LIAMActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LIAMActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LIAMActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\obj\Release\LiamActi.8091FDFC.Up2Date
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service\LIAMActiveDirectory\bin\Release\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Release\LiamActiveDirectory.csproj.AssemblyReference.cache
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Release\LiamActiveDirectory.csproj.CoreCompileInputs.cache
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Release\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Release\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamActiveDirectory.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamActiveDirectory.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamBaseClasses.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamHelper.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\System.ValueTuple.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\Newtonsoft.Json.dll
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamBaseClasses.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\LiamHelper.pdb
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\bin\Release\System.ValueTuple.xml
C:\Workspace\C4IT DEV LIAM WEB Service_git\LIAMActiveDirectory\obj\Release\LiamActi.8091FDFC.Up2Date

View File

@@ -1 +1 @@
ab9bb136583040c5ab0b8fc2b80edba3f154caa1532f30973b39973f0def47e6
e6f9f5153aa55a3a910f7c5ef0287e4500a3f354c3a1c3198c24d7eea04e0de2

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="System.ValueTuple" version="4.6.1" targetFramework="net462" />
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="System.ValueTuple" version="4.6.1" targetFramework="net462" />
</packages>