mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-21 10:43:16 +00:00
Interface out directory service for Azure implem.
This commit is contained in:
@@ -555,7 +555,7 @@ namespace Bit.Console
|
||||
}
|
||||
|
||||
Con.WriteLine("Syncing...");
|
||||
var result = await Sync.SyncAllAsync(force);
|
||||
var result = await Sync.SyncAllAsync(force, true);
|
||||
|
||||
if(result.Success)
|
||||
{
|
||||
@@ -595,7 +595,7 @@ namespace Bit.Console
|
||||
Con.WriteLine("Querying...");
|
||||
Con.WriteLine();
|
||||
|
||||
var result = await Sync.GatherAsync(force);
|
||||
var result = await Sync.SyncAllAsync(force, false);
|
||||
if(result.Success)
|
||||
{
|
||||
Con.WriteLine("Groups:");
|
||||
|
||||
@@ -68,6 +68,9 @@
|
||||
<Compile Include="Models\ProfileResponse.cs" />
|
||||
<Compile Include="Models\TokenResponse.cs" />
|
||||
<Compile Include="Services\ApiService.cs" />
|
||||
<Compile Include="Services\AzureDirectoryService.cs" />
|
||||
<Compile Include="Services\LdapDirectoryService.cs" />
|
||||
<Compile Include="Services\IDirectoryService.cs" />
|
||||
<Compile Include="Services\SettingsService.cs" />
|
||||
<Compile Include="Utilities\Crypto.cs" />
|
||||
<Compile Include="Services\TokenService.cs" />
|
||||
|
||||
32
src/Core/Services/AzureDirectoryService.cs
Normal file
32
src/Core/Services/AzureDirectoryService.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class AzureDirectoryService : IDirectoryService
|
||||
{
|
||||
private static AzureDirectoryService _instance;
|
||||
|
||||
private AzureDirectoryService() { }
|
||||
|
||||
public static IDirectoryService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new AzureDirectoryService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Core/Services/IDirectoryService.cs
Normal file
14
src/Core/Services/IDirectoryService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Bit.Core.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IDirectoryService
|
||||
{
|
||||
Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false);
|
||||
}
|
||||
}
|
||||
235
src/Core/Services/LdapDirectoryService.cs
Normal file
235
src/Core/Services/LdapDirectoryService.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.DirectoryServices;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class LdapDirectoryService : IDirectoryService
|
||||
{
|
||||
private static LdapDirectoryService _instance;
|
||||
|
||||
private LdapDirectoryService() { }
|
||||
|
||||
public static IDirectoryService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(_instance == null)
|
||||
{
|
||||
_instance = new LdapDirectoryService();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<List<GroupEntry>, List<UserEntry>>> GetEntriesAsync(bool force = false)
|
||||
{
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
{
|
||||
throw new ApplicationException("Not logged in or have an org set.");
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server == 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 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 == 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 entry = SettingsService.Instance.Server.GetDirectoryEntry();
|
||||
var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.GroupFilter) ? null :
|
||||
SettingsService.Instance.Sync.GroupFilter;
|
||||
|
||||
if(!force && !string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.RevisionDateAttribute) &&
|
||||
SettingsService.Instance.LastGroupSyncDate.HasValue)
|
||||
{
|
||||
filter = string.Format("(&{0}({1}>{2}))",
|
||||
filter != null ? string.Format("({0})", filter) : string.Empty,
|
||||
SettingsService.Instance.Sync.RevisionDateAttribute,
|
||||
SettingsService.Instance.LastGroupSyncDate.Value.ToGeneralizedTimeUTC());
|
||||
}
|
||||
|
||||
var searcher = new DirectorySearcher(entry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var groups = new List<GroupEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var group = new GroupEntry
|
||||
{
|
||||
DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault()
|
||||
};
|
||||
|
||||
if(group.DistinguishedName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Name
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.GroupNameAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.GroupNameAttribute].Count > 0)
|
||||
{
|
||||
group.Name = item.Properties[SettingsService.Instance.Sync.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 = item.Properties.ParseDateTime(SettingsService.Instance.Sync.CreationDateAttribute);
|
||||
group.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.RevisionDateAttribute);
|
||||
|
||||
// Members
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.MemberAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.MemberAttribute].Count > 0)
|
||||
{
|
||||
foreach(var member in item.Properties[SettingsService.Instance.Sync.MemberAttribute])
|
||||
{
|
||||
var memberDn = member.ToString();
|
||||
if(!group.Members.Contains(memberDn))
|
||||
{
|
||||
group.Members.Add(memberDn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.Add(group);
|
||||
}
|
||||
|
||||
return Task.FromResult(groups);
|
||||
}
|
||||
|
||||
private 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 == 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 entry = SettingsService.Instance.Server.GetDirectoryEntry();
|
||||
var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserFilter) ? null :
|
||||
SettingsService.Instance.Sync.UserFilter;
|
||||
|
||||
if(!force && !string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.RevisionDateAttribute) &&
|
||||
SettingsService.Instance.LastUserSyncDate.HasValue)
|
||||
{
|
||||
filter = string.Format("(&{0}({1}>{2}))",
|
||||
filter != null ? string.Format("({0})", filter) : string.Empty,
|
||||
SettingsService.Instance.Sync.RevisionDateAttribute,
|
||||
SettingsService.Instance.LastUserSyncDate.Value.ToGeneralizedTimeUTC());
|
||||
}
|
||||
|
||||
var searcher = new DirectorySearcher(entry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var users = new List<UserEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var user = new UserEntry
|
||||
{
|
||||
DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault()
|
||||
};
|
||||
|
||||
if(user.DistinguishedName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Email
|
||||
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.Sync.UserEmailPrefixAttribute][0].ToString(),
|
||||
SettingsService.Instance.Sync.UserEmailSuffix).ToLowerInvariant();
|
||||
}
|
||||
else if(item.Properties.Contains(SettingsService.Instance.Sync.UserEmailAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.UserEmailAttribute].Count > 0)
|
||||
{
|
||||
user.Email = item.Properties[SettingsService.Instance.Sync.UserEmailAttribute][0]
|
||||
.ToString()
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dates
|
||||
user.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.CreationDateAttribute);
|
||||
user.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.RevisionDateAttribute);
|
||||
|
||||
users.Add(user);
|
||||
}
|
||||
|
||||
return Task.FromResult(users);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Services;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.DirectoryServices;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -11,16 +9,28 @@ namespace Bit.Core.Utilities
|
||||
{
|
||||
public static class Sync
|
||||
{
|
||||
public static async Task<SyncResult> SyncAllAsync(bool force = false)
|
||||
public static async Task<SyncResult> SyncAllAsync(bool force = false, bool sendToServer = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var gatherResult = await GatherAsync(force);
|
||||
if(!gatherResult.Success)
|
||||
var entriesResult = await GetDirectoryService().GetEntriesAsync(force);
|
||||
var groups = entriesResult.Item1;
|
||||
var users = entriesResult.Item2;
|
||||
|
||||
FlattenGroupsToUsers(groups, null, groups, users);
|
||||
|
||||
if(!sendToServer)
|
||||
{
|
||||
return gatherResult;
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
|
||||
var request = new ImportRequest(gatherResult.Groups, gatherResult.Users);
|
||||
var request = new ImportRequest(groups, users);
|
||||
var response = await ApiService.Instance.PostImportAsync(request);
|
||||
if(response.Succeeded)
|
||||
{
|
||||
@@ -37,8 +47,8 @@ namespace Bit.Core.Utilities
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = gatherResult.Groups,
|
||||
Users = gatherResult.Users
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
else
|
||||
@@ -50,229 +60,25 @@ namespace Bit.Core.Utilities
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<SyncResult> GatherAsync(bool force = false)
|
||||
{
|
||||
if(!AuthService.Instance.Authenticated || !AuthService.Instance.OrganizationSet)
|
||||
catch (ApplicationException e)
|
||||
{
|
||||
return new SyncResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Not logged in or have an org set."
|
||||
ErrorMessage = e.Message
|
||||
};
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Server == null)
|
||||
{
|
||||
return new SyncResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "No configuration for directory server."
|
||||
};
|
||||
}
|
||||
|
||||
if(SettingsService.Instance.Sync == null)
|
||||
private static IDirectoryService GetDirectoryService()
|
||||
{
|
||||
return new SyncResult
|
||||
switch(SettingsService.Instance.Server.Type)
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "No configuration for sync."
|
||||
};
|
||||
case Enums.DirectoryType.AzureActiveDirectory:
|
||||
return AzureDirectoryService.Instance;
|
||||
default:
|
||||
return LdapDirectoryService.Instance;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
FlattenGroupsToUsers(groups, null, groups, users);
|
||||
|
||||
return new SyncResult
|
||||
{
|
||||
Success = true,
|
||||
Groups = groups,
|
||||
Users = users
|
||||
};
|
||||
}
|
||||
|
||||
private 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 == 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 entry = SettingsService.Instance.Server.GetDirectoryEntry();
|
||||
var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.GroupFilter) ? null :
|
||||
SettingsService.Instance.Sync.GroupFilter;
|
||||
|
||||
if(!force && !string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.RevisionDateAttribute) &&
|
||||
SettingsService.Instance.LastGroupSyncDate.HasValue)
|
||||
{
|
||||
filter = string.Format("(&{0}({1}>{2}))",
|
||||
filter != null ? string.Format("({0})", filter) : string.Empty,
|
||||
SettingsService.Instance.Sync.RevisionDateAttribute,
|
||||
SettingsService.Instance.LastGroupSyncDate.Value.ToGeneralizedTimeUTC());
|
||||
}
|
||||
|
||||
var searcher = new DirectorySearcher(entry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var groups = new List<GroupEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var group = new GroupEntry
|
||||
{
|
||||
DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault()
|
||||
};
|
||||
|
||||
if(group.DistinguishedName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Name
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.GroupNameAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.GroupNameAttribute].Count > 0)
|
||||
{
|
||||
group.Name = item.Properties[SettingsService.Instance.Sync.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 = item.Properties.ParseDateTime(SettingsService.Instance.Sync.CreationDateAttribute);
|
||||
group.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.RevisionDateAttribute);
|
||||
|
||||
// Members
|
||||
if(item.Properties.Contains(SettingsService.Instance.Sync.MemberAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.MemberAttribute].Count > 0)
|
||||
{
|
||||
foreach(var member in item.Properties[SettingsService.Instance.Sync.MemberAttribute])
|
||||
{
|
||||
var memberDn = member.ToString();
|
||||
if(!group.Members.Contains(memberDn))
|
||||
{
|
||||
group.Members.Add(memberDn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.Add(group);
|
||||
}
|
||||
|
||||
return Task.FromResult(groups);
|
||||
}
|
||||
|
||||
private 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 == 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 entry = SettingsService.Instance.Server.GetDirectoryEntry();
|
||||
var filter = string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.UserFilter) ? null :
|
||||
SettingsService.Instance.Sync.UserFilter;
|
||||
|
||||
if(!force && !string.IsNullOrWhiteSpace(SettingsService.Instance.Sync.RevisionDateAttribute) &&
|
||||
SettingsService.Instance.LastUserSyncDate.HasValue)
|
||||
{
|
||||
filter = string.Format("(&{0}({1}>{2}))",
|
||||
filter != null ? string.Format("({0})", filter) : string.Empty,
|
||||
SettingsService.Instance.Sync.RevisionDateAttribute,
|
||||
SettingsService.Instance.LastUserSyncDate.Value.ToGeneralizedTimeUTC());
|
||||
}
|
||||
|
||||
var searcher = new DirectorySearcher(entry, filter);
|
||||
var result = searcher.FindAll();
|
||||
|
||||
var users = new List<UserEntry>();
|
||||
foreach(SearchResult item in result)
|
||||
{
|
||||
var user = new UserEntry
|
||||
{
|
||||
DistinguishedName = new Uri(item.Path).Segments?.LastOrDefault()
|
||||
};
|
||||
|
||||
if(user.DistinguishedName == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Email
|
||||
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.Sync.UserEmailPrefixAttribute][0].ToString(),
|
||||
SettingsService.Instance.Sync.UserEmailSuffix).ToLowerInvariant();
|
||||
}
|
||||
else if(item.Properties.Contains(SettingsService.Instance.Sync.UserEmailAttribute) &&
|
||||
item.Properties[SettingsService.Instance.Sync.UserEmailAttribute].Count > 0)
|
||||
{
|
||||
user.Email = item.Properties[SettingsService.Instance.Sync.UserEmailAttribute][0]
|
||||
.ToString()
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dates
|
||||
user.CreationDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.CreationDateAttribute);
|
||||
user.RevisionDate = item.Properties.ParseDateTime(SettingsService.Instance.Sync.RevisionDateAttribute);
|
||||
|
||||
users.Add(user);
|
||||
}
|
||||
|
||||
return Task.FromResult(users);
|
||||
}
|
||||
|
||||
private static void FlattenGroupsToUsers(List<GroupEntry> currentGroups, List<UserEntry> currentGroupsUsers,
|
||||
|
||||
Reference in New Issue
Block a user