Feature #49: Active Directory Managed Groups

Document Template Attachments, Device Batches, Device Profiles and User
Flags can be associated with an Active Directory group. This AD group is
then automatically synchronized with relevant User/Machine accounts.
Contains various other UI tweaks and configuration enhancements.
This commit is contained in:
Gary Sharp
2014-06-16 22:21:31 +10:00
parent ebf78dd08d
commit a819d2722a
119 changed files with 8349 additions and 2373 deletions
+1 -20
View File
@@ -18,26 +18,7 @@ namespace Disco.Services
public static string FriendlyId(this User u)
{
return FriendlyUserId(u.UserId);
}
public static string FriendlyUserId(string UserId)
{
var splitUserId = SplitUserId(UserId);
if (splitUserId.Item1 != null && splitUserId.Item1.Equals(ActiveDirectory.Context.PrimaryDomain.NetBiosName, StringComparison.OrdinalIgnoreCase))
return splitUserId.Item2;
else
return UserId;
}
public static Tuple<string, string> SplitUserId(string UserId)
{
var slashIndex = UserId.IndexOf('\\');
if (slashIndex < 0)
return Tuple.Create<string, string>(null, UserId);
else
return Tuple.Create(UserId.Substring(0, slashIndex), UserId.Substring(slashIndex + 1));
return ActiveDirectory.FriendlyAccountId(u.UserId);
}
}
}
+2 -21
View File
@@ -42,28 +42,9 @@ namespace Disco.Services.Users.UserFlags
return _Cache.Values.ToList();
}
public UserFlag Update(UserFlag UserFlag)
public void AddOrUpdate(UserFlag UserFlag)
{
UserFlag existingItem;
if (_Cache.TryGetValue(UserFlag.Id, out existingItem))
{
if (_Cache.TryUpdate(UserFlag.Id, UserFlag, existingItem))
{
return UserFlag;
}
else
return null;
}
else
{
if (_Cache.TryAdd(UserFlag.Id, UserFlag))
{
return UserFlag;
}
else
return null;
}
_Cache.AddOrUpdate(UserFlag.Id, UserFlag, (key, existingItem) => UserFlag);
}
public UserFlag Remove(int UserFlagId)
+100 -32
View File
@@ -1,4 +1,5 @@
using Disco.Data.Repository;
using Disco.Data.Repository.Monitor;
using Disco.Models.Repository;
using Disco.Services.Extensions;
using Disco.Services.Tasks;
@@ -9,16 +10,39 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reactive.Linq;
namespace Disco.Services.Users.UserFlags
{
public static class UserFlagService
{
private static Cache _cache;
internal static Lazy<IObservable<RepositoryMonitorEvent>> UserFlagAssignmentRepositoryEvents;
static UserFlagService()
{
// Statically defined (lazy) Assignment Repository Definition
// Used by UserFlagAssignedUsersManagedGroup & UserFlagAssignedUserDevicesManagedGroup
UserFlagAssignmentRepositoryEvents =
new Lazy<IObservable<RepositoryMonitorEvent>>(() =>
RepositoryMonitor.StreamAfterCommit.Where(e =>
e.EntityType == typeof(UserFlagAssignment) &&
(e.EventType != RepositoryMonitorEventType.Modified ||
e.ModifiedProperties.Contains("RemovedDate"))
)
);
}
public static void Initialize(DiscoDataContext Database)
{
_cache = new Cache(Database);
// Initialize Managed Groups (if configured)
_cache.GetUserFlags().ForEach(uf =>
{
UserFlagUsersManagedGroup.Initialize(uf);
UserFlagUserDevicesManagedGroup.Initialize(uf);
});
}
public static List<UserFlag> GetUserFlags() { return _cache.GetUserFlags(); }
@@ -41,13 +65,17 @@ namespace Disco.Services.Users.UserFlags
Name = UserFlag.Name,
Description = UserFlag.Description,
Icon = UserFlag.Icon,
IconColour = UserFlag.IconColour
IconColour = UserFlag.IconColour,
UsersLinkedGroup = UserFlag.UsersLinkedGroup,
UserDevicesLinkedGroup = UserFlag.UserDevicesLinkedGroup
};
Database.UserFlags.Add(flag);
Database.SaveChanges();
return _cache.Update(flag);
_cache.AddOrUpdate(flag);
return flag;
}
public static UserFlag Update(DiscoDataContext Database, UserFlag UserFlag)
{
@@ -61,12 +89,20 @@ namespace Disco.Services.Users.UserFlags
Database.SaveChanges();
return _cache.Update(UserFlag);
_cache.AddOrUpdate(UserFlag);
UserFlagUsersManagedGroup.Initialize(UserFlag);
UserFlagUserDevicesManagedGroup.Initialize(UserFlag);
return UserFlag;
}
public static void DeleteUserFlag(DiscoDataContext Database, int UserFlagId, IScheduledTaskStatus Status)
{
UserFlag flag = Database.UserFlags.Find(UserFlagId);
// Dispose of AD Managed Groups
Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(UserFlagUserDevicesManagedGroup.GetKey(flag));
Interop.ActiveDirectory.ActiveDirectory.Context.ManagedGroups.Remove(UserFlagUsersManagedGroup.GetKey(flag));
// Delete Assignments
Status.UpdateStatus(0, string.Format("Removing '{0}' [{1}] User Flag", flag.Name, flag.Id), "Starting");
List<UserFlagAssignment> flagAssignments = Database.UserFlagAssignments.Where(fa => fa.UserFlagId == flag.Id).ToList();
@@ -94,34 +130,43 @@ namespace Disco.Services.Users.UserFlags
{
if (Users.Count > 0)
{
double progressInterval;
const int databaseChunkSize = 100;
string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim();
var addUsers = Users.Where(u => !u.UserFlagAssignments.Any(a => a.UserFlagId == UserFlag.Id && !a.RemovedDate.HasValue)).ToList();
progressInterval = (double)100 / addUsers.Count;
var addedUserAssignments = addUsers.Select((user, index) =>
var addedUserAssignments = addUsers.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
{
Status.UpdateStatus(index * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));
var chunkIndexOffset = databaseChunkSize * chunkIndex;
var fa = new UserFlagAssignment()
var chunkResults = chunk.Select((user, index) =>
{
UserFlagId = UserFlag.Id,
UserId = user.UserId,
AddedDate = DateTime.Now,
AddedUserId = Technician.UserId,
Comments = comments
};
Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));
Database.UserFlagAssignments.Add(fa);
var fa = new UserFlagAssignment()
{
UserFlagId = UserFlag.Id,
UserId = user.UserId,
AddedDate = DateTime.Now,
AddedUserId = Technician.UserId,
Comments = comments
};
Database.UserFlagAssignments.Add(fa);
return fa;
}).ToList();
// Save Chunk Items to Database
Database.SaveChanges();
return fa;
return chunkResults;
}).Where(fa => fa != null).ToList();
Status.SetFinishedMessage(string.Format("{0} Users/s Added; {1} User/s Skipped", addUsers.Count, (Users.Count - addUsers.Count)));
return addedUserAssignments;
}
else
@@ -134,6 +179,7 @@ namespace Disco.Services.Users.UserFlags
public static IEnumerable<UserFlagAssignment> BulkAssignOverrideUsers(DiscoDataContext Database, UserFlag UserFlag, User Technician, string Comments, List<User> Users, IScheduledTaskStatus Status)
{
double progressInterval;
const int databaseChunkSize = 100;
string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim();
Status.UpdateStatus(0, "Calculating assignment changes");
@@ -147,31 +193,53 @@ namespace Disco.Services.Users.UserFlags
progressInterval = (double)100 / (removeAssignments.Count + addUsers.Count);
var removedDateTime = DateTime.Now;
removeAssignments.Select((flagAssignment, index) =>
// Remove Assignments
removeAssignments.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
{
Status.UpdateStatus(index * progressInterval, string.Format("Removing Flag: {0}", flagAssignment.User.ToString()));
flagAssignment.RemovedDate = removedDateTime;
flagAssignment.RemovedUserId = Technician.UserId;
var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count;
var chunkResults = chunk.Select((flagAssignment, index) =>
{
Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Removing Flag: {0}", flagAssignment.User.ToString()));
flagAssignment.RemovedDate = removedDateTime;
flagAssignment.RemovedUserId = Technician.UserId;
return flagAssignment;
}).ToList();
// Save Chunk Items to Database
Database.SaveChanges();
return flagAssignment;
return chunkResults;
}).ToList();
var addedUserAssignments = addUsers.Select((user, index) =>
// Add Assignments
var addedUserAssignments = addUsers.Chunk(databaseChunkSize).SelectMany((chunk, chunkIndex) =>
{
Status.UpdateStatus((removeAssignments.Count + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));
var chunkIndexOffset = (chunkIndex * databaseChunkSize) + removeAssignments.Count;
var fa = new UserFlagAssignment()
var chunkResults = chunk.Select((user, index) =>
{
UserFlagId = UserFlag.Id,
UserId = user.UserId,
AddedDate = DateTime.Now,
AddedUserId = Technician.UserId,
Comments = comments
};
Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString()));
Database.UserFlagAssignments.Add(fa);
var fa = new UserFlagAssignment()
{
UserFlagId = UserFlag.Id,
UserId = user.UserId,
AddedDate = DateTime.Now,
AddedUserId = Technician.UserId,
Comments = comments
};
Database.UserFlagAssignments.Add(fa);
return fa;
}).ToList();
// Save Chunk Items to Database
Database.SaveChanges();
return fa;
return chunkResults;
}).ToList();
Status.SetFinishedMessage(string.Format("{0} Users/s Added; {1} User/s Removed; {2} User/s Skipped", addUsers.Count, removeAssignments.Count, (Users.Count - addUsers.Count)));
@@ -0,0 +1,167 @@
using Disco.Data.Repository;
using Disco.Data.Repository.Monitor;
using Disco.Models.Repository;
using Disco.Models.Services.Interop.ActiveDirectory;
using Disco.Services.Interop.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Disco.Services.Users.UserFlags
{
public class UserFlagUserDevicesManagedGroup : ADManagedGroup
{
private const string KeyFormat = "UserFlag_{0}_UserDevices";
private const string DescriptionFormat = "User associated with the {0} Flag will have their assigned Devices added to this Active Directory group.";
private const string CategoryDescriptionFormat = "Assigned User Devices Linked Group";
private const string GroupDescriptionFormat = "{0} [User Flag User Devices]";
private IDisposable repositorySubscription;
private int UserFlagId;
private string UserFlagName;
public override string Description { get { return string.Format(DescriptionFormat, UserFlagName); } }
public override string CategoryDescription { get { return CategoryDescriptionFormat; } }
public override string GroupDescription { get { return string.Format(GroupDescriptionFormat, UserFlagName); } }
public override bool IncludeFilterBeginDate { get { return false; } }
private UserFlagUserDevicesManagedGroup(string Key, ADManagedGroupConfiguration Configuration, UserFlag UserFlag)
: base(Key, Configuration)
{
this.UserFlagId = UserFlag.Id;
this.UserFlagName = UserFlag.Name;
}
public override void Initialize()
{
// Subscribe to changes
repositorySubscription = UserFlagService.UserFlagAssignmentRepositoryEvents.Value
.Where(e =>
(((UserFlagAssignment)e.Entity).UserFlagId == UserFlagId))
.Subscribe(ProcessRepositoryEvent);
}
public static string GetKey(UserFlag UserFlag)
{
return string.Format(KeyFormat, UserFlag.Id);
}
public static string GetDescription(UserFlag UserFlag)
{
return string.Format(DescriptionFormat, UserFlag.Name);
}
public static string GetCategoryDescription(UserFlag UserFlag)
{
return CategoryDescriptionFormat;
}
public static bool TryGetManagedGroup(UserFlag UserFlag, out UserFlagUserDevicesManagedGroup ManagedGroup)
{
ADManagedGroup managedGroup;
string key = GetKey(UserFlag);
if (ActiveDirectory.Context.ManagedGroups.TryGetValue(key, out managedGroup))
{
ManagedGroup = (UserFlagUserDevicesManagedGroup)managedGroup;
return true;
}
else
{
ManagedGroup = null;
return false;
}
}
public static UserFlagUserDevicesManagedGroup Initialize(UserFlag UserFlag)
{
if (UserFlag.Id > 0)
{
var key = GetKey(UserFlag);
if (!string.IsNullOrEmpty(UserFlag.UserDevicesLinkedGroup))
{
var config = ADManagedGroup.ConfigurationFromJson(UserFlag.UserDevicesLinkedGroup);
if (config != null && !string.IsNullOrWhiteSpace(config.GroupId))
{
var group = new UserFlagUserDevicesManagedGroup(
key,
config,
UserFlag);
// Add to AD Context
ActiveDirectory.Context.ManagedGroups.AddOrUpdate(group);
return group;
}
}
// Remove from AD Context
ActiveDirectory.Context.ManagedGroups.Remove(key);
}
return null;
}
private IEnumerable<string> DetermineDeviceMembers(DiscoDataContext Database, string UserId)
{
return DetermineDeviceMembers(Database.Users.Where(u => u.UserId == UserId));
}
private IEnumerable<string> DetermineDeviceMembers(IQueryable<User> Users)
{
return Users
.SelectMany(u => u.DeviceUserAssignments)
.Where(da => !da.UnassignedDate.HasValue && da.Device.DeviceDomainId != null)
.Select(da => da.Device.DeviceDomainId)
.ToList()
.Where(ActiveDirectory.IsValidDomainAccountId)
.Select(id => id + "$");
}
public override IEnumerable<string> DetermineMembers(DiscoDataContext Database)
{
var assignments = Database.UserFlagAssignments
.Where(a => a.UserFlagId == UserFlagId && !a.RemovedDate.HasValue)
.Select(a => a.User);
return DetermineDeviceMembers(assignments);
}
private void ProcessRepositoryEvent(RepositoryMonitorEvent Event)
{
var userFlagAssignemnt = (UserFlagAssignment)Event.Entity;
string userId = userFlagAssignemnt.UserId;
switch (Event.EventType)
{
case RepositoryMonitorEventType.Added:
if (!userFlagAssignemnt.RemovedDate.HasValue)
AddMember(userFlagAssignemnt.UserId, (database) => DetermineDeviceMembers(database, userId));
break;
case RepositoryMonitorEventType.Modified:
if (userFlagAssignemnt.RemovedDate.HasValue)
RemoveMember(userFlagAssignemnt.UserId, (database) => DetermineDeviceMembers(database, userId));
else
AddMember(userFlagAssignemnt.UserId, (database) => DetermineDeviceMembers(database, userId));
break;
case RepositoryMonitorEventType.Deleted:
// Remove the user's devices if no other (non-removed) assignments exist.
RemoveMember(userId, (database) =>
{
if (database.UserFlagAssignments.Any(a => a.UserFlagId == UserFlagId && a.UserId == userId && !a.RemovedDate.HasValue))
return null;
else
return DetermineDeviceMembers(database, userId);
});
break;
}
}
public override void Dispose()
{
if (repositorySubscription != null)
repositorySubscription.Dispose();
}
}
}
@@ -0,0 +1,149 @@
using Disco.Data.Repository;
using Disco.Data.Repository.Monitor;
using Disco.Models.Repository;
using Disco.Models.Services.Interop.ActiveDirectory;
using Disco.Services.Interop.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
namespace Disco.Services.Users.UserFlags
{
public class UserFlagUsersManagedGroup : ADManagedGroup
{
private const string KeyFormat = "UserFlag_{0}_Users";
private const string DescriptionFormat = "User associated with the {0} Flag will be added to this Active Directory group.";
private const string CategoryDescriptionFormat = "Assigned Users Linked Group";
private const string GroupDescriptionFormat = "{0} [User Flag Users]";
private IDisposable repositorySubscription;
private int UserFlagId;
private string UserFlagName;
public override string Description { get { return string.Format(DescriptionFormat, UserFlagName); } }
public override string CategoryDescription { get { return CategoryDescriptionFormat; } }
public override string GroupDescription { get { return string.Format(GroupDescriptionFormat, UserFlagName); } }
public override bool IncludeFilterBeginDate { get { return false; } }
private UserFlagUsersManagedGroup(string Key, ADManagedGroupConfiguration Configuration, UserFlag UserFlag)
: base(Key, Configuration)
{
this.UserFlagId = UserFlag.Id;
this.UserFlagName = UserFlag.Name;
}
public override void Initialize()
{
// Subscribe to changes
repositorySubscription = UserFlagService.UserFlagAssignmentRepositoryEvents.Value
.Where(e =>
(((UserFlagAssignment)e.Entity).UserFlagId == UserFlagId))
.Subscribe(ProcessRepositoryEvent);
}
public static string GetKey(UserFlag UserFlag)
{
return string.Format(KeyFormat, UserFlag.Id);
}
public static string GetDescription(UserFlag UserFlag)
{
return string.Format(DescriptionFormat, UserFlag.Name);
}
public static string GetCategoryDescription(UserFlag UserFlag)
{
return CategoryDescriptionFormat;
}
public static bool TryGetManagedGroup(UserFlag UserFlag, out UserFlagUsersManagedGroup ManagedGroup)
{
ADManagedGroup managedGroup;
string key = GetKey(UserFlag);
if (ActiveDirectory.Context.ManagedGroups.TryGetValue(key, out managedGroup))
{
ManagedGroup = (UserFlagUsersManagedGroup)managedGroup;
return true;
}
else
{
ManagedGroup = null;
return false;
}
}
public static UserFlagUsersManagedGroup Initialize(UserFlag UserFlag)
{
if (UserFlag.Id > 0)
{
var key = GetKey(UserFlag);
if (!string.IsNullOrEmpty(UserFlag.UsersLinkedGroup))
{
var config = ADManagedGroup.ConfigurationFromJson(UserFlag.UsersLinkedGroup);
if (config != null && !string.IsNullOrWhiteSpace(config.GroupId))
{
var group = new UserFlagUsersManagedGroup(
key,
config,
UserFlag);
// Add to AD Context
ActiveDirectory.Context.ManagedGroups.AddOrUpdate(group);
return group;
}
}
// Remove from AD Context
ActiveDirectory.Context.ManagedGroups.Remove(key);
}
return null;
}
public override IEnumerable<string> DetermineMembers(DiscoDataContext Database)
{
return Database.UserFlagAssignments
.Where(a => a.UserFlagId == UserFlagId && !a.RemovedDate.HasValue)
.Select(a => a.UserId);
}
private void ProcessRepositoryEvent(RepositoryMonitorEvent Event)
{
var userFlagAssignemnt = (UserFlagAssignment)Event.Entity;
switch (Event.EventType)
{
case RepositoryMonitorEventType.Added:
if (!userFlagAssignemnt.RemovedDate.HasValue)
AddMember(userFlagAssignemnt.UserId);
break;
case RepositoryMonitorEventType.Modified:
if (userFlagAssignemnt.RemovedDate.HasValue)
RemoveMember(userFlagAssignemnt.UserId);
else
AddMember(userFlagAssignemnt.UserId);
break;
case RepositoryMonitorEventType.Deleted:
string userId = userFlagAssignemnt.UserId;
// Remove the user if no other (non-removed) assignments exist.
RemoveMember(userId, (database) =>
{
if (database.UserFlagAssignments.Any(a => a.UserFlagId == UserFlagId && a.UserId == userId && !a.RemovedDate.HasValue))
return null;
else
return new string[] { userId };
});
break;
}
}
public override void Dispose()
{
if (repositorySubscription != null)
repositorySubscription.Dispose();
}
}
}
@@ -41,7 +41,7 @@ namespace Disco.Services.Users.UserFlags
// Parse Users
var userIds = UserIds
.Select(u => u.Contains('\\') ? u : string.Concat(ActiveDirectory.Context.PrimaryDomain.NetBiosName, @"\", u))
.Select(u => ActiveDirectory.ParseDomainAccountId(u))
.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
Status.UpdateStatus(10, "Loading users from the database");