/// <summary> /// Creates a consistent base <see cref="ComposedRuleResult"/> instance with default values /// </summary> /// <param name="entry">The entry for which we are creating a result</param> /// <param name="isValuePresent">Allows calling code to know if the <paramref name="entry"/> has a value for the given <see cref="AttributeName"/></param> /// <returns></returns> protected ComposedRuleResult InitResult(SearchResultEntry entry, out bool isValuePresent) { isValuePresent = false; var result = new ComposedRuleResult() { AttributeName = this.AttributeName, EntityDistinguishedName = entry.Attributes[StringLiterals.DistinguishedName][0].ToString(), ObjectType = ComposedRule.GetObjectType(entry), OriginalValue = null, ProposedAction = ActionType.None }; if (entry.Attributes.Contains(this.AttributeName)) { isValuePresent = true; result.OriginalValue = entry.Attributes[this.AttributeName][0].ToString(); } return(result); }
/// <summary> /// When implemented by child classes executes this rule and returns a result /// </summary> /// <param name="parent">The composed rule containing this rule</param> /// <param name="entry">The search result we are checking</param> /// <param name="attributeValue">The current attribute value as pass through the chain</param> /// <returns>Either a success or error result</returns> public abstract RuleResult Execute(ComposedRule parent, SearchResultEntry entry, string attributeValue);
/// <summary> /// Executes a rule collection against the supplied connection /// </summary> /// <param name="collection">The rule collection to execute</param> /// <param name="connection">The connection used to retrieve entries for processing</param> /// <returns><see cref="RuleCollectionResult"/> or null if canceled</returns> private RuleCollectionResult ExecuteRuleCollection(RuleCollection collection, LdapConnection connection) { // these count all the totals for the connection against which this RuleCollection is being run var stopWatch = new Stopwatch(); long skipCount = 0; long entryCount = 0; long duplicateCount = 0; long errorCount = 0; this.OnStatusUpdate?.Invoke(StringLiterals.LdapConnectionEstablishing); var searchRequest = collection.CreateSearchRequest(); this.OnStatusUpdate?.Invoke(StringLiterals.LdapConnectionEstablished); var errors = new List <ComposedRuleResult>(); this.OnStatusUpdate?.Invoke(StringLiterals.BeginningQuery); while (true) { var searchResponse = (SearchResponse)connection.SendRequest(searchRequest); // verify support for paged results if (searchResponse.Controls.Length != 1 || !(searchResponse.Controls[0] is PageResultResponseControl)) { this.OnStatusUpdate?.Invoke(StringLiterals.CannotPageResultSet); throw new InvalidOperationException(StringLiterals.CannotPageResultSet); } foreach (SearchResultEntry entry in searchResponse.Entries) { if (this.CancellationPending) { return(null); } if (collection.Skip(entry)) { skipCount++; continue; } // this tracks the number of entries we have processed and not skipped entryCount++; foreach (var composedRule in collection.Rules) { // run each composed rule which can produce multiple results var results = composedRule.Execute(entry); for (var i = 0; i < results.Length; i++) { var result = results[i]; if (!result.Success) { errorCount++; if (result.Results.Any(r => (r.ErrorTypeFlags & ErrorType.Duplicate) != 0)) { duplicateCount++; if (result.ProposedAction == ActionType.Edit) { // Add original LDAP entry with the same value. var originalEntry = DuplicateStore.GetOriginalSearchResultEntry(result.AttributeName, result.OriginalValue); var additionalResult = new ComposedRuleResult { AttributeName = result.AttributeName, EntityDistinguishedName = originalEntry.DistinguishedName, EntityCommonName = originalEntry.Attributes[StringLiterals.Cn][0].ToString(), ObjectType = ComposedRule.GetObjectType(entry), OriginalValue = result.OriginalValue, ProposedAction = result.ProposedAction, ProposedValue = result.ProposedValue, Results = new RuleResult[] { new RuleResult(false) { ErrorTypeFlags = ErrorType.Duplicate, ProposedAction = result.ProposedAction, UpdatedValue = result.OriginalValue } }, Success = result.Success }; errors.Add(additionalResult); } } errors.Add(result); } } } } // handle paging var cookie = searchResponse.Controls.OfType <PageResultResponseControl>().First().Cookie; // if this is true, there are no more pages to request if (cookie.Length == 0) { break; } searchRequest.Controls.OfType <PageResultRequestControl>().First().Cookie = cookie; } // we are all done, stop tracking time stopWatch.Stop(); return(new RuleCollectionResult { TotalDuplicates = duplicateCount, TotalErrors = errorCount, TotalFound = skipCount + entryCount, TotalSkips = skipCount, TotalProcessed = entryCount, Elapsed = stopWatch.Elapsed, Errors = errors.ToArray() }); }
/// <summary> /// Gets the object type as a string for the given <paramref name="entry"/> /// </summary> /// <param name="entry"><see cref="SearchResultEntry"/> whose object type we want</param> /// <returns></returns> protected string GetObjectType(SearchResultEntry entry) { return(ComposedRule.GetObjectType(entry)); }
/// <summary> /// Logic to determine if a given <see cref="SearchResultEntry"/> is skipped for processing /// </summary> /// <param name="entry"><see cref="SearchResultEntry"/> to check</param> /// <returns>True if <paramref name="entry"/> should be skipped, false if it should be processed</returns> public override bool Skip(SearchResultEntry entry) { string objectDn = String.Empty; try { objectDn = entry.Attributes[StringLiterals.DistinguishedName][0].ToString(); // Active Directory Synchronization in Office 365 // http://technet.microsoft.com/en-us/library/hh852469.aspx#bkmk_adcleanup // Remove duplicate proxyAddress and userPrincipalName attributes. // Update blank and invalid userPrincipalName attributes with a valid userPrincipalName. // Remove invalid and questionable characters in the givenName, surname (sn), sAMAccountName, displayName, // mail, proxyAddresses, mailNickname, and userPrincipalName attributes. // Appendix F Directory Object Preparation // http://technet.microsoft.com/en-us/library/hh852533.aspx // Any object is filtered if: // match well known exclusion pattern if (entry.Attributes[StringLiterals.Cn][0].ToString().EndsWith("$", StringComparison.CurrentCultureIgnoreCase)) { return(true); } foreach (string exclusion in Constants.WellKnownExclusions) { if (entry.Attributes[StringLiterals.Cn][0].ToString().ToUpperInvariant().StartsWith(exclusion.ToUpperInvariant(), StringComparison.CurrentCultureIgnoreCase)) { return(true); } } // •Object is a conflict object (DN contains \0ACNF: ) if (entry.Attributes[StringLiterals.DistinguishedName][0].ToString().IndexOf("\0ACNF:", StringComparison.CurrentCultureIgnoreCase) != -1) { return(true); } // •isCriticalSystemObject is present if (entry.Attributes.Contains(StringLiterals.IsCriticalSystemObject)) { if (Convert.ToBoolean(entry.Attributes[StringLiterals.IsCriticalSystemObject][0].ToString(), CultureInfo.CurrentCulture) == true) { return(true); } } // determine objectClass string objectType = ComposedRule.GetObjectType(entry); // User objects are filtered if: if (objectType.Equals("user", StringComparison.CurrentCultureIgnoreCase)) { if (entry.Attributes.Contains(StringLiterals.SamAccountName)) { // SamAccountName exclusions if (entry.Attributes[StringLiterals.SamAccountName][0].ToString().ToUpperInvariant().StartsWith("CAS_", StringComparison.CurrentCultureIgnoreCase) || entry.Attributes[StringLiterals.SamAccountName][0].ToString().ToUpperInvariant().StartsWith("SUPPORT_", StringComparison.CurrentCultureIgnoreCase) || entry.Attributes[StringLiterals.SamAccountName][0].ToString().ToUpperInvariant().StartsWith("MSOL_", StringComparison.CurrentCultureIgnoreCase)) { return(true); } } else { // •sAMAccountName is not present //return true; } if (entry.Attributes.Contains(StringLiterals.MailNickName)) { // mailNickName exclusions if (entry.Attributes[StringLiterals.MailNickName][0].ToString().ToUpperInvariant().StartsWith("CAS_", StringComparison.CurrentCultureIgnoreCase) || entry.Attributes[StringLiterals.MailNickName][0].ToString().ToUpperInvariant().StartsWith("SYSTEMMAILBOX", StringComparison.CurrentCultureIgnoreCase)) { return(true); } } if (entry.Attributes.Contains(StringLiterals.DisplayName) && entry.Attributes.Contains(StringLiterals.MsExchHideFromAddressLists)) { if (Convert.ToBoolean(entry.Attributes[StringLiterals.MsExchHideFromAddressLists][0].ToString(), CultureInfo.CurrentCulture) == true && entry.Attributes[StringLiterals.DisplayName][0].ToString().ToUpperInvariant().StartsWith("MSOL", StringComparison.CurrentCultureIgnoreCase)) { return(true); } } // •msExchRecipientTypeDetails == (0x1000 OR 0x2000 OR 0x4000 OR 0x400000 OR 0x800000 OR 0x1000000 OR 0x20000000) if (entry.Attributes.Contains(StringLiterals.MsExchRecipientTypeDetails)) { switch (entry.Attributes[StringLiterals.MsExchRecipientTypeDetails][0].ToString()) { case "0x1000": case "0x2000": case "0x4000": case "0x400000": case "0x800000": case "0x1000000": case "0x20000000": return(true); } } } // Group objects are filter if: // List of attributes that are synchronized to Office 365 and attributes that are written back to the on-premises Active Directory Domain Services // http://support.microsoft.com/kb/2256198 // NOTE: these are listed as group filters in the article, but they appear to be an inaccurate mix of filters and errors. // SecurityEnabledGroup objects are filtered if: // •isCriticalSystemObject = TRUE // •mail is present AND DisplayName is not present // •Group has more than 15,000 immediate members // MailEnabledGroup objects are filtered if: // •DisplayName is empty // •(ProxyAddress does not have a primary SMTP address) AND (mail attribute is not present/invalid - i.e. indexof ('@') <= 0) // •Group has more than 15,000 immediate members if (objectType.Equals("group", StringComparison.CurrentCultureIgnoreCase)) { if (Convert.ToInt64(entry.Attributes[StringLiterals.GroupType][0].ToString(), CultureInfo.CurrentCulture) < 0) // security group { if (!entry.Attributes.Contains(StringLiterals.Mail)) { return(true); } } else // distribution group { if (!entry.Attributes.Contains(StringLiterals.ProxyAddresses)) { return(true); } } } return(false); } catch (Exception ex) { this.InvokeStatus(StringLiterals.Exception + "Result Filter: " + objectDn + " " + ex.Message); } return(false); }