Skip to content

Commit

Permalink
Resolved #17: Add support for replication progress reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelGrafnetter committed Sep 16, 2016
1 parent 94b2d70 commit c6fb24c
Show file tree
Hide file tree
Showing 36 changed files with 787 additions and 83 deletions.
6 changes: 6 additions & 0 deletions Changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version 2.17
- [Module] The Get-ADReplAccount -All command now reports replication progress.
- [Framework] Added the ability to retrieve the replication cursor.
- [Framework] The ReplicationCookie class is now immutable and replication progress is reported using a delegate.
- [Framework] Win32 exceptions are now translated to more specific .NET exceptions by the Validator class.

Version 2.16.1
- [Module] Added the -ShowPlainTextPasswords parameter to the Test-PasswordQuality cmdlet.
Cracked and cleartext passwords now do not get displayed by default.
Expand Down
1 change: 1 addition & 0 deletions Src/DSInternals.Common/DSInternals.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.DirectoryServices" />
</ItemGroup>
<ItemGroup>
<Compile Include="Cryptography\HashEqualityComparer.cs" />
Expand Down
2 changes: 1 addition & 1 deletion Src/DSInternals.Common/DSInternals.Common.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>This package is shared between all other DSInternals packages. It contains implementations of common hash functions used by Windows, including NT hash, LM hash and OrgId hash. It also contains methods for SysKey/BootKey retrieval.</description>
<summary>This package is shared between all other DSInternals packages.</summary>
<releaseNotes>Added support for the UserAccountControl attribude.</releaseNotes>
<releaseNotes>Win32 exceptions are now translated to more specific .NET exceptions by the Validator class.</releaseNotes>
<copyright>Copyright (c) 2015-2016 Michael Grafnetter. All rights reserved.</copyright>
<tags>ActiveDirectory Security</tags>
</metadata>
Expand Down
4 changes: 2 additions & 2 deletions Src/DSInternals.Common/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DSInternals Common Library")]
[assembly: AssemblyVersion("2.16")]
[assembly: AssemblyFileVersion("2.16")]
[assembly: AssemblyVersion("2.17")]
[assembly: AssemblyFileVersion("2.17")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
Expand Down
44 changes: 37 additions & 7 deletions Src/DSInternals.Common/Validator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using DSInternals.Common.Properties;
using System;
using System.ComponentModel;
using System.DirectoryServices.ActiveDirectory;
using System.IO;
using System.Security;
using System.Text.RegularExpressions;
Expand All @@ -15,18 +16,47 @@ public static class Validator

public static void AssertSuccess(NtStatus status)
{
if(status != NtStatus.Success)
{
Win32ErrorCode code = NativeMethods.RtlNtStatusToDosError(status);
throw new Win32Exception((int) code);
}
Win32ErrorCode code = NativeMethods.RtlNtStatusToDosError(status);
AssertSuccess(code);
}
public static void AssertSuccess(Win32ErrorCode code)
{
if (code != Win32ErrorCode.Success)
if(code == Win32ErrorCode.Success)
{
// No error occured, so exit gracefully.
return;
}
var genericException = new Win32Exception((int)code);
Exception exceptionToThrow;
// We will try to translate the generic Win32 exception to a more specific built-in exception.
switch(code)
{
throw new Win32Exception((int) code);
case Win32ErrorCode.DS_INVALID_DN_SYNTAX:
exceptionToThrow = new ArgumentException(genericException.Message, genericException);
break;
case Win32ErrorCode.ACCESS_DENIED:
case Win32ErrorCode.DS_DRA_ACCESS_DENIED:
exceptionToThrow = new UnauthorizedAccessException(genericException.Message, genericException);
break;
case Win32ErrorCode.NOT_ENOUGH_MEMORY:
case Win32ErrorCode.OUTOFMEMORY:
case Win32ErrorCode.DS_DRA_OUT_OF_MEM:
case Win32ErrorCode.RPC_S_OUT_OF_RESOURCES:
exceptionToThrow = new OutOfMemoryException(genericException.Message, genericException);
break;
case Win32ErrorCode.NO_LOGON_SERVERS:
case Win32ErrorCode.NO_SUCH_DOMAIN:
case Win32ErrorCode.RPC_S_SERVER_UNAVAILABLE:
case Win32ErrorCode.RPC_S_CALL_FAILED:
exceptionToThrow = new ActiveDirectoryServerDownException(genericException.Message, genericException);
break;
// TODO: Add translation for ActiveDirectoryOperationException and for other exception types.
default:
// We were not able to translate the Win32Exception to a more specific type.
exceptionToThrow = genericException;
break;
}
throw exceptionToThrow;
}

public static void AssertIsHex(string value, string paramName)
Expand Down
2 changes: 1 addition & 1 deletion Src/DSInternals.DataStore/DSInternals.DataStore.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>DSInternals DataStore is an advanced framework for offline ntds.dit file manipulation. It can be used to extract password hashes from Active Directory backups or to modify the sIDHistory and primaryGroupId attributes.</description>
<summary>DSInternals DataStore is an advanced framework for offline ntds.dit file manipulation.</summary>
<releaseNotes>Added support for the UserAccountControl attribude.</releaseNotes>
<releaseNotes>Updated dependencies.</releaseNotes>
<copyright>Copyright (c) 2015-2016 Michael Grafnetter. All rights reserved.</copyright>
<tags>ActiveDirectory Security NTDS</tags>
<references>
Expand Down
4 changes: 2 additions & 2 deletions Src/DSInternals.DataStore/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DSInternals DataStore Library")]
[assembly: AssemblyVersion("2.16")]
[assembly: AssemblyFileVersion("2.16")]
[assembly: AssemblyVersion("2.17")]
[assembly: AssemblyFileVersion("2.17")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
{
using DSInternals.Common.Data;
using DSInternals.PowerShell.Properties;
using DSInternals.Replication;
using DSInternals.Replication.Model;
using System;
using System.Linq;
using System.Management.Automation;
using System.Security.Principal;

Expand Down Expand Up @@ -72,10 +75,28 @@ protected override void ProcessRecord()

protected void ReturnAllAccounts()
{
foreach (var account in this.ReplicationClient.GetAccounts(this.NamingContext))
// Write the initial progress
// TODO: Extract strings as resources
var progress = new ProgressRecord(1, "Replication", "Replicating Active Directory objects.");
progress.PercentComplete = 0;
this.WriteProgress(progress);

// Update the progress after each replication cycle
ReplicationProgressHandler progressReporter = (ReplicationCookie cookie, int processedObjectCount, int totalObjectCount) =>
{
progress.PercentComplete = (int) (((double)processedObjectCount / (double)totalObjectCount) * 100);
this.WriteProgress(progress);
};

// Replicate all accounts
foreach (var account in this.ReplicationClient.GetAccounts(this.NamingContext, progressReporter))
{
this.WriteObject(account);
}

// Write progress completed
progress.RecordType = ProgressRecordType.Completed;
this.WriteProgress(progress);
}

protected void ReturnSingleAccount()
Expand Down
4 changes: 2 additions & 2 deletions Src/DSInternals.PowerShell/DSInternals.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
RootModule = 'DSInternals.psm1'

# Version number of this module.
ModuleVersion = '2.16.1'
ModuleVersion = '2.17'

# ID used to uniquely identify this module
GUID = '766b3ad8-eb78-48e6-84bd-61b31d96b53e'
Expand Down Expand Up @@ -117,7 +117,7 @@ PrivateData = @{

# ReleaseNotes of this module
ReleaseNotes = @"
- Added the -ShowPlainTextPasswords parameter to the Test-PasswordQuality cmdlet.
- The Get-ADReplAccount -All command now reports replication progress.
"@
} # End of PSData hashtable

Expand Down
4 changes: 2 additions & 2 deletions Src/DSInternals.PowerShell/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("DSInternals PowerShell Commands")]
[assembly: AssemblyVersion("2.16.1")]
[assembly: AssemblyFileVersion("2.16.1")]
[assembly: AssemblyVersion("2.17")]
[assembly: AssemblyFileVersion("2.17")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
Expand Down
2 changes: 1 addition & 1 deletion Src/DSInternals.Replication.Interop/AssemblyInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using namespace System::Security::Permissions;
//
[assembly:AssemblyTitleAttribute(L"DSInternals Replication Interop Library")];
// Note: Do not forget to change the version in app.rc files.
[assembly:AssemblyVersionAttribute("2.16.0")];
[assembly:AssemblyVersionAttribute("2.17.0")];
[assembly:AssemblyDescriptionAttribute(L"")];
[assembly:AssemblyConfigurationAttribute(L"")];
[assembly:AssemblyCompanyAttribute(L"")];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Rpcrt4.lib;Secur32.lib</AdditionalDependencies>
<AdditionalDependencies>RpcRT4.lib;Secur32.lib</AdditionalDependencies>
<LinkTimeCodeGeneration>Default</LinkTimeCodeGeneration>
</Link>
<Midl>
Expand Down Expand Up @@ -139,7 +139,7 @@
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Rpcrt4.lib;Secur32.lib</AdditionalDependencies>
<AdditionalDependencies>RpcRT4.lib;Secur32.lib</AdditionalDependencies>
<LinkTimeCodeGeneration>Default</LinkTimeCodeGeneration>
</Link>
<Midl>
Expand Down
76 changes: 62 additions & 14 deletions Src/DSInternals.Replication.Interop/DrsConnection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ namespace DSInternals
{
return this->_sessionKey;
}
void DrsConnection::SessionKey::set(array<byte>^ newKey)

Guid DrsConnection::ServerSiteGuid::get()
{
this->_sessionKey = newKey;
return this->_serverSiteObjectGuid;
}

midl_ptr<DRS_EXTENSIONS_INT> DrsConnection::CreateClientInfo()
Expand All @@ -93,6 +94,45 @@ namespace DSInternals
return clientInfo;
}

/// <summary>
/// Gets the replication cursor information for the specified partition.
/// </summary>
/// <param name="namingContext">The distinguished name of the partition for which to retrieve the replication cursor information.</param>
array<ReplicationCursor^>^ DrsConnection::GetReplicationCursors(String^ namingContext)
{
// Validate connection
// TODO: Extract connection validation as a proteted method
if (this->IsInvalid)
{
// TODO: Exception type
throw gcnew Exception("Not connected");
}
Validator::AssertNotNullOrWhiteSpace(namingContext, "namingContext");

// Prepare the parameters
DRS_HANDLE handle = this->handle.ToPointer();
const DWORD inVersion = 1;
DWORD outVersion = 0;
auto request = CreateReplicationCursorsRequest(namingContext);

DRS_MSG_GETREPLINFO_REPLY reply = { nullptr };

// Retrieve info from DC
auto result = IDL_DRSGetReplInfo_NoSEH(handle, inVersion, (DRS_MSG_GETREPLINFO_REQ*)request.get(), &outVersion, &reply);

// Validate the return code
Validator::AssertSuccess((Win32ErrorCode)result);

// TODO: Check the returned outVersion.

// Prevent memory leak by storing the cursors in midl_ptr
auto cursors = midl_ptr<DS_REPL_CURSORS>(reply.pCursors);

// Process the results
auto managedCursors = RpcTypeConverter::ToReplicationCursors(move(cursors));
return managedCursors;
}

midl_ptr<DRS_MSG_GETCHGREQ_V8> DrsConnection::CreateGenericReplicateRequest(midl_ptr<DSNAME> &&dsName, array<ATTRTYP>^ partialAttributeSet, ULONG maxBytes, ULONG maxObjects)
{
// TODO: Add support for Windows Server 2003
Expand All @@ -115,6 +155,14 @@ namespace DSInternals
return request;
}

midl_ptr<DRS_MSG_GETREPLINFO_REQ_V1> DrsConnection::CreateReplicationCursorsRequest(String^ namingContext)
{
auto request = make_midl_ptr<DRS_MSG_GETREPLINFO_REQ_V1>();
request->InfoType = DS_REPL_INFO_TYPE::DS_REPL_INFO_CURSORS_FOR_NC;
request->pszObjectDN = RpcTypeConverter::ToNativeString(namingContext).release();
return request;
}

midl_ptr<DRS_MSG_GETCHGREQ_V8> DrsConnection::CreateReplicateAllRequest(ReplicationCookie^ cookie, array<ATTRTYP>^ partialAttributeSet, ULONG maxBytes, ULONG maxObjects)
{
auto ncToReplicate = RpcTypeConverter::ToDsName(cookie->NamingContext);
Expand All @@ -124,13 +172,13 @@ namespace DSInternals
request->usnvecFrom.usnHighPropUpdate = cookie->HighPropUpdate;
request->usnvecFrom.usnReserved = cookie->Reserved;
request->uuidInvocIdSrc = RpcTypeConverter::ToUUID(cookie->InvocationId);
request->ulFlags |= DRS_OPTIONS::DRS_GET_NC_SIZE;
return request;
}

midl_ptr<DRS_MSG_GETCHGREQ_V8> DrsConnection::CreateReplicateSingleRequest(Guid objectGuid, array<ATTRTYP>^ partialAttributeSet)
{
auto objectToReplicate = RpcTypeConverter::ToDsName(objectGuid);
// TODO: Are sizes important?
auto request = CreateGenericReplicateRequest(move(objectToReplicate), partialAttributeSet, defaultMaxBytes, defaultMaxObjects);
request->ulExtendedOp = EXOP_REQ::EXOP_REPL_OBJ;
// Guid of an existing DC must be set for the replication to work
Expand All @@ -141,10 +189,9 @@ namespace DSInternals
midl_ptr<DRS_MSG_GETCHGREQ_V8> DrsConnection::CreateReplicateSingleRequest(String^ distinguishedName, array<ATTRTYP>^ partialAttributeSet)
{
auto objectToReplicate = RpcTypeConverter::ToDsName(distinguishedName);
// TODO: Are sizes important?
auto request = CreateGenericReplicateRequest(move(objectToReplicate), partialAttributeSet, defaultMaxBytes, defaultMaxObjects);
request->ulExtendedOp = EXOP_REQ::EXOP_REPL_OBJ;
// Guid of an existing DC must be set for the replication to work
// Guid of an existing object must be set for the replication to work
request->uuidDsaObjDest = RpcTypeConverter::ToUUID(this->_serverSiteObjectGuid);
return request;
}
Expand All @@ -161,16 +208,17 @@ namespace DSInternals

ReplicationResult^ DrsConnection::ReplicateAllObjects(ReplicationCookie^ cookie, array<ATTRTYP>^ partialAttributeSet, ULONG maxBytes, ULONG maxObjects)
{
// TODO: Validate Cookie not null
// TODO: To Params
// Validate parameters
Validator::AssertNotNull(cookie, "cookie");

auto request = CreateReplicateAllRequest(cookie, partialAttributeSet, maxBytes, maxObjects);
auto reply = GetNCChanges(move(request));
auto objects = ReadObjects(reply->pObjects, reply->cNumObjects);
USN_VECTOR usnTo = reply->usnvecTo;
Guid invocationId = RpcTypeConverter::ToGuid(reply->uuidInvocIdSrc);
auto newCookie = gcnew ReplicationCookie(cookie->NamingContext, invocationId, usnTo.usnHighObjUpdate, usnTo.usnHighPropUpdate, usnTo.usnReserved);
bool hasMoreData = reply->fMoreData != 0;
return gcnew ReplicationResult(objects, hasMoreData, newCookie);
return gcnew ReplicationResult(objects, hasMoreData, newCookie, reply->cNumNcSizeObjects);
}

ReplicaObject^ DrsConnection::ReplicateSingleObject(String^ distinguishedName)
Expand Down Expand Up @@ -246,7 +294,7 @@ namespace DSInternals

if (reply->pResult->cItems != request->cNames)
{
// TODO: Exxception type
// TODO: Exception type
throw gcnew Exception("Obj not found");
}
return reply;
Expand Down Expand Up @@ -325,7 +373,7 @@ namespace DSInternals

midl_ptr<PARTIAL_ATTR_VECTOR_V1_EXT> DrsConnection::CreateNativePas(array<ATTRTYP>^ partialAttributeSet)
{
// TODO: Move to type converter?
// TODO: Move to type RpcTypeConverter?
if (partialAttributeSet == nullptr)
{
return nullptr;
Expand Down Expand Up @@ -415,13 +463,13 @@ namespace DSInternals

Guid DrsConnection::ReadGuid(const GUID &guid)
{
// TODO: Type converter needed?
// TODO: Move to RpcTypeConverter?
return *reinterpret_cast<Guid *>(const_cast<GUID *>(&guid));
}

String^ DrsConnection::ReadName(const DSNAME* dsName)
{
// TODO: Move to type converter
// TODO: Move to RpcTypeConverter?
if (dsName == nullptr || dsName->NameLen <= 0)
{
return nullptr;
Expand All @@ -433,7 +481,7 @@ namespace DSInternals

SecurityIdentifier^ DrsConnection::ReadSid(const DSNAME* dsName)
{
// TODO: Move to type converter
// TODO: Move to type RpcTypeConverter?
if (dsName == nullptr || dsName->SidLen <= 0)
{
return nullptr;
Expand Down Expand Up @@ -469,7 +517,7 @@ namespace DSInternals
memcpy(pinnedManagedKey, nativeKey.SessionKey, nativeKey.SessionKeyLength);
// Do not forget to free the unmanaged memory
SECURITY_STATUS status3 = FreeContextBuffer(nativeKey.SessionKey);
this->SessionKey = managedKey;
this->_sessionKey = managedKey;
}
}
}
Expand Down
Loading

1 comment on commit c6fb24c

@MichaelGrafnetter
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit should have referenced issue #25 instead of #17... I was simply too eager to release version 2.17.

Please sign in to comment.