1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-14 23:33:19 +00:00

azure directory service implementation w/ config

This commit is contained in:
Kyle Spearrin
2017-05-15 11:08:06 -04:00
parent 6ede5550b8
commit db1ead6754
12 changed files with 342 additions and 85 deletions

View File

@@ -286,29 +286,52 @@ namespace Bit.Console
}
}
if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory)
{
config.Azure = new AzureConfiguration();
if(parameters.ContainsKey("i"))
{
config.Azure.Id = parameters["i"];
}
if(parameters.ContainsKey("s"))
{
config.Azure.Secret = new EncryptedData(parameters["s"]);
}
if(parameters.ContainsKey("t"))
{
config.Azure.Tenant = parameters["t"];
}
}
else
{
config.Ldap = config.Ldap ?? new LdapConfiguration();
if(parameters.ContainsKey("a"))
{
config.Address = parameters["a"];
config.Ldap.Address = parameters["a"];
}
if(parameters.ContainsKey("port"))
{
config.Port = parameters["port"];
config.Ldap.Port = parameters["port"];
}
if(parameters.ContainsKey("path"))
{
config.Path = parameters["path"];
config.Ldap.Path = parameters["path"];
}
if(parameters.ContainsKey("u"))
{
config.Username = parameters["u"];
config.Ldap.Username = parameters["u"];
}
if(parameters.ContainsKey("p"))
{
config.Password = new EncryptedData(parameters["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,52 +364,89 @@ 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);
if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory)
{
config.Azure = config.Azure ?? new AzureConfiguration();
Con.Write("Tenant [{0}]: ", config.Azure.Tenant);
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{
config.Address = input;
config.Azure.Tenant = input;
}
Con.Write("Port [{0}]: ", config.Port);
Con.Write("Application Id [{0}]: ", config.Azure.Id);
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{
config.Port = input;
config.Azure.Id = input;
}
Con.Write("Path [{0}]: ", config.Path);
Con.Write("Secret key: ");
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{
config.Path = input;
config.Azure.Secret = new EncryptedData(input);
input = null;
}
Con.Write("Username [{0}]: ", config.Username);
}
else
{
config.Ldap = new LdapConfiguration();
Con.Write("Address [{0}]: ", config.Ldap.Address);
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{
config.Username = 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.Password = new EncryptedData(input);
config.Ldap.Password = new EncryptedData(input);
input = null;
}
}
input = null;
}
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();

View File

@@ -33,6 +33,18 @@
<Reference Include="BouncyCastle.Crypto, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
<HintPath>..\..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Graph, Version=1.3.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Graph.1.3.0\lib\net45\Microsoft.Graph.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Graph.Core, Version=1.4.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.Graph.Core.1.4.0\lib\net45\Microsoft.Graph.Core.dll</HintPath>
</Reference>
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory, Version=3.13.9.1126, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.9\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll</HintPath>
</Reference>
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform, Version=3.13.9.1126, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.9\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
@@ -56,10 +68,12 @@
<Compile Include="Models\ApiResult.cs" />
<Compile Include="Models\Entry.cs" />
<Compile Include="Models\ImportRequest.cs" />
<Compile Include="Models\AzureConfiguration.cs" />
<Compile Include="Models\ServerConfiguration.cs" />
<Compile Include="Models\Organization.cs" />
<Compile Include="Models\ProfileOrganizationResponse.cs" />
<Compile Include="Models\SyncConfiguration.cs" />
<Compile Include="Models\ServerConfiguration.cs" />
<Compile Include="Models\LdapConfiguration.cs" />
<Compile Include="Models\LoginResult.cs" />
<Compile Include="Models\ErrorResponse.cs" />
<Compile Include="Models\EncryptedData.cs" />
@@ -72,6 +86,7 @@
<Compile Include="Services\LdapDirectoryService.cs" />
<Compile Include="Services\IDirectoryService.cs" />
<Compile Include="Services\SettingsService.cs" />
<Compile Include="Utilities\AzureAuthenticationProvider.cs" />
<Compile Include="Utilities\Crypto.cs" />
<Compile Include="Services\TokenService.cs" />
<Compile Include="Services\AuthService.cs" />

View File

@@ -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; }
}
}

View File

@@ -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; }
}

View File

@@ -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; }

View File

@@ -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;
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> 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<GroupEntry> groups = null;
if(SettingsService.Instance.Sync.SyncGroups)
{
groups = await GetGroupsAsync(force);
}
List<UserEntry> users = null;
if(SettingsService.Instance.Sync.SyncUsers)
{
users = await GetUsersAsync(force);
}
return new Tuple<List<GroupEntry>, List<UserEntry>>(groups, users);
}
private async static Task<List<GroupEntry>> 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<GroupEntry>();
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<List<UserEntry>> 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<UserEntry>();
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;
}
}
}

View File

@@ -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;
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="BouncyCastle" version="1.8.1" targetFramework="net452" />
<package id="Microsoft.Graph" version="1.3.0" targetFramework="net452" />
<package id="Microsoft.Graph.Core" version="1.4.0" targetFramework="net452" />
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="3.13.9" targetFramework="net452" />
<package id="Newtonsoft.Json" version="10.0.2" targetFramework="net452" />
<package id="System.Net.Http" version="4.3.2" targetFramework="net452" />
</packages>