diff --git a/src/Console/Program.cs b/src/Console/Program.cs index 353f598b..1408c686 100644 --- a/src/Console/Program.cs +++ b/src/Console/Program.cs @@ -42,9 +42,10 @@ namespace Bit.Console Con.WriteLine("1. Log in to bitwarden"); Con.WriteLine("2. Log out"); Con.WriteLine("3. Configure directory connection"); - Con.WriteLine("4. Sync directory"); - Con.WriteLine("5. Start/stop background service"); - Con.WriteLine("6. Exit"); + Con.WriteLine("4. Configure sync"); + Con.WriteLine("5. Sync directory"); + Con.WriteLine("6. Start/stop background service"); + Con.WriteLine("7. Exit"); Con.WriteLine(); Con.Write("What would you like to do? "); selection = Con.ReadLine(); @@ -64,11 +65,16 @@ namespace Bit.Console await LogOutAsync(); break; case "3": - case "dir": - case "directory": - await DirectoryAsync(); + case "cdir": + case "configdirectory": + await ConfigDirectoryAsync(); break; case "4": + case "csync": + case "configsync": + await ConfigSyncAsync(); + break; + case "5": case "sync": await SyncAsync(); break; @@ -165,7 +171,7 @@ namespace Bit.Console Con.WriteLine("Two-step login is enabled on this account. Please enter your verification code."); Con.Write("Verification code: "); token = Con.ReadLine().Trim(); - result = await Core.Services.AuthService.Instance.LogInTwoFactorWithHashAsync(token, email, + result = await Core.Services.AuthService.Instance.LogInTwoFactorWithHashAsync(token, email, result.MasterPasswordHash); } @@ -240,7 +246,7 @@ namespace Bit.Console return Task.FromResult(0); } - private static Task DirectoryAsync() + private static Task ConfigDirectoryAsync() { var config = new ServerConfiguration(); @@ -271,16 +277,6 @@ namespace Bit.Console { config.Password = new EncryptedData(parameters["p"]); } - - if(parameters.ContainsKey("gf")) - { - config.GroupFilter = parameters["gf"]; - } - - if(parameters.ContainsKey("uf")) - { - config.UserFilter = parameters["uf"]; - } } else { @@ -305,7 +301,76 @@ namespace Bit.Console config.Password = new EncryptedData(input); input = null; } - Con.WriteLine(); + + input = null; + } + + Con.WriteLine(); + Con.WriteLine(); + if(string.IsNullOrWhiteSpace(config.Address)) + { + Con.ForegroundColor = ConsoleColor.Red; + Con.WriteLine("Invalid input parameters."); + Con.ResetColor(); + } + else + { + Core.Services.SettingsService.Instance.Server = config; + Con.ForegroundColor = ConsoleColor.Green; + Con.WriteLine("Saved directory server configuration."); + Con.ResetColor(); + } + + return Task.FromResult(0); + } + + private static Task ConfigSyncAsync() + { + var config = new SyncConfiguration(); + + if(_usingArgs) + { + var parameters = ParseParameters(); + + config.SyncGroups = parameters.ContainsKey("g"); + if(parameters.ContainsKey("gf")) + { + config.GroupFilter = parameters["gf"]; + } + if(parameters.ContainsKey("gn")) + { + config.GroupNameAttribute = parameters["gn"]; + } + + config.SyncGroups = parameters.ContainsKey("u"); + if(parameters.ContainsKey("uf")) + { + config.UserFilter = parameters["uf"]; + } + if(parameters.ContainsKey("ue")) + { + config.UserEmailAttribute = parameters["ue"]; + } + + if(parameters.ContainsKey("m")) + { + config.MemberAttribute = parameters["m"]; + } + + if(parameters.ContainsKey("c")) + { + config.CreationDateAttribute = parameters["c"]; + } + + if(parameters.ContainsKey("r")) + { + config.RevisionDateAttribute = parameters["r"]; + } + } + else + { + string input; + Con.Write("Sync groups? [y]: "); input = Con.ReadLine().Trim().ToLower(); config.SyncGroups = input == "y" || input == "yes" || string.IsNullOrWhiteSpace(input); @@ -343,24 +408,34 @@ namespace Bit.Console } } + Con.Write("Member Of Attribute [{0}]: ", config.MemberAttribute); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.MemberAttribute = input; + } + Con.Write("Creation Attribute [{0}]: ", config.CreationDateAttribute); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.CreationDateAttribute = input; + } + Con.Write("Changed Attribute [{0}]: ", config.RevisionDateAttribute); + input = Con.ReadLine().Trim(); + if(!string.IsNullOrWhiteSpace(input)) + { + config.RevisionDateAttribute = input; + } + input = null; } Con.WriteLine(); Con.WriteLine(); - if(string.IsNullOrWhiteSpace(config.Address)) - { - Con.ForegroundColor = ConsoleColor.Red; - Con.WriteLine("Invalid input parameters."); - Con.ResetColor(); - } - else - { - Core.Services.SettingsService.Instance.Server = config; - Con.ForegroundColor = ConsoleColor.Green; - Con.WriteLine("Saved directory server configuration."); - Con.ResetColor(); - } + Core.Services.SettingsService.Instance.Sync = config; + Con.ForegroundColor = ConsoleColor.Green; + Con.WriteLine("Saved sync configuration."); + Con.ResetColor(); return Task.FromResult(0); } @@ -378,10 +453,21 @@ namespace Bit.Console else { Con.WriteLine("Syncing..."); - await Sync.SyncAllAsync(); - Con.ForegroundColor = ConsoleColor.Green; - Con.WriteLine("Syncing complete."); - Con.ResetColor(); + var result = await Sync.SyncAllAsync(); + + if(result.Success) + { + Con.ForegroundColor = ConsoleColor.Green; + Con.WriteLine("Syncing complete ({0} users, {1} groups).", result.UserCount, result.GroupCount); + Con.ResetColor(); + } + else + { + Con.ForegroundColor = ConsoleColor.Red; + Con.WriteLine("Syncing failed."); + Con.WriteLine(result.ErrorMessage); + Con.ResetColor(); + } } } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index bc41741e..ce41eaaf 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -57,10 +57,12 @@ + + diff --git a/src/Core/Models/Organization.cs b/src/Core/Models/Organization.cs index d5179e04..58fd85c3 100644 --- a/src/Core/Models/Organization.cs +++ b/src/Core/Models/Organization.cs @@ -8,6 +8,8 @@ namespace Bit.Core.Models { public class Organization { + public Organization() { } + public Organization(ProfileOrganizationResponseModel org) { Name = org.Name; diff --git a/src/Core/Models/ServerConfiguration.cs b/src/Core/Models/ServerConfiguration.cs index 544f269b..a1edb87e 100644 --- a/src/Core/Models/ServerConfiguration.cs +++ b/src/Core/Models/ServerConfiguration.cs @@ -17,19 +17,7 @@ namespace Bit.Core.Models public EncryptedData Password { get; set; } [JsonIgnore] public string ServerPath => $"LDAP://{Address}:{Port}/{Path}"; - 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/Models/SyncConfiguration.cs b/src/Core/Models/SyncConfiguration.cs new file mode 100644 index 00000000..92e7e3f5 --- /dev/null +++ b/src/Core/Models/SyncConfiguration.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Models +{ + public class SyncConfiguration + { + 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 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"; + } +} diff --git a/src/Core/Models/SyncResult.cs b/src/Core/Models/SyncResult.cs new file mode 100644 index 00000000..290e9cbe --- /dev/null +++ b/src/Core/Models/SyncResult.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bit.Core.Models +{ + public class SyncResult + { + public bool Success { get; set; } + public string ErrorMessage { get; set; } + public int GroupCount { get; set; } + public int UserCount { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index a02f4582..48f3b6da 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -98,7 +98,7 @@ namespace Bit.Core.Services var requestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri(ApiClient.BaseAddress, "import"), // TODO: org id + RequestUri = new Uri(ApiClient.BaseAddress, $"organizations/{SettingsService.Instance.Organization.Id}/import"), Content = new StringContent(stringContent, Encoding.UTF8, "application/json"), }; diff --git a/src/Core/Services/SettingsService.cs b/src/Core/Services/SettingsService.cs index 77a13655..bea3c8e3 100644 --- a/src/Core/Services/SettingsService.cs +++ b/src/Core/Services/SettingsService.cs @@ -153,6 +153,19 @@ namespace Bit.Core.Services } } + public SyncConfiguration Sync + { + get + { + return Settings.Sync; + } + set + { + Settings.Sync = value; + SaveSettings(); + } + } + public class SettingsModel { public string ApiBaseUrl { get; set; } @@ -160,6 +173,7 @@ namespace Bit.Core.Services public EncryptedData AccessToken { get; set; } public EncryptedData RefreshToken { get; set; } public ServerConfiguration Server { get; set; } + public SyncConfiguration Sync { get; set; } public Organization Organization { get; set; } } } diff --git a/src/Core/Utilities/Sync.cs b/src/Core/Utilities/Sync.cs index 442d5a78..2d1229b4 100644 --- a/src/Core/Utilities/Sync.cs +++ b/src/Core/Utilities/Sync.cs @@ -11,16 +11,43 @@ namespace Bit.Core.Utilities { public static class Sync { - public static async Task SyncAllAsync() + public static async Task SyncAllAsync() { + if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet) + { + return new SyncResult + { + Success = false, + ErrorMessage = "Not logged in or have an org set." + }; + } + + if(SettingsService.Instance.Server == null) + { + return new SyncResult + { + Success = false, + ErrorMessage = "No configuration for directory server." + }; + } + + if(SettingsService.Instance.Sync == null) + { + return new SyncResult + { + Success = false, + ErrorMessage = "No configuration for sync." + }; + } + List groups = null; - if(SettingsService.Instance.Server.SyncGroups) + if(SettingsService.Instance.Sync.SyncGroups) { groups = await GetGroupsAsync(); } List users = null; - if(SettingsService.Instance.Server.SyncUsers) + if(SettingsService.Instance.Sync.SyncUsers) { users = await GetUsersAsync(); } @@ -28,12 +55,29 @@ namespace Bit.Core.Utilities FlattenGroupsToUsers(groups, null, groups, users); var request = new ImportRequest(groups, users); - await ApiService.Instance.PostImportAsync(request); + var response = await ApiService.Instance.PostImportAsync(request); + if(response.Succeeded) + { + return new SyncResult + { + Success = true, + GroupCount = groups.Count, + UserCount = users.Count + }; + } + else + { + return new SyncResult + { + Success = false, + ErrorMessage = response.Errors.FirstOrDefault()?.Message + }; + } } private static Task> GetGroupsAsync() { - if(!SettingsService.Instance.Server.SyncGroups) + if(!SettingsService.Instance.Sync.SyncGroups) { throw new ApplicationException("Not configured to sync groups."); } @@ -43,14 +87,19 @@ namespace Bit.Core.Utilities throw new ApplicationException("No configuration for directory server."); } + if(SettingsService.Instance.Sync == null) + { + throw new ApplicationException("No configuration for sync."); + } + if(!AuthService.Instance.Authenticated) { throw new ApplicationException("Not authenticated."); } var entry = SettingsService.Instance.Server.GetDirectoryEntry(); - var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Server.GroupFilter) ? null : - SettingsService.Instance.Server.GroupFilter; + var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.GroupFilter) ? null : + SettingsService.Instance.Sync.GroupFilter; var searcher = new DirectorySearcher(entry, filter); var result = searcher.FindAll(); @@ -68,10 +117,10 @@ namespace Bit.Core.Utilities } // Name - if(item.Properties.Contains(SettingsService.Instance.Server.GroupNameAttribute) && - item.Properties[SettingsService.Instance.Server.GroupNameAttribute].Count > 0) + if(item.Properties.Contains(SettingsService.Instance.Sync.GroupNameAttribute) && + item.Properties[SettingsService.Instance.Sync.GroupNameAttribute].Count > 0) { - group.Name = item.Properties[SettingsService.Instance.Server.GroupNameAttribute][0].ToString(); + group.Name = item.Properties[SettingsService.Instance.Sync.GroupNameAttribute][0].ToString(); } else if(item.Properties.Contains("cn") && item.Properties["cn"].Count > 0) { @@ -83,14 +132,14 @@ namespace Bit.Core.Utilities } // Dates - group.CreationDate = ParseDate(item.Properties, SettingsService.Instance.Server.CreationDateAttribute); - group.RevisionDate = ParseDate(item.Properties, SettingsService.Instance.Server.RevisionDateAttribute); + group.CreationDate = ParseDate(item.Properties, SettingsService.Instance.Sync.CreationDateAttribute); + group.RevisionDate = ParseDate(item.Properties, SettingsService.Instance.Sync.RevisionDateAttribute); // Members - if(item.Properties.Contains(SettingsService.Instance.Server.MemberAttribute) && - item.Properties[SettingsService.Instance.Server.MemberAttribute].Count > 0) + if(item.Properties.Contains(SettingsService.Instance.Sync.MemberAttribute) && + item.Properties[SettingsService.Instance.Sync.MemberAttribute].Count > 0) { - foreach(var member in item.Properties[SettingsService.Instance.Server.MemberAttribute]) + foreach(var member in item.Properties[SettingsService.Instance.Sync.MemberAttribute]) { var memberDn = member.ToString(); if(!group.Members.Contains(memberDn)) @@ -108,7 +157,7 @@ namespace Bit.Core.Utilities private static Task> GetUsersAsync() { - if(!SettingsService.Instance.Server.SyncUsers) + if(!SettingsService.Instance.Sync.SyncUsers) { throw new ApplicationException("Not configured to sync users."); } @@ -118,14 +167,19 @@ namespace Bit.Core.Utilities throw new ApplicationException("No configuration for directory server."); } + if(SettingsService.Instance.Sync == null) + { + throw new ApplicationException("No configuration for sync."); + } + if(!AuthService.Instance.Authenticated) { throw new ApplicationException("Not authenticated."); } var entry = SettingsService.Instance.Server.GetDirectoryEntry(); - var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Server.UserFilter) ? null : - SettingsService.Instance.Server.UserFilter; + var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserFilter) ? null : + SettingsService.Instance.Sync.UserFilter; var searcher = new DirectorySearcher(entry, filter); var result = searcher.FindAll(); @@ -143,19 +197,19 @@ namespace Bit.Core.Utilities } // Email - if(SettingsService.Instance.Server.EmailPrefixSuffix && - item.Properties.Contains(SettingsService.Instance.Server.UserEmailPrefixAttribute) && - item.Properties[SettingsService.Instance.Server.UserEmailPrefixAttribute].Count > 0 && - !string.IsNullOrWhiteSpace(SettingsService.Instance.Server.UserEmailSuffix)) + if(SettingsService.Instance.Sync.EmailPrefixSuffix && + item.Properties.Contains(SettingsService.Instance.Sync.UserEmailPrefixAttribute) && + item.Properties[SettingsService.Instance.Sync.UserEmailPrefixAttribute].Count > 0 && + !string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserEmailSuffix)) { user.Email = string.Concat( - item.Properties[SettingsService.Instance.Server.UserEmailPrefixAttribute][0].ToString(), - SettingsService.Instance.Server.UserEmailSuffix).ToLowerInvariant(); + item.Properties[SettingsService.Instance.Sync.UserEmailPrefixAttribute][0].ToString(), + SettingsService.Instance.Sync.UserEmailSuffix).ToLowerInvariant(); } - else if(item.Properties.Contains(SettingsService.Instance.Server.UserEmailAttribute) && - item.Properties[SettingsService.Instance.Server.UserEmailAttribute].Count > 0) + else if(item.Properties.Contains(SettingsService.Instance.Sync.UserEmailAttribute) && + item.Properties[SettingsService.Instance.Sync.UserEmailAttribute].Count > 0) { - user.Email = item.Properties[SettingsService.Instance.Server.UserEmailAttribute][0] + user.Email = item.Properties[SettingsService.Instance.Sync.UserEmailAttribute][0] .ToString() .ToLowerInvariant(); } @@ -165,8 +219,8 @@ namespace Bit.Core.Utilities } // Dates - user.CreationDate = ParseDate(item.Properties, SettingsService.Instance.Server.CreationDateAttribute); - user.RevisionDate = ParseDate(item.Properties, SettingsService.Instance.Server.RevisionDateAttribute); + user.CreationDate = ParseDate(item.Properties, SettingsService.Instance.Sync.CreationDateAttribute); + user.RevisionDate = ParseDate(item.Properties, SettingsService.Instance.Sync.RevisionDateAttribute); users.Add(user); }