feature: flag permissions

feature: flag permissions
This commit is contained in:
Gary Sharp
2025-07-20 10:45:55 +10:00
parent 7deead494b
commit be7ee4cae8
72 changed files with 5590 additions and 2109 deletions
+11 -17
View File
@@ -8,7 +8,7 @@ namespace Disco.Services.Devices.DeviceFlags
{
internal class Cache
{
private ConcurrentDictionary<int, DeviceFlag> _Cache;
private ConcurrentDictionary<int, (DeviceFlag, FlagPermission permission)> cache;
public Cache(DiscoDataContext Database)
{
@@ -26,36 +26,30 @@ namespace Disco.Services.Devices.DeviceFlags
var flags = Database.DeviceFlags.ToList();
// Add Queues to In-Memory Cache
_Cache = new ConcurrentDictionary<int, DeviceFlag>(flags.Select(f => new KeyValuePair<int, DeviceFlag>(f.Id, f)));
cache = new ConcurrentDictionary<int, (DeviceFlag, FlagPermission permission)>(flags.Select(f => new KeyValuePair<int, (DeviceFlag, FlagPermission permission)>(f.Id, (f, f.Permissions))));
}
public DeviceFlag GetDeviceFlag(int deviceFlagId)
public (DeviceFlag flag, FlagPermission permission) GetDeviceFlag(int deviceFlagId)
{
if (_Cache.TryGetValue(deviceFlagId, out var item))
if (cache.TryGetValue(deviceFlagId, out var item))
return item;
else
return null;
return (null, null);
}
public List<DeviceFlag> GetDeviceFlags()
public List<(DeviceFlag flag, FlagPermission permission)> GetDeviceFlags()
{
return _Cache.Values.ToList();
return cache.Values.ToList();
}
public void AddOrUpdate(DeviceFlag flag)
{
_Cache.AddOrUpdate(flag.Id, flag, (key, existingItem) => flag);
var value = (flag, flag.Permissions);
cache.AddOrUpdate(flag.Id, value, (key, existingItem) => value);
}
public DeviceFlag Remove(int deviceFlagId)
public void Remove(int deviceFlagId)
{
if (_Cache.TryRemove(deviceFlagId, out var item))
return item;
else
return null;
}
public DeviceFlag Remove(DeviceFlag deviceFlag)
{
return Remove(deviceFlag.Id);
cache.TryRemove(deviceFlagId, out _);
}
}
}
@@ -1,6 +1,7 @@
using Disco.Data.Repository;
using Disco.Models.Repository;
using Disco.Services.Authorization;
using Disco.Services.Devices.DeviceFlags;
using Disco.Services.Expressions;
using Disco.Services.Logging;
using Disco.Services.Users;
@@ -15,16 +16,18 @@ namespace Disco.Services
{
#region Edit Comments
public static bool CanEditComments(this DeviceFlagAssignment fa)
public static bool CanEdit(this DeviceFlagAssignment fa)
{
return UserService.CurrentAuthorization.Has(Claims.Device.Actions.EditFlags);
var (_, permission) = DeviceFlagService.GetDeviceFlag(fa.DeviceFlagId);
return permission.CanEdit();
}
public static void OnEditComments(this DeviceFlagAssignment fa, string Comments)
public static void OnEdit(this DeviceFlagAssignment fa, string comments)
{
if (!fa.CanEditComments())
if (!fa.CanEdit())
throw new InvalidOperationException("Editing comments for device flags is denied");
fa.Comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim();
fa.Comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim();
}
#endregion
@@ -34,36 +37,38 @@ namespace Disco.Services
if (fa.RemovedDate.HasValue)
return false;
return UserService.CurrentAuthorization.Has(Claims.Device.Actions.RemoveFlags);
var (_, permission) = DeviceFlagService.GetDeviceFlag(fa.DeviceFlagId);
return permission.CanRemove();
}
public static void OnRemove(this DeviceFlagAssignment fa, DiscoDataContext Database, User RemovingUser)
public static void OnRemove(this DeviceFlagAssignment fa, DiscoDataContext database)
{
if (!fa.CanRemove())
throw new InvalidOperationException("Removing device flags is denied");
fa.OnRemoveUnsafe(Database, RemovingUser);
fa.OnRemoveUnsafe(database, UserService.CurrentUser);
}
public static void OnRemoveUnsafe(this DeviceFlagAssignment fa, DiscoDataContext Database, User RemovingUser)
public static void OnRemoveUnsafe(this DeviceFlagAssignment fa, DiscoDataContext database, User removingUser)
{
fa = Database.DeviceFlagAssignments
fa = database.DeviceFlagAssignments
.Include(a => a.DeviceFlag)
.First(a => a.Id == fa.Id);
RemovingUser = Database.Users.First(u => u.UserId == RemovingUser.UserId);
removingUser = database.Users.First(u => u.UserId == removingUser.UserId);
fa.RemovedDate = DateTime.Now;
fa.RemovedUserId = RemovingUser.UserId;
fa.RemovedUserId = removingUser.UserId;
if (!string.IsNullOrWhiteSpace(fa.DeviceFlag.OnUnassignmentExpression))
{
try
{
Database.SaveChanges();
var expressionResult = fa.EvaluateOnUnassignmentExpression(Database, RemovingUser, fa.AddedDate);
database.SaveChanges();
var expressionResult = fa.EvaluateOnUnassignmentExpression(database, removingUser, fa.AddedDate);
if (!string.IsNullOrWhiteSpace(expressionResult))
{
fa.OnUnassignmentExpressionResult = expressionResult;
Database.SaveChanges();
database.SaveChanges();
}
}
catch (Exception ex)
@@ -75,58 +80,52 @@ namespace Disco.Services
#endregion
#region Add
public static bool CanAddDeviceFlags(this Device d)
{
return UserService.CurrentAuthorization.Has(Claims.Device.Actions.AddFlags);
}
public static bool CanAddDeviceFlag(this Device d, DeviceFlag flag)
{
// Shortcut
if (!d.CanAddDeviceFlags())
return false;
// Already has Device Flag?
if (d.DeviceFlagAssignments.Any(fa => !fa.RemovedDate.HasValue && fa.DeviceFlagId == flag.Id))
return false;
return true;
var (_, permission) = DeviceFlagService.GetDeviceFlag(flag.Id);
return permission.CanAssign();
}
public static DeviceFlagAssignment OnAddDeviceFlag(this Device d, DiscoDataContext Database, DeviceFlag flag, User AddingUser, string Comments)
public static DeviceFlagAssignment OnAddDeviceFlag(this Device d, DiscoDataContext database, DeviceFlag flag, string comments)
{
if (!d.CanAddDeviceFlag(flag))
throw new InvalidOperationException("Adding device flag is denied");
return d.OnAddDeviceFlagUnsafe(Database, flag, AddingUser, Comments);
return d.OnAddDeviceFlagUnsafe(database, flag, UserService.CurrentUser, comments);
}
public static DeviceFlagAssignment OnAddDeviceFlagUnsafe(this Device d, DiscoDataContext Database, DeviceFlag flag, User AddingUser, string Comments)
public static DeviceFlagAssignment OnAddDeviceFlagUnsafe(this Device d, DiscoDataContext database, DeviceFlag flag, User addingUser, string comments)
{
flag = Database.DeviceFlags.First(f => f.Id == flag.Id);
d = Database.Devices.First(de => de.SerialNumber == d.SerialNumber);
AddingUser = Database.Users.First(user => user.UserId == AddingUser.UserId);
flag = database.DeviceFlags.First(f => f.Id == flag.Id);
d = database.Devices.First(de => de.SerialNumber == d.SerialNumber);
addingUser = database.Users.First(user => user.UserId == addingUser.UserId);
var fa = new DeviceFlagAssignment()
{
DeviceFlag = flag,
Device = d,
AddedDate = DateTime.Now,
AddedUser = AddingUser,
AddedUserId = AddingUser.UserId,
Comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim()
AddedUser = addingUser,
AddedUserId = addingUser.UserId,
Comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim()
};
Database.DeviceFlagAssignments.Add(fa);
database.DeviceFlagAssignments.Add(fa);
if (!string.IsNullOrWhiteSpace(flag.OnAssignmentExpression))
{
try
{
Database.SaveChanges();
var expressionResult = fa.EvaluateOnAssignmentExpression(Database, AddingUser, fa.AddedDate);
database.SaveChanges();
var expressionResult = fa.EvaluateOnAssignmentExpression(database, addingUser, fa.AddedDate);
if (!string.IsNullOrWhiteSpace(expressionResult))
{
fa.OnAssignmentExpressionResult = expressionResult;
Database.SaveChanges();
database.SaveChanges();
}
}
catch (Exception ex)
@@ -13,7 +13,7 @@ namespace Disco.Services.Devices.DeviceFlags
{
public static class DeviceFlagService
{
private static Cache _cache;
private static Cache cache;
internal static Lazy<IObservable<RepositoryMonitorEvent>> DeviceFlagAssignmentRepositoryEvents;
static DeviceFlagService()
@@ -31,18 +31,51 @@ namespace Disco.Services.Devices.DeviceFlags
public static void Initialize(DiscoDataContext database)
{
_cache = new Cache(database);
cache = new Cache(database);
// Initialize Managed Groups (if configured)
_cache.GetDeviceFlags().ForEach(uf =>
cache.GetDeviceFlags().ForEach(uf =>
{
DeviceFlagDevicesManagedGroup.Initialize(uf);
DeviceFlagDeviceAssignedUsersManagedGroup.Initialize(uf);
DeviceFlagDevicesManagedGroup.Initialize(uf.flag);
DeviceFlagDeviceAssignedUsersManagedGroup.Initialize(uf.flag);
});
}
public static List<DeviceFlag> GetDeviceFlags() { return _cache.GetDeviceFlags(); }
public static DeviceFlag GetDeviceFlag(int deviceFlagId) { return _cache.GetDeviceFlag(deviceFlagId); }
public static IEnumerable<(DeviceFlag flag, FlagPermission permission)> GetDeviceFlags() { return cache.GetDeviceFlags(); }
public static (DeviceFlag flag, FlagPermission permission) GetDeviceFlag(int deviceFlagId) { return cache.GetDeviceFlag(deviceFlagId); }
public static DeviceFlag GetAvailableUserFlag(int deviceFlagId, Device targetDevice)
{
var (deviceFlag, permission) = cache.GetDeviceFlag(deviceFlagId);
if (targetDevice.DeviceFlagAssignments
.Where(a => a.DeviceFlagId == deviceFlagId && !a.RemovedDate.HasValue).Any())
return null;
if (permission.CanAssign())
return deviceFlag;
return null;
}
public static IEnumerable<DeviceFlag> GetAvailableDeviceFlags(Device targetDevice)
{
var records = cache.GetDeviceFlags();
var usedFlags = targetDevice.DeviceFlagAssignments
.Where(a => !a.RemovedDate.HasValue)
.Select(a => a.DeviceFlagId)
.ToList();
foreach (var (flag, permission) in records)
{
if (usedFlags.Contains(flag.Id))
continue;
if (permission.CanAssign())
yield return flag;
}
}
#region Device Flag Maintenance
public static DeviceFlag CreateDeviceFlag(DiscoDataContext database, string name, string description)
@@ -52,7 +85,7 @@ namespace Disco.Services.Devices.DeviceFlags
throw new ArgumentException("The Device Flag Name is required", nameof(name));
// Name Unique
if (_cache.GetDeviceFlags().Any(f => f.Name.Equals(name, StringComparison.Ordinal)))
if (cache.GetDeviceFlags().Any(f => f.flag.Name.Equals(name, StringComparison.Ordinal)))
throw new ArgumentException("Another Device Flag already exists with that name", nameof(name));
// Clone to break reference
@@ -67,7 +100,7 @@ namespace Disco.Services.Devices.DeviceFlags
database.DeviceFlags.Add(flag);
database.SaveChanges();
_cache.AddOrUpdate(flag);
cache.AddOrUpdate(flag);
return flag;
}
@@ -78,12 +111,12 @@ namespace Disco.Services.Devices.DeviceFlags
throw new ArgumentException("The Device Flag Name is required", nameof(deviceFlag));
// Name Unique
if (_cache.GetDeviceFlags().Any(f => f.Id != deviceFlag.Id && f.Name == deviceFlag.Name))
if (cache.GetDeviceFlags().Any(f => f.flag.Id != deviceFlag.Id && f.flag.Name == deviceFlag.Name))
throw new ArgumentException("Another Device Flag already exists with that name", nameof(deviceFlag));
database.SaveChanges();
_cache.AddOrUpdate(deviceFlag);
cache.AddOrUpdate(deviceFlag);
DeviceFlagDevicesManagedGroup.Initialize(deviceFlag);
DeviceFlagDeviceAssignedUsersManagedGroup.Initialize(deviceFlag);
@@ -113,7 +146,7 @@ namespace Disco.Services.Devices.DeviceFlags
database.SaveChanges();
// Remove from Cache
_cache.Remove(deviceFlagId);
cache.Remove(deviceFlagId);
status.Finished($"Successfully Deleted Device Flag: '{flag.Name}' [{flag.Id}]");
}
@@ -140,7 +173,7 @@ namespace Disco.Services.Devices.DeviceFlags
{
status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {device}");
return device.OnAddDeviceFlag(database, deviceFlag, technician, comments);
return device.OnAddDeviceFlagUnsafe(database, deviceFlag, technician, comments);
}).ToList();
// Save Chunk Items to Database
@@ -206,7 +239,7 @@ namespace Disco.Services.Devices.DeviceFlags
{
status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {device}");
return device.OnAddDeviceFlag(database, deviceFlag, technician, comments);
return device.OnAddDeviceFlagUnsafe(database, deviceFlag, technician, comments);
}).ToList();
// Save Chunk Items to Database
@@ -229,11 +262,11 @@ namespace Disco.Services.Devices.DeviceFlags
public static string RandomUnusedIcon()
{
return UIHelpers.RandomIcon(_cache.GetDeviceFlags().Select(f => f.Icon));
return UIHelpers.RandomIcon(cache.GetDeviceFlags().Select(f => f.flag.Icon));
}
public static string RandomUnusedThemeColour()
{
return UIHelpers.RandomThemeColour(_cache.GetDeviceFlags().Select(f => f.IconColour));
return UIHelpers.RandomThemeColour(cache.GetDeviceFlags().Select(f => f.flag.IconColour));
}
}
}