diff --git a/src/Console/Program.cs b/src/Console/Program.cs index 46161e02..8fb786ab 100644 --- a/src/Console/Program.cs +++ b/src/Console/Program.cs @@ -242,30 +242,66 @@ namespace Bit.Console } else { + string input; + Con.Write("Address: "); config.Address = Con.ReadLine().Trim(); - Con.Write("Port (389): "); - var portInput = Con.ReadLine().Trim(); - if(!string.IsNullOrWhiteSpace(portInput)) + Con.Write("Port [{0}]: ", config.Port); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) { - config.Port = portInput; + config.Port = input; } Con.Write("Path: "); config.Path = Con.ReadLine().Trim(); Con.Write("Username: "); config.Username = Con.ReadLine().Trim(); Con.Write("Password: "); - var passwordInput = ReadSecureLine(); - if(!string.IsNullOrWhiteSpace(passwordInput)) + input = ReadSecureLine(); + if(!string.IsNullOrWhiteSpace(input)) { - config.Password = new EncryptedData(passwordInput); - passwordInput = null; + config.Password = new EncryptedData(input); + input = null; } Con.WriteLine(); - Con.Write("Group filter: "); - config.GroupFilter = Con.ReadLine().Trim(); - Con.Write("User filter: "); - config.UserFilter = Con.ReadLine().Trim(); + Con.Write("Sync groups? [y]: "); + input = Con.ReadLine().Trim().ToLower(); + config.SyncGroups = input == "y" || input == "yes" || string.IsNullOrWhiteSpace(input); + if(config.SyncGroups) + { + Con.Write("Group filter [{0}]: ", config.GroupFilter); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.GroupFilter = input; + } + Con.Write("Group name attribute [{0}]: ", config.GroupNameAttribute); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.GroupNameAttribute = input; + } + } + Con.Write("Sync users? [y]: "); + input = Con.ReadLine().Trim().ToLower(); + config.SyncUsers = input == "y" || input == "yes" || string.IsNullOrWhiteSpace(input); + if(config.SyncUsers) + { + Con.Write("User filter [{0}]: ", config.UserFilter); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.UserFilter = input; + } + Con.Write("User email attribute [{0}]: ", config.UserEmailAttribute); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.GroupNameAttribute = input; + } + } + + input = null; } Con.WriteLine(); diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5407094b..41853de8 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Core/Models/Entry.cs b/src/Core/Models/Entry.cs new file mode 100644 index 00000000..54fa8206 --- /dev/null +++ b/src/Core/Models/Entry.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Models +{ + public abstract class Entry + { + public string DistinguishedName { get; set; } + public DateTime? CreationDate { get; set; } + public DateTime? RevisionDate { get; set; } + } + + public class GroupEntry : Entry + { + public string Name { get; set; } + public HashSet Members { get; set; } = new HashSet(); + public List GroupMembers { get; set; } = new List(); + public List UserMembers { get; set; } = new List(); + } + + public class UserEntry : Entry + { + public string Email { get; set; } + } +} diff --git a/src/Core/Models/ServerConfiguration.cs b/src/Core/Models/ServerConfiguration.cs index 3525a8ef..544f269b 100644 --- a/src/Core/Models/ServerConfiguration.cs +++ b/src/Core/Models/ServerConfiguration.cs @@ -17,8 +17,19 @@ namespace Bit.Core.Models public EncryptedData Password { get; set; } [JsonIgnore] public string ServerPath => $"LDAP://{Address}:{Port}/{Path}"; - public string GroupFilter { get; set; } - public string UserFilter { get; set; } + public string GroupFilter { get; set; } = "(&(objectClass=group))"; + public string UserFilter { get; set; } = "(&(objectClass=person))"; + public bool SyncGroups { get; set; } = true; + public bool SyncUsers { get; set; } = true; + public string Type { get; set; } = "Active Directory"; + public string MemberAttribute { get; set; } = "memberOf"; + public string GroupNameAttribute { get; set; } = "name"; + public string UserEmailAttribute { get; set; } = "mail"; + public bool EmailPrefixSuffix { get; set; } = false; + public string UserEmailPrefixAttribute { get; set; } = "sAMAccountName"; + public string UserEmailSuffix { get; set; } = "@companyname.com"; + public string CreationDateAttribute { get; set; } = "whenCreated"; + public string RevisionDateAttribute { get; set; } = "whenChanged"; public DirectoryEntry GetDirectoryEntry() { diff --git a/src/Core/Utilities/Sync.cs b/src/Core/Utilities/Sync.cs index 887625ea..4beb08b6 100644 --- a/src/Core/Utilities/Sync.cs +++ b/src/Core/Utilities/Sync.cs @@ -1,4 +1,5 @@ -using System; +using Bit.Core.Models; +using System; using System.Collections; using System.Collections.Generic; using System.DirectoryServices; @@ -10,8 +11,13 @@ namespace Bit.Core.Utilities { public static class Sync { - public static Task SyncGroupsAsync() + public static Task> SyncGroupsAsync() { + if(!Services.SettingsService.Instance.Server.SyncGroups) + { + throw new ApplicationException("Not configured to sync groups."); + } + if(Services.SettingsService.Instance.Server == null) { throw new ApplicationException("No configuration for directory server."); @@ -23,18 +29,70 @@ namespace Bit.Core.Utilities } var entry = Services.SettingsService.Instance.Server.GetDirectoryEntry(); - var filter = string.IsNullOrWhiteSpace(Services.SettingsService.Instance.Server.GroupFilter) ? null : + var filter = string.IsNullOrWhiteSpace(Services.SettingsService.Instance.Server.GroupFilter) ? null : Services.SettingsService.Instance.Server.GroupFilter; var searcher = new DirectorySearcher(entry, filter); var result = searcher.FindAll(); - PrintSearchResults(result); + var groups = new List(); + foreach(SearchResult item in result) + { + var group = new GroupEntry + { + DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault() + }; - return Task.FromResult(0); + if(group.DistinguishedName == null) + { + continue; + } + + // Name + if(item.Properties.Contains(Services.SettingsService.Instance.Server.GroupNameAttribute) && + item.Properties[Services.SettingsService.Instance.Server.GroupNameAttribute].Count > 0) + { + group.Name = item.Properties[Services.SettingsService.Instance.Server.GroupNameAttribute][0].ToString(); + } + else if(item.Properties.Contains("cn") && item.Properties["cn"].Count > 0) + { + group.Name = item.Properties["cn"][0].ToString(); + } + else + { + continue; + } + + // Dates + group.CreationDate = ParseDate(item.Properties, Services.SettingsService.Instance.Server.CreationDateAttribute); + group.RevisionDate = ParseDate(item.Properties, Services.SettingsService.Instance.Server.RevisionDateAttribute); + + // Members + if(item.Properties.Contains(Services.SettingsService.Instance.Server.MemberAttribute) && + item.Properties[Services.SettingsService.Instance.Server.MemberAttribute].Count > 0) + { + foreach(var member in item.Properties[Services.SettingsService.Instance.Server.MemberAttribute]) + { + var memberDn = member.ToString(); + if(!group.Members.Contains(memberDn)) + { + group.Members.Add(memberDn); + } + } + } + + groups.Add(group); + } + + return Task.FromResult(groups); } - public static Task SyncUsersAsync() + public static Task> SyncUsersAsync() { + if(!Services.SettingsService.Instance.Server.SyncUsers) + { + throw new ApplicationException("Not configured to sync users."); + } + if(Services.SettingsService.Instance.Server == null) { throw new ApplicationException("No configuration for directory server."); @@ -51,40 +109,101 @@ namespace Bit.Core.Utilities var searcher = new DirectorySearcher(entry, filter); var result = searcher.FindAll(); - PrintSearchResults(result); + var users = new List(); + foreach(SearchResult item in result) + { + var user = new UserEntry + { + DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault() + }; - return Task.FromResult(0); + if(user.DistinguishedName == null) + { + continue; + } + + // Email + if(Services.SettingsService.Instance.Server.EmailPrefixSuffix && + item.Properties.Contains(Services.SettingsService.Instance.Server.UserEmailPrefixAttribute) && + item.Properties[Services.SettingsService.Instance.Server.UserEmailPrefixAttribute].Count > 0 && + !string.IsNullOrWhiteSpace(Services.SettingsService.Instance.Server.UserEmailSuffix)) + { + user.Email = string.Concat( + item.Properties[Services.SettingsService.Instance.Server.UserEmailPrefixAttribute][0].ToString(), + Services.SettingsService.Instance.Server.UserEmailSuffix).ToLowerInvariant(); + } + else if(item.Properties.Contains(Services.SettingsService.Instance.Server.UserEmailAttribute) && + item.Properties[Services.SettingsService.Instance.Server.UserEmailAttribute].Count > 0) + { + user.Email = item.Properties[Services.SettingsService.Instance.Server.UserEmailAttribute][0] + .ToString() + .ToLowerInvariant(); + } + else + { + continue; + } + + // Dates + user.CreationDate = ParseDate(item.Properties, Services.SettingsService.Instance.Server.CreationDateAttribute); + user.RevisionDate = ParseDate(item.Properties, Services.SettingsService.Instance.Server.RevisionDateAttribute); + + users.Add(user); + } + + return Task.FromResult(users); } public static async Task SyncAllAsync() { - await SyncGroupsAsync(); - await SyncUsersAsync(); + List groups = null; + if(Services.SettingsService.Instance.Server.SyncGroups) + { + groups = await SyncGroupsAsync(); + } + + List users = null; + if(Services.SettingsService.Instance.Server.SyncUsers) + { + users = await SyncUsersAsync(); + } + + AssociateMembers(ref groups, ref users); } - private static void PrintSearchResults(SearchResultCollection result) + private static void AssociateMembers(ref List groups, ref List users) { - foreach(SearchResult item in result) + if(groups == null) { - Console.WriteLine(item.Path); + return; + } - foreach(DictionaryEntry prop in item.Properties) + foreach(var group in groups) + { + if(group.Members.Any()) { - Console.Write(" " + prop.Key + ": "); + group.GroupMembers = groups.Where(g => group.Members.Contains(g.DistinguishedName)).ToList(); - var vals = prop.Value as ResultPropertyValueCollection; - for(int i = 0; i < vals.Count; i++) + if(users != null) { - Console.Write(vals[i]); - if(i != vals.Count - 1) - { - Console.Write(" | "); - } + group.UserMembers = users.Where(u => group.Members.Contains(u.DistinguishedName)).ToList(); } - - Console.Write("\n"); } } + + // TODO: Handle nested group associations + } + + private static DateTime? ParseDate(ResultPropertyCollection collection, string dateKey) + { + DateTime date; + if(collection.Contains(dateKey) && collection[dateKey].Count > 0 && + DateTime.TryParse(collection[dateKey][0].ToString(), out date)) + { + return date; + } + + return null; } } }