This commit is contained in:
Drechsler, Meik
2025-10-15 14:56:07 +02:00
commit f563d78417
896 changed files with 654481 additions and 0 deletions

View File

@@ -0,0 +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();
}
}
}

View File

@@ -0,0 +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"
}

View File

@@ -0,0 +1,74 @@
<?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" />
</Project>

View File

@@ -0,0 +1,36 @@
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")]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +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;
}
}
}

View File

@@ -0,0 +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;
}
}
}
}

View File

@@ -0,0 +1,516 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using C4IT.Logging;
using static C4IT.Logging.cLogManager;
namespace LiamAD
{
public class cActiveDirectoryBase
{
private cADLogonInfo privLogonInfo = null;
public PrincipalContext adContext = null;
public DirectoryEntry directoryEntry = 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);
}
private async Task<bool> privLogonAsync(cADLogonInfo LogonInfo)
{
try
{
//TODO: remove dummy delay?
await Task.Delay(0);
ResetError();
adContext = new PrincipalContext(ContextType.Domain, LogonInfo.Domain, LogonInfo.User, new NetworkCredential("", LogonInfo.UserSecret).Password);
var ldapPath = $"LDAP://{LogonInfo.Domain}/{LogonInfo.TargetGroupPath}";
directoryEntry = new DirectoryEntry
{
Path = ldapPath,
Username = LogonInfo.User,
Password = new NetworkCredential(LogonInfo.User, LogonInfo.UserSecret).Password,
AuthenticationType = AuthenticationTypes.Secure | AuthenticationTypes.Sealing
};
return adContext != null;
}
catch (Exception E)
{
SetErrorException("exception error while ad login", E, LogLevels.Debug);
cLogManager.LogException(E, LogLevels.Debug);
}
return false;
}
private async Task<bool> privRelogon()
{
if (privLogonInfo == null)
return false;
var RetVal = await privLogonAsync(privLogonInfo);
return RetVal;
}
public async Task<bool> LogonAsync(cADLogonInfo LogonInfo)
{
var RetVal = await privLogonAsync(LogonInfo);
if (RetVal == true)
privLogonInfo = LogonInfo;
return RetVal;
}
internal AuthorizationRuleCollection GetAccessControlList(string path)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
DirectoryInfo dADir = new DirectoryInfo(path);
var dAACL = dADir.GetAccessControl();
return dAACL.GetAccessRules(true, false, typeof(System.Security.Principal.SecurityIdentifier));
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
}
internal string resolveSid(string sid)
{
try
{
return new System.Security.Principal.SecurityIdentifier(sid).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
}
catch (Exception E)
{
LogException(E);
return null;
}
}
internal async Task<cADCollectionBase> RequestSecurityGroupsListAsync(string groupFilter)
{
var CM = MethodBase.GetCurrentMethod();
LogMethodBegin(CM);
try
{
await Task.Delay(0);
var Result = privRequestSecurityGroupsListAsync(groupFilter);
if (Result != null)
{
var RetVal = new cADCollectionBase(Result.Count);
foreach (var Entry in Result)
{
var res = new cSecurityGroupResult(Entry);
RetVal.Add(res);
}
return RetVal;
}
}
catch (Exception E)
{
LogException(E);
return null;
}
finally
{
LogMethodEnd(CM);
}
return null;
}
private List<cSecurityGroupResult> privRequestSecurityGroupsListAsync(string groupFilter = null, string rawLDAPFilter = null)
{
ResetError();
List<cSecurityGroupResult> securityGroups = new List<cSecurityGroupResult>();
if (String.IsNullOrEmpty(privLogonInfo.TargetGroupPath) ||
(string.IsNullOrEmpty(groupFilter) && string.IsNullOrEmpty(rawLDAPFilter)))
{
return new List<cSecurityGroupResult>();
}
try
{
var ctx = adContext;
var entry = directoryEntry;
using (DirectorySearcher dSearch = new DirectorySearcher(entry))
{
dSearch.Filter = string.IsNullOrEmpty(rawLDAPFilter) ? "(&(" + groupFilter + ")(objectClass=group))" : rawLDAPFilter;
dSearch.PageSize = 100000;
SearchResultCollection sr = dSearch.FindAll();
if (sr.Count > 0)
{
foreach (SearchResult k in sr)
{
var sid = new SecurityIdentifier(k.Properties["objectSid"][0] as byte[], 0).Value;
var dn = k.Properties["distinguishedname"][0].ToString();
// Initialisieren Sie die managedBy-SID als null
string managedBySid = null;
// Prüfen, ob das managedBy-Attribut existiert und nicht null ist
if (k.Properties.Contains("managedBy") && k.Properties["managedBy"].Count > 0)
{
// managedBy-DN erhalten
string managedByDn = k.Properties["managedBy"][0].ToString();
// Erstellen eines DirectoryEntry-Objekts für den managedBy-DN
using (DirectoryEntry managedByEntry = new DirectoryEntry($"LDAP://{managedByDn}"))
{
if (managedByEntry.Properties.Contains("objectSid") && managedByEntry.Properties["objectSid"].Count > 0)
{
// SID des managedBy-Objekts erhalten
managedBySid = new SecurityIdentifier(managedByEntry.Properties["objectSid"][0] as byte[], 0).Value;
}
}
}
// Neues SecurityGroup-Objekt erstellen
cSecurityGroupResult group = new cSecurityGroupResult()
{
ID = sid,
Path = dn,
DisplayName = k.Properties["Name"][0].ToString(),
Description = k.Properties.Contains("Description") ? k.Properties["Description"][0].ToString() : string.Empty,
Scope = (GroupScope)GroupPrincipal.FindByIdentity(ctx, IdentityType.Sid, sid).GroupScope,
ManagedBySID = managedBySid
};
securityGroups.Add(group);
}
}
}
}
catch (Exception e)
{
cLogManager.LogException(e);
return new List<cSecurityGroupResult>(securityGroups);
}
return securityGroups;
}
public class cADCollectionBase : SortedList<string, cADResultBase>
{
public cADCollectionBase() { }
public cADCollectionBase(int n) : base(n) { }
public void Add(cADResultBase adr)
{
if (!this.ContainsKey(adr.ID))
this.Add(adr.ID, adr);
}
}
public class cSecurityGroupResult : cADResultBase
{
public cSecurityGroupResult() { }
public cSecurityGroupResult(cADResultBase b) : base(b)
{
this.ManagedBySID = (b as cSecurityGroupResult)?.ManagedBySID;
}
public GroupScope Scope { get; internal set; }
public string ManagedBySID { get; internal set; }
}
public class cADUserResult : cADResultBase
{
public string GivenName { get; internal set; }
public string SurName { get; internal set; }
public string UserPrincipalName { get; internal set; }
public string Email { get; internal set; }
public cADUserResult() { }
public cADUserResult(cADResultBase b) : base(b)
{
}
public cADUserResult(Principal Result) : base(Result)
{
UserPrincipalName = Result.UserPrincipalName;
}
public GroupScope Scope { get; internal set; }
}
public class cADResultBase
{
public string ID { get; set; } = null;
public string DisplayName { get; set; } = null;
public string Path { get; set; } = null;
public DateTime CreatedDate { get; set; } = DateTime.MinValue;
public string Description { get; set; } = null;
public cADResultBase()
{ }
public cADResultBase(cADResultBase Result)
{
if (Result == null)
return;
ID = Result.ID;
DisplayName = Result.DisplayName;
Description = Result.Description;
Path = Result.Path;
}
public cADResultBase(Principal Result)
{
if (Result == null)
return;
ID = Result.Sid.ToString();
DisplayName = Result.DisplayName;
Path = Result.DistinguishedName;
}
}
/// <summary>
/// Asynchrones Abrufen des managedBy-Attributs einer AD-Gruppe anhand ihrer SID.
/// Gibt den Distinguished Name (DN) des Managers zurück.
/// </summary>
/// <param name="groupSid">Die SID der AD-Gruppe.</param>
/// <returns>Der DN des Managers oder null, falls nicht gefunden.</returns>
public async Task<string> GetManagedByDnAsync(string dn)
{
try
{
// Simuliert einen asynchronen Aufruf
await Task.Yield();
using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.DistinguishedName, dn))
{
if (group == null)
{
cLogManager.LogEntry($"Gruppe mit dn {dn} nicht gefunden.");
return null;
}
// Zugriff auf das underlying DirectoryEntry, um das managedBy-Attribut zu lesen
var directoryEntry = group.GetUnderlyingObject() as DirectoryEntry;
if (directoryEntry != null && directoryEntry.Properties.Contains("managedBy"))
{
var managedByValue = directoryEntry.Properties["managedBy"].Value as string;
if (!string.IsNullOrEmpty(managedByValue))
{
return managedByValue;
}
else
{
cLogManager.LogEntry($"managedBy-Attribut für Gruppe mit dn {dn} ist leer.");
}
}
else
{
cLogManager.LogEntry($"Gruppe mit dn {dn} hat kein managedBy-Attribut.");
}
}
}
catch (Exception ex)
{
cLogManager.LogException(ex);
}
return null;
}
/// <summary>
/// Asynchrones Abrufen der Mitglieder des Managers einer AD-Gruppe anhand des groupSid.
/// Verwendet den DN des Managers.
/// </summary>
/// <param name="groupSid">Die SID der AD-Gruppe.</param>
/// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
public async Task<cADCollectionBase> GetManagedByMembersAsync(string groupSid)
{
try
{
var managedByDn = await GetManagedByDnAsync(groupSid);
if (!string.IsNullOrEmpty(managedByDn))
{
return await GetMembersByDnAsync(managedByDn);
}
else
{
cLogManager.LogEntry($"Keine gültige managedBy DN für Gruppe mit SID {groupSid} gefunden.");
}
}
catch (Exception ex)
{
cLogManager.LogException(ex);
}
return null;
}
/// <summary>
/// Asynchrones Abrufen der Mitglieder einer AD-Gruppe anhand des Distinguished Name (DN).
/// </summary>
/// <param name="dn">Der Distinguished Name der AD-Gruppe.</param>
/// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
public async Task<cADCollectionBase> GetMembersByDnAsync(string dn)
{
try
{
// Simuliert einen asynchronen Aufruf
await Task.Yield();
var result = privGetMembersByDnAsync(dn).ToList();
if (result != null && result.Any())
{
var retVal = new cADCollectionBase(result.Count);
foreach (var entry in result)
{
var res = new cADUserResult(entry);
if (!string.IsNullOrEmpty(res.Path))
retVal.Add(res);
}
return retVal;
}
else
{
cLogManager.LogEntry($"Keine Mitglieder für Gruppe mit DN {dn} gefunden.");
}
}
catch (Exception e)
{
cLogManager.LogException(e);
}
return null;
}
/// <summary>
/// Interne Methode zum Abrufen der Mitglieder einer AD-Gruppe anhand des Distinguished Name (DN).
/// </summary>
/// <param name="dn">Der Distinguished Name der AD-Gruppe.</param>
/// <returns>Eine PrincipalSearchResult mit den Mitgliedern oder null.</returns>
private PrincipalSearchResult<Principal> privGetMembersByDnAsync(string dn)
{
try
{
using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.DistinguishedName, dn))
{
if (group == null)
{
cLogManager.LogEntry($"Gruppe mit DN {dn} nicht gefunden.");
return null;
}
else
{
return group.GetMembers(true);
}
}
}
catch (Exception e)
{
cLogManager.LogException(e);
}
return null;
}
/// <summary>
/// Asynchrones Abrufen der Mitglieder einer AD-Gruppe anhand ihrer SID.
/// Diese Methode bleibt unverändert und kann weiterhin verwendet werden.
/// </summary>
/// <param name="sid">Die SID der AD-Gruppe.</param>
/// <returns>Eine cADCollectionBase mit den Mitgliedern oder null.</returns>
internal async Task<cADCollectionBase> GetMembersAsync(string sid)
{
try
{
// Simuliert einen asynchronen Aufruf
await Task.Yield();
var result = privGetMembersAsync(sid).ToList();
if (result != null && result.Any())
{
var retVal = new cADCollectionBase(result.Count);
foreach (var entry in result)
{
var res = new cADUserResult(entry);
if (!string.IsNullOrEmpty(res.Path))
retVal.Add(res);
}
return retVal;
}
else
{
cLogManager.LogEntry($"Keine Mitglieder für Gruppe mit SID {sid} gefunden.");
}
}
catch (Exception e)
{
cLogManager.LogException(e);
}
return null;
}
/// <summary>
/// Interne Methode zum Abrufen der Mitglieder einer AD-Gruppe anhand ihrer SID.
/// Diese Methode bleibt unverändert und kann weiterhin verwendet werden.
/// </summary>
/// <param name="sid">Die SID der AD-Gruppe.</param>
/// <returns>Eine PrincipalSearchResult mit den Mitgliedern oder null.</returns>
private PrincipalSearchResult<Principal> privGetMembersAsync(string sid)
{
try
{
using (var group = GroupPrincipal.FindByIdentity(adContext, IdentityType.Sid, sid))
{
if (group == null)
{
cLogManager.LogEntry($"Gruppe mit SID {sid} nicht gefunden.");
return null;
}
else
{
return group.GetMembers(true);
}
}
}
catch (Exception e)
{
cLogManager.LogException(e);
}
return null;
}
}
}

View File

@@ -0,0 +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")]

View File

@@ -0,0 +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")]

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +1 @@
88ca960765b758a5c4edcddedf6dc811f2a49d840cd7ade015d7644911a99c77

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +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")]

View File

@@ -0,0 +1,14 @@
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

View File

@@ -0,0 +1 @@
ab9bb136583040c5ab0b8fc2b80edba3f154caa1532f30973b39973f0def47e6

View File

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