diff --git a/src/Console/Program.cs b/src/Console/Program.cs index fe88473b..ccf88a3b 100644 --- a/src/Console/Program.cs +++ b/src/Console/Program.cs @@ -286,29 +286,52 @@ namespace Bit.Console } } - if(parameters.ContainsKey("a")) + if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory) { - config.Address = parameters["a"]; - } + config.Azure = new AzureConfiguration(); - if(parameters.ContainsKey("port")) - { - config.Port = parameters["port"]; - } + if(parameters.ContainsKey("i")) + { + config.Azure.Id = parameters["i"]; + } - if(parameters.ContainsKey("path")) - { - config.Path = parameters["path"]; - } + if(parameters.ContainsKey("s")) + { + config.Azure.Secret = new EncryptedData(parameters["s"]); + } - if(parameters.ContainsKey("u")) - { - config.Username = parameters["u"]; + if(parameters.ContainsKey("t")) + { + config.Azure.Tenant = parameters["t"]; + } } - - if(parameters.ContainsKey("p")) + else { - config.Password = new EncryptedData(parameters["p"]); + config.Ldap = config.Ldap ?? new LdapConfiguration(); + if(parameters.ContainsKey("a")) + { + config.Ldap.Address = parameters["a"]; + } + + if(parameters.ContainsKey("port")) + { + config.Ldap.Port = parameters["port"]; + } + + if(parameters.ContainsKey("path")) + { + config.Ldap.Path = parameters["path"]; + } + + if(parameters.ContainsKey("u")) + { + config.Ldap.Username = parameters["u"]; + } + + if(parameters.ContainsKey("p")) + { + config.Ldap.Password = new EncryptedData(parameters["p"]); + } } } else @@ -316,8 +339,8 @@ namespace Bit.Console string input; Con.WriteLine("1. Active Directory"); - //Con.WriteLine("2. Azure Active Directory "); - Con.WriteLine("2. Other LDAP Directory"); + Con.WriteLine("2. Azure Active Directory "); + Con.WriteLine("3. Other LDAP Directory"); string currentType; switch(config.Type) @@ -325,9 +348,12 @@ namespace Bit.Console case Core.Enums.DirectoryType.ActiveDirectory: currentType = "1"; break; - default: + case Core.Enums.DirectoryType.AzureActiveDirectory: currentType = "2"; break; + default: + currentType = "3"; + break; } Con.Write("Type [{0}]: ", currentType); input = Con.ReadLine(); @@ -338,44 +364,74 @@ namespace Bit.Console case "1": config.Type = Core.Enums.DirectoryType.ActiveDirectory; break; - //case "2": - // config.Type = Core.Enums.DirectoryType.AzureActiveCirectory; + case "2": + config.Type = Core.Enums.DirectoryType.AzureActiveDirectory; + break; default: config.Type = Core.Enums.DirectoryType.Other; break; } } - Con.Write("Address [{0}]: ", config.Address); - input = Con.ReadLine(); - if(!string.IsNullOrEmpty(input)) + if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory) { - config.Address = input; + config.Azure = config.Azure ?? new AzureConfiguration(); + + Con.Write("Tenant [{0}]: ", config.Azure.Tenant); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Azure.Tenant = input; + } + Con.Write("Application Id [{0}]: ", config.Azure.Id); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Azure.Id = input; + } + Con.Write("Secret key: "); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Azure.Secret = new EncryptedData(input); + input = null; + } } - Con.Write("Port [{0}]: ", config.Port); - input = Con.ReadLine(); - if(!string.IsNullOrEmpty(input)) + else { - config.Port = input; - } - Con.Write("Path [{0}]: ", config.Path); - input = Con.ReadLine(); - if(!string.IsNullOrEmpty(input)) - { - config.Path = input; - } - Con.Write("Username [{0}]: ", config.Username); - input = Con.ReadLine(); - if(!string.IsNullOrEmpty(input)) - { - config.Username = input; - } - Con.Write("Password: "); - input = ReadSecureLine(); - if(!string.IsNullOrEmpty(input)) - { - config.Password = new EncryptedData(input); - input = null; + config.Ldap = new LdapConfiguration(); + + Con.Write("Address [{0}]: ", config.Ldap.Address); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Ldap.Address = input; + } + Con.Write("Port [{0}]: ", config.Ldap.Port); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Ldap.Port = input; + } + Con.Write("Path [{0}]: ", config.Ldap.Path); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Ldap.Path = input; + } + Con.Write("Username [{0}]: ", config.Ldap.Username); + input = Con.ReadLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Ldap.Username = input; + } + Con.Write("Password: "); + input = ReadSecureLine(); + if(!string.IsNullOrEmpty(input)) + { + config.Ldap.Password = new EncryptedData(input); + input = null; + } } input = null; @@ -383,7 +439,14 @@ namespace Bit.Console Con.WriteLine(); Con.WriteLine(); - if(string.IsNullOrWhiteSpace(config.Address)) + if(config.Ldap != null && string.IsNullOrWhiteSpace(config.Ldap.Address)) + { + Con.ForegroundColor = ConsoleColor.Red; + Con.WriteLine("Invalid input parameters."); + Con.ResetColor(); + } + else if(config.Azure != null && (string.IsNullOrWhiteSpace(config.Azure.Id) || + config.Azure.Secret == null || string.IsNullOrWhiteSpace(config.Azure.Tenant))) { Con.ForegroundColor = ConsoleColor.Red; Con.WriteLine("Invalid input parameters."); @@ -601,7 +664,7 @@ namespace Bit.Console Con.WriteLine("Groups:"); foreach(var group in result.Groups) { - Con.WriteLine(" {0} - {1}", group.Name, group.DistinguishedName); + Con.WriteLine(" {0} - {1}", group.Name, group.Id); } Con.WriteLine(); diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index cb41e8fc..034dbabd 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -33,6 +33,18 @@ ..\..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll + + ..\..\packages\Microsoft.Graph.1.3.0\lib\net45\Microsoft.Graph.dll + + + ..\..\packages\Microsoft.Graph.Core.1.4.0\lib\net45\Microsoft.Graph.Core.dll + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.9\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.9\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll + ..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll @@ -56,10 +68,12 @@ + + - + @@ -72,6 +86,7 @@ + diff --git a/src/Core/Models/AzureConfiguration.cs b/src/Core/Models/AzureConfiguration.cs new file mode 100644 index 00000000..cd7258a0 --- /dev/null +++ b/src/Core/Models/AzureConfiguration.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; + +namespace Bit.Core.Models +{ + public class AzureConfiguration + { + public string Tenant { get; set; } = "yourcompany.onmicrosoft.com"; + public string Id { get; set; } + public EncryptedData Secret { get; set; } + } +} diff --git a/src/Core/Models/Entry.cs b/src/Core/Models/Entry.cs index 351e1ea0..df412cce 100644 --- a/src/Core/Models/Entry.cs +++ b/src/Core/Models/Entry.cs @@ -8,7 +8,7 @@ namespace Bit.Core.Models { public abstract class Entry { - public string DistinguishedName { get; set; } + public string Id { get; set; } public DateTime? CreationDate { get; set; } public DateTime? RevisionDate { get; set; } } diff --git a/src/Core/Models/ImportRequest.cs b/src/Core/Models/ImportRequest.cs index 619ad841..77d91b7c 100644 --- a/src/Core/Models/ImportRequest.cs +++ b/src/Core/Models/ImportRequest.cs @@ -19,7 +19,7 @@ namespace Bit.Core.Models public Group(GroupEntry entry) { Name = entry.Name; - ExternalId = entry.DistinguishedName; + ExternalId = entry.Id; } public string Name { get; set; } diff --git a/src/Core/Models/LdapConfiguration.cs b/src/Core/Models/LdapConfiguration.cs new file mode 100644 index 00000000..763b954b --- /dev/null +++ b/src/Core/Models/LdapConfiguration.cs @@ -0,0 +1,28 @@ +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 LdapConfiguration + { + public string Address { get; set; } + public string Port { get; set; } = "389"; + public string Path { get; set; } + public string Username { get; set; } + public EncryptedData Password { get; set; } + [JsonIgnore] + public string ServerPath => $"LDAP://{Address}:{Port}/{Path}"; + public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory; + + public DirectoryEntry GetDirectoryEntry() + { + var entry = new DirectoryEntry(ServerPath, Username, Password.DecryptToString(), AuthenticationTypes.None); + return entry; + } + } +} diff --git a/src/Core/Models/ServerConfiguration.cs b/src/Core/Models/ServerConfiguration.cs index fecf81d4..fbdffe92 100644 --- a/src/Core/Models/ServerConfiguration.cs +++ b/src/Core/Models/ServerConfiguration.cs @@ -10,19 +10,8 @@ namespace Bit.Core.Models { public class ServerConfiguration { - public string Address { get; set; } - public string Port { get; set; } = "389"; - public string Path { get; set; } - public string Username { get; set; } - public EncryptedData Password { get; set; } - [JsonIgnore] - public string ServerPath => $"LDAP://{Address}:{Port}/{Path}"; public Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory; - - public DirectoryEntry GetDirectoryEntry() - { - var entry = new DirectoryEntry(ServerPath, Username, Password.DecryptToString(), AuthenticationTypes.None); - return entry; - } + public LdapConfiguration Ldap { get; set; } + public AzureConfiguration Azure { get; set; } } } diff --git a/src/Core/Services/AzureDirectoryService.cs b/src/Core/Services/AzureDirectoryService.cs index b54c57d8..fd33d79f 100644 --- a/src/Core/Services/AzureDirectoryService.cs +++ b/src/Core/Services/AzureDirectoryService.cs @@ -2,14 +2,23 @@ using System; using System.Threading.Tasks; using System.Collections.Generic; +using Microsoft.Graph; +using System.Net.Http.Headers; +using System.Diagnostics; +using System.Linq; +using Bit.Core.Utilities; namespace Bit.Core.Services { public class AzureDirectoryService : IDirectoryService { private static AzureDirectoryService _instance; + private static GraphServiceClient _graphClient; - private AzureDirectoryService() { } + private AzureDirectoryService() + { + _graphClient = new GraphServiceClient(new AzureAuthenticationProvider()); + } public static IDirectoryService Instance { @@ -24,9 +33,112 @@ namespace Bit.Core.Services } } - public Task, List>> GetEntriesAsync(bool force = false) + public async Task, List>> GetEntriesAsync(bool force = false) { - throw new NotImplementedException(); + if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet) + { + throw new ApplicationException("Not logged in or have an org set."); + } + + if(SettingsService.Instance.Server?.Azure == null) + { + throw new ApplicationException("No configuration for directory server."); + } + + if(SettingsService.Instance.Sync == null) + { + throw new ApplicationException("No configuration for sync."); + } + + List groups = null; + if(SettingsService.Instance.Sync.SyncGroups) + { + groups = await GetGroupsAsync(force); + } + + List users = null; + if(SettingsService.Instance.Sync.SyncUsers) + { + users = await GetUsersAsync(force); + } + + return new Tuple, List>(groups, users); + } + + private async static Task> GetGroupsAsync(bool force = false) + { + if(!SettingsService.Instance.Sync.SyncGroups) + { + throw new ApplicationException("Not configured to sync groups."); + } + + if(SettingsService.Instance.Server?.Azure == null) + { + 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 entries = new List(); + + var groups = await _graphClient.Groups.Request().GetAsync(); + foreach(var group in groups) + { + entries.Add(new GroupEntry + { + Id = group.Id, + Name = group.DisplayName + }); + } + + return entries; + } + + private async static Task> GetUsersAsync(bool force = false) + { + if(!SettingsService.Instance.Sync.SyncUsers) + { + throw new ApplicationException("Not configured to sync users."); + } + + if(SettingsService.Instance.Server?.Azure == null) + { + 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 entries = new List(); + + var users = await _graphClient.Users.Request().GetAsync(); + foreach(var user in users) + { + var entry = new UserEntry + { + Id = user.Id, + Email = user.Mail + }; + + entries.Add(entry); + } + + return entries; } } } diff --git a/src/Core/Services/LdapDirectoryService.cs b/src/Core/Services/LdapDirectoryService.cs index 27795cb3..b3eb72c4 100644 --- a/src/Core/Services/LdapDirectoryService.cs +++ b/src/Core/Services/LdapDirectoryService.cs @@ -34,7 +34,7 @@ namespace Bit.Core.Services throw new ApplicationException("Not logged in or have an org set."); } - if(SettingsService.Instance.Server == null) + if(SettingsService.Instance.Server?.Ldap == null) { throw new ApplicationException("No configuration for directory server."); } @@ -66,7 +66,7 @@ namespace Bit.Core.Services throw new ApplicationException("Not configured to sync groups."); } - if(SettingsService.Instance.Server == null) + if(SettingsService.Instance.Server?.Ldap == null) { throw new ApplicationException("No configuration for directory server."); } @@ -81,7 +81,7 @@ namespace Bit.Core.Services throw new ApplicationException("Not authenticated."); } - var entry = SettingsService.Instance.Server.GetDirectoryEntry(); + var entry = SettingsService.Instance.Server.Ldap.GetDirectoryEntry(); var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.GroupFilter) ? null : SettingsService.Instance.Sync.GroupFilter; @@ -102,10 +102,10 @@ namespace Bit.Core.Services { var group = new GroupEntry { - DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault() + Id = new Uri(item.Path).Segments?.LastOrDefault() }; - if(group.DistinguishedName == null) + if(group.Id == null) { continue; } @@ -156,7 +156,7 @@ namespace Bit.Core.Services throw new ApplicationException("Not configured to sync users."); } - if(SettingsService.Instance.Server == null) + if(SettingsService.Instance.Server?.Ldap == null) { throw new ApplicationException("No configuration for directory server."); } @@ -171,7 +171,7 @@ namespace Bit.Core.Services throw new ApplicationException("Not authenticated."); } - var entry = SettingsService.Instance.Server.GetDirectoryEntry(); + var entry = SettingsService.Instance.Server.Ldap.GetDirectoryEntry(); var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserFilter) ? null : SettingsService.Instance.Sync.UserFilter; @@ -192,10 +192,10 @@ namespace Bit.Core.Services { var user = new UserEntry { - DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault() + Id = new Uri(item.Path).Segments?.LastOrDefault() }; - if(user.DistinguishedName == null) + if(user.Id == null) { continue; } diff --git a/src/Core/Utilities/AzureAuthenticationProvider.cs b/src/Core/Utilities/AzureAuthenticationProvider.cs new file mode 100644 index 00000000..f1fd44a7 --- /dev/null +++ b/src/Core/Utilities/AzureAuthenticationProvider.cs @@ -0,0 +1,33 @@ +using Bit.Core.Services; +using Microsoft.Graph; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Bit.Core.Utilities +{ + public class AzureAuthenticationProvider : IAuthenticationProvider + { + public async Task AuthenticateRequestAsync(HttpRequestMessage request) + { + if(SettingsService.Instance.Server?.Azure == null) + { + throw new ApplicationException("No server configuration."); + } + + var authContext = new AuthenticationContext( + $"https://login.windows.net/{SettingsService.Instance.Server.Azure.Tenant}/oauth2/token"); + var creds = new ClientCredential(SettingsService.Instance.Server.Azure.Id, + SettingsService.Instance.Server.Azure.Secret.DecryptToString()); + var authResult = await authContext.AcquireTokenAsync("https://graph.microsoft.com/", creds); + request.Headers.Add("Authorization", $"Bearer {authResult.AccessToken}"); + } + + // ref: https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/511 + private static void SomeMethodToLinkPlatform() + { + var creds = new UserPasswordCredential("user", "pass"); + } + } +} diff --git a/src/Core/Utilities/Sync.cs b/src/Core/Utilities/Sync.cs index 2f2eea43..ec3e5fea 100644 --- a/src/Core/Utilities/Sync.cs +++ b/src/Core/Utilities/Sync.cs @@ -86,14 +86,14 @@ namespace Bit.Core.Utilities { foreach(var group in currentGroups) { - var groupsInThisGroup = allGroups.Where(g => group.Members.Contains(g.DistinguishedName)).ToList(); - var usersInThisGroup = allUsers.Where(u => group.Members.Contains(u.DistinguishedName)).ToList(); + var groupsInThisGroup = allGroups.Where(g => group.Members.Contains(g.Id)).ToList(); + var usersInThisGroup = allUsers.Where(u => group.Members.Contains(u.Id)).ToList(); foreach(var user in usersInThisGroup) { - if(!user.Groups.Contains(group.DistinguishedName)) + if(!user.Groups.Contains(group.Id)) { - user.Groups.Add(group.DistinguishedName); + user.Groups.Add(group.Id); } } @@ -101,9 +101,9 @@ namespace Bit.Core.Utilities { foreach(var user in currentGroupsUsers) { - if(!user.Groups.Contains(group.DistinguishedName)) + if(!user.Groups.Contains(group.Id)) { - user.Groups.Add(group.DistinguishedName); + user.Groups.Add(group.Id); } } diff --git a/src/Core/packages.config b/src/Core/packages.config index e51e4099..2acd2287 100644 --- a/src/Core/packages.config +++ b/src/Core/packages.config @@ -1,5 +1,9 @@  + + + + \ No newline at end of file