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(parameters.ContainsKey("a")) if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory)
{ {
config.Address = parameters["a"]; config.Azure = new AzureConfiguration();
}
if(parameters.ContainsKey("port")) if(parameters.ContainsKey("i"))
{ {
config.Port = parameters["port"]; config.Azure.Id = parameters["i"];
} }
if(parameters.ContainsKey("path")) if(parameters.ContainsKey("s"))
{ {
config.Path = parameters["path"]; config.Azure.Secret = new EncryptedData(parameters["s"]);
} }
if(parameters.ContainsKey("u")) if(parameters.ContainsKey("t"))
{ {
config.Username = parameters["u"]; config.Azure.Tenant = parameters["t"];
}
} }
else
if(parameters.ContainsKey("p"))
{ {
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 else
@@ -316,8 +339,8 @@ namespace Bit.Console
string input; string input;
Con.WriteLine("1. Active Directory"); Con.WriteLine("1. Active Directory");
//Con.WriteLine("2. Azure Active Directory "); Con.WriteLine("2. Azure Active Directory ");
Con.WriteLine("2. Other LDAP Directory"); Con.WriteLine("3. Other LDAP Directory");
string currentType; string currentType;
switch(config.Type) switch(config.Type)
@@ -325,9 +348,12 @@ namespace Bit.Console
case Core.Enums.DirectoryType.ActiveDirectory: case Core.Enums.DirectoryType.ActiveDirectory:
currentType = "1"; currentType = "1";
break; break;
default: case Core.Enums.DirectoryType.AzureActiveDirectory:
currentType = "2"; currentType = "2";
break; break;
default:
currentType = "3";
break;
} }
Con.Write("Type [{0}]: ", currentType); Con.Write("Type [{0}]: ", currentType);
input = Con.ReadLine(); input = Con.ReadLine();
@@ -338,44 +364,74 @@ namespace Bit.Console
case "1": case "1":
config.Type = Core.Enums.DirectoryType.ActiveDirectory; config.Type = Core.Enums.DirectoryType.ActiveDirectory;
break; break;
//case "2": case "2":
// config.Type = Core.Enums.DirectoryType.AzureActiveCirectory; config.Type = Core.Enums.DirectoryType.AzureActiveDirectory;
break;
default: default:
config.Type = Core.Enums.DirectoryType.Other; config.Type = Core.Enums.DirectoryType.Other;
break; break;
} }
} }
Con.Write("Address [{0}]: ", config.Address); if(config.Type == Core.Enums.DirectoryType.AzureActiveDirectory)
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{ {
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); else
input = Con.ReadLine();
if(!string.IsNullOrEmpty(input))
{ {
config.Port = input; config.Ldap = new LdapConfiguration();
}
Con.Write("Path [{0}]: ", config.Path); Con.Write("Address [{0}]: ", config.Ldap.Address);
input = Con.ReadLine(); input = Con.ReadLine();
if(!string.IsNullOrEmpty(input)) if(!string.IsNullOrEmpty(input))
{ {
config.Path = input; config.Ldap.Address = input;
} }
Con.Write("Username [{0}]: ", config.Username); Con.Write("Port [{0}]: ", config.Ldap.Port);
input = Con.ReadLine(); input = Con.ReadLine();
if(!string.IsNullOrEmpty(input)) if(!string.IsNullOrEmpty(input))
{ {
config.Username = input; config.Ldap.Port = input;
} }
Con.Write("Password: "); Con.Write("Path [{0}]: ", config.Ldap.Path);
input = ReadSecureLine(); input = Con.ReadLine();
if(!string.IsNullOrEmpty(input)) if(!string.IsNullOrEmpty(input))
{ {
config.Password = new EncryptedData(input); config.Ldap.Path = input;
input = null; }
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; input = null;
@@ -383,7 +439,14 @@ namespace Bit.Console
Con.WriteLine(); Con.WriteLine();
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.ForegroundColor = ConsoleColor.Red;
Con.WriteLine("Invalid input parameters."); Con.WriteLine("Invalid input parameters.");
@@ -601,7 +664,7 @@ namespace Bit.Console
Con.WriteLine("Groups:"); Con.WriteLine("Groups:");
foreach(var group in result.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(); Con.WriteLine();

View File

@@ -33,6 +33,18 @@
<Reference Include="BouncyCastle.Crypto, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942"> <Reference Include="BouncyCastle.Crypto, Version=1.8.1.0, Culture=neutral, PublicKeyToken=0e99375e54769942">
<HintPath>..\..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll</HintPath> <HintPath>..\..\packages\BouncyCastle.1.8.1\lib\BouncyCastle.Crypto.dll</HintPath>
</Reference> </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"> <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> <HintPath>..\..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference> </Reference>
@@ -56,10 +68,12 @@
<Compile Include="Models\ApiResult.cs" /> <Compile Include="Models\ApiResult.cs" />
<Compile Include="Models\Entry.cs" /> <Compile Include="Models\Entry.cs" />
<Compile Include="Models\ImportRequest.cs" /> <Compile Include="Models\ImportRequest.cs" />
<Compile Include="Models\AzureConfiguration.cs" />
<Compile Include="Models\ServerConfiguration.cs" />
<Compile Include="Models\Organization.cs" /> <Compile Include="Models\Organization.cs" />
<Compile Include="Models\ProfileOrganizationResponse.cs" /> <Compile Include="Models\ProfileOrganizationResponse.cs" />
<Compile Include="Models\SyncConfiguration.cs" /> <Compile Include="Models\SyncConfiguration.cs" />
<Compile Include="Models\ServerConfiguration.cs" /> <Compile Include="Models\LdapConfiguration.cs" />
<Compile Include="Models\LoginResult.cs" /> <Compile Include="Models\LoginResult.cs" />
<Compile Include="Models\ErrorResponse.cs" /> <Compile Include="Models\ErrorResponse.cs" />
<Compile Include="Models\EncryptedData.cs" /> <Compile Include="Models\EncryptedData.cs" />
@@ -72,6 +86,7 @@
<Compile Include="Services\LdapDirectoryService.cs" /> <Compile Include="Services\LdapDirectoryService.cs" />
<Compile Include="Services\IDirectoryService.cs" /> <Compile Include="Services\IDirectoryService.cs" />
<Compile Include="Services\SettingsService.cs" /> <Compile Include="Services\SettingsService.cs" />
<Compile Include="Utilities\AzureAuthenticationProvider.cs" />
<Compile Include="Utilities\Crypto.cs" /> <Compile Include="Utilities\Crypto.cs" />
<Compile Include="Services\TokenService.cs" /> <Compile Include="Services\TokenService.cs" />
<Compile Include="Services\AuthService.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 abstract class Entry
{ {
public string DistinguishedName { get; set; } public string Id { get; set; }
public DateTime? CreationDate { get; set; } public DateTime? CreationDate { get; set; }
public DateTime? RevisionDate { get; set; } public DateTime? RevisionDate { get; set; }
} }

View File

@@ -19,7 +19,7 @@ namespace Bit.Core.Models
public Group(GroupEntry entry) public Group(GroupEntry entry)
{ {
Name = entry.Name; Name = entry.Name;
ExternalId = entry.DistinguishedName; ExternalId = entry.Id;
} }
public string Name { get; set; } 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 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 Enums.DirectoryType Type { get; set; } = Enums.DirectoryType.ActiveDirectory;
public LdapConfiguration Ldap { get; set; }
public DirectoryEntry GetDirectoryEntry() public AzureConfiguration Azure { get; set; }
{
var entry = new DirectoryEntry(ServerPath, Username, Password.DecryptToString(), AuthenticationTypes.None);
return entry;
}
} }
} }

View File

@@ -2,14 +2,23 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; 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 namespace Bit.Core.Services
{ {
public class AzureDirectoryService : IDirectoryService public class AzureDirectoryService : IDirectoryService
{ {
private static AzureDirectoryService _instance; private static AzureDirectoryService _instance;
private static GraphServiceClient _graphClient;
private AzureDirectoryService() { } private AzureDirectoryService()
{
_graphClient = new GraphServiceClient(new AzureAuthenticationProvider());
}
public static IDirectoryService Instance 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."); 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."); throw new ApplicationException("No configuration for directory server.");
} }
@@ -66,7 +66,7 @@ namespace Bit.Core.Services
throw new ApplicationException("Not configured to sync groups."); 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."); throw new ApplicationException("No configuration for directory server.");
} }
@@ -81,7 +81,7 @@ namespace Bit.Core.Services
throw new ApplicationException("Not authenticated."); 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 : var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.GroupFilter) ? null :
SettingsService.Instance.Sync.GroupFilter; SettingsService.Instance.Sync.GroupFilter;
@@ -102,10 +102,10 @@ namespace Bit.Core.Services
{ {
var group = new GroupEntry 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; continue;
} }
@@ -156,7 +156,7 @@ namespace Bit.Core.Services
throw new ApplicationException("Not configured to sync users."); 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."); throw new ApplicationException("No configuration for directory server.");
} }
@@ -171,7 +171,7 @@ namespace Bit.Core.Services
throw new ApplicationException("Not authenticated."); 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 : var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserFilter) ? null :
SettingsService.Instance.Sync.UserFilter; SettingsService.Instance.Sync.UserFilter;
@@ -192,10 +192,10 @@ namespace Bit.Core.Services
{ {
var user = new UserEntry 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; 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) foreach(var group in currentGroups)
{ {
var groupsInThisGroup = allGroups.Where(g => group.Members.Contains(g.DistinguishedName)).ToList(); var groupsInThisGroup = allGroups.Where(g => group.Members.Contains(g.Id)).ToList();
var usersInThisGroup = allUsers.Where(u => group.Members.Contains(u.DistinguishedName)).ToList(); var usersInThisGroup = allUsers.Where(u => group.Members.Contains(u.Id)).ToList();
foreach(var user in usersInThisGroup) 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) 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"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="BouncyCastle" version="1.8.1" targetFramework="net452" /> <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="Newtonsoft.Json" version="10.0.2" targetFramework="net452" />
<package id="System.Net.Http" version="4.3.2" targetFramework="net452" />
</packages> </packages>