From be7ee4cae8853ed40d1314431cbd7bc61ad08fb6 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Sun, 20 Jul 2025 10:45:55 +1000 Subject: [PATCH] feature: flag permissions feature: flag permissions --- Disco.Data/Disco.Data.csproj | 7 + .../202507170522576_DBv28.Designer.cs | 27 + .../Migrations/202507170522576_DBv28.cs | 40 + .../Migrations/202507170522576_DBv28.resx | 123 ++ Disco.Models/Disco.Models.csproj | 2 + .../Repository/Device/Flag/DeviceFlag.cs | 19 +- .../Device/Flag/DeviceFlagAssignment.cs | 16 +- Disco.Models/Repository/FlagPermission.cs | 121 ++ Disco.Models/Repository/FlagType.cs | 8 + Disco.Models/Repository/User/Flag/UserFlag.cs | 19 +- .../User/Flag/UserFlagAssignment.cs | 16 +- .../DeviceFlag/ConfigDeviceFlagShowModel.cs | 5 +- .../UserFlag/ConfigUserFlagShowModel.cs | 5 +- Disco.Services/Authorization/Claims.cs | 40 +- Disco.Services/Authorization/Claims.tt | 40 +- .../Authorization/Roles/RoleCache.cs | 102 +- Disco.Services/Devices/DeviceFlags/Cache.cs | 28 +- .../DeviceFlags/DeviceFlagExtensions.cs | 73 +- .../Devices/DeviceFlags/DeviceFlagService.cs | 65 +- Disco.Services/Disco.Services.csproj | 1 + Disco.Services/Users/UserFlags/Cache.cs | 28 +- .../UserFlags/FlagPermissionExtensions.cs | 144 ++ .../Users/UserFlags/UserFlagExtensions.cs | 73 +- .../Users/UserFlags/UserFlagService.cs | 112 +- Disco.Services/Users/UserService.cs | 18 +- Disco.Services/Web/BaseController.cs | 10 +- .../AuthorizationRoleController.cs | 10 +- .../DeviceFlagAssignmentController.cs | 44 +- .../API/Controllers/DeviceFlagController.cs | 20 + .../Areas/API/Controllers/SystemController.cs | 50 +- .../UserFlagAssignmentController.cs | 36 +- .../API/Controllers/UserFlagController.cs | 19 + .../API/Models/Shared/FlagPermissionModel.cs | 21 + .../Models/Shared/SubjectDescriptorModel.cs | 17 +- .../Controllers/DeviceFlagController.cs | 5 +- .../Config/Controllers/UserFlagController.cs | 5 +- .../Config/Models/DeviceFlag/ShowModel.cs | 5 +- .../Areas/Config/Models/UserFlag/ShowModel.cs | 5 +- .../Views/AuthorizationRole/Show.cshtml | 4 +- .../Views/AuthorizationRole/Show.generated.cs | 20 +- .../Areas/Config/Views/DeviceFlag/Show.cshtml | 356 ++++- .../Config/Views/DeviceFlag/Show.generated.cs | 1374 ++++++++++++++--- .../Areas/Config/Views/UserFlag/Show.cshtml | 338 +++- .../Config/Views/UserFlag/Show.generated.cs | 1318 ++++++++++++++-- Disco.Web/ClientSource/Style/Config.css | 17 + Disco.Web/ClientSource/Style/Config.less | 27 + Disco.Web/ClientSource/Style/Config.min.css | 2 +- Disco.Web/Controllers/DeviceController.cs | 11 +- Disco.Web/Controllers/UserController.cs | 11 +- Disco.Web/Disco.Web.csproj | 1 + ...eviceFlagAssignmentController.generated.cs | 14 +- .../API.DeviceFlagController.generated.cs | 30 + .../T4MVC/API.SystemController.generated.cs | 16 +- .../T4MVC/API.UserFlagController.generated.cs | 30 + .../Views/Device/DeviceParts/_Flags.cshtml | 23 +- .../Device/DeviceParts/_Flags.generated.cs | 177 ++- .../Views/Device/DeviceParts/_Subject.cshtml | 35 +- .../Device/DeviceParts/_Subject.generated.cs | 645 ++++---- Disco.Web/Views/Device/Show.cshtml | 19 +- Disco.Web/Views/Device/Show.generated.cs | 113 +- Disco.Web/Views/Device/_DeviceTable.cshtml | 82 +- .../Views/Device/_DeviceTable.generated.cs | 122 +- Disco.Web/Views/Job/JobParts/_Subject.cshtml | 36 +- .../Views/Job/JobParts/_Subject.generated.cs | 700 ++++----- Disco.Web/Views/User/Show.cshtml | 19 +- Disco.Web/Views/User/Show.generated.cs | 107 +- Disco.Web/Views/User/UserParts/_Flags.cshtml | 23 +- .../Views/User/UserParts/_Flags.generated.cs | 177 ++- .../Views/User/UserParts/_Subject.cshtml | 33 +- .../User/UserParts/_Subject.generated.cs | 363 ++--- Disco.Web/Views/User/_UserTable.cshtml | 18 +- Disco.Web/Views/User/_UserTable.generated.cs | 59 +- 72 files changed, 5590 insertions(+), 2109 deletions(-) create mode 100644 Disco.Data/Migrations/202507170522576_DBv28.Designer.cs create mode 100644 Disco.Data/Migrations/202507170522576_DBv28.cs create mode 100644 Disco.Data/Migrations/202507170522576_DBv28.resx create mode 100644 Disco.Models/Repository/FlagPermission.cs create mode 100644 Disco.Models/Repository/FlagType.cs create mode 100644 Disco.Services/Users/UserFlags/FlagPermissionExtensions.cs create mode 100644 Disco.Web/Areas/API/Models/Shared/FlagPermissionModel.cs diff --git a/Disco.Data/Disco.Data.csproj b/Disco.Data/Disco.Data.csproj index d9ec35b5..b8aa9ca5 100644 --- a/Disco.Data/Disco.Data.csproj +++ b/Disco.Data/Disco.Data.csproj @@ -197,6 +197,10 @@ 202507110430252_DBv27.cs + + + 202507170522576_DBv28.cs + @@ -293,6 +297,9 @@ 202507110430252_DBv27.cs + + 202507170522576_DBv28.cs + ResXFileCodeGenerator Resources.Designer.cs diff --git a/Disco.Data/Migrations/202507170522576_DBv28.Designer.cs b/Disco.Data/Migrations/202507170522576_DBv28.Designer.cs new file mode 100644 index 00000000..17276fc6 --- /dev/null +++ b/Disco.Data/Migrations/202507170522576_DBv28.Designer.cs @@ -0,0 +1,27 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + public sealed partial class DBv28 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DBv28)); + + string IMigrationMetadata.Id + { + get { return "202507170522576_DBv28"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Disco.Data/Migrations/202507170522576_DBv28.cs b/Disco.Data/Migrations/202507170522576_DBv28.cs new file mode 100644 index 00000000..ad853226 --- /dev/null +++ b/Disco.Data/Migrations/202507170522576_DBv28.cs @@ -0,0 +1,40 @@ +namespace Disco.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class DBv28 : DbMigration + { + public override void Up() + { + AddColumn("dbo.UserFlagAssignments", "RemoveDate", c => c.DateTime()); + AddColumn("dbo.UserFlagAssignments", "RemoveUserId", c => c.String(maxLength: 50)); + AddColumn("dbo.UserFlags", "Permissions", c => c.String()); + AddColumn("dbo.UserFlags", "DefaultRemoveDays", c => c.Int()); + AddColumn("dbo.DeviceFlagAssignments", "RemoveDate", c => c.DateTime()); + AddColumn("dbo.DeviceFlagAssignments", "RemoveUserId", c => c.String(maxLength: 50)); + AddColumn("dbo.DeviceFlags", "Permissions", c => c.String()); + AddColumn("dbo.DeviceFlags", "DefaultRemoveDays", c => c.Int()); + AddForeignKey("dbo.UserFlagAssignments", "RemoveUserId", "dbo.Users", "Id"); + AddForeignKey("dbo.DeviceFlagAssignments", "RemoveUserId", "dbo.Users", "Id"); + CreateIndex("dbo.UserFlagAssignments", "RemoveUserId"); + CreateIndex("dbo.DeviceFlagAssignments", "RemoveUserId"); + } + + public override void Down() + { + DropIndex("dbo.DeviceFlagAssignments", new[] { "RemoveUserId" }); + DropIndex("dbo.UserFlagAssignments", new[] { "RemoveUserId" }); + DropForeignKey("dbo.DeviceFlagAssignments", "RemoveUserId", "dbo.Users"); + DropForeignKey("dbo.UserFlagAssignments", "RemoveUserId", "dbo.Users"); + DropColumn("dbo.DeviceFlags", "DefaultRemoveDays"); + DropColumn("dbo.DeviceFlags", "Permissions"); + DropColumn("dbo.DeviceFlagAssignments", "RemoveUserId"); + DropColumn("dbo.DeviceFlagAssignments", "RemoveDate"); + DropColumn("dbo.UserFlags", "DefaultRemoveDays"); + DropColumn("dbo.UserFlags", "Permissions"); + DropColumn("dbo.UserFlagAssignments", "RemoveUserId"); + DropColumn("dbo.UserFlagAssignments", "RemoveDate"); + } + } +} diff --git a/Disco.Data/Migrations/202507170522576_DBv28.resx b/Disco.Data/Migrations/202507170522576_DBv28.resx new file mode 100644 index 00000000..757dad57 --- /dev/null +++ b/Disco.Data/Migrations/202507170522576_DBv28.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + \ No newline at end of file diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj index bcfee045..5f02c943 100644 --- a/Disco.Models/Disco.Models.csproj +++ b/Disco.Models/Disco.Models.csproj @@ -59,7 +59,9 @@ + + diff --git a/Disco.Models/Repository/Device/Flag/DeviceFlag.cs b/Disco.Models/Repository/Device/Flag/DeviceFlag.cs index 1f098eb3..120bc999 100644 --- a/Disco.Models/Repository/Device/Flag/DeviceFlag.cs +++ b/Disco.Models/Repository/Device/Flag/DeviceFlag.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using Disco.Models.Services.Authorization; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Disco.Models.Repository { @@ -27,11 +29,20 @@ namespace Disco.Models.Repository [DataType(DataType.MultilineText)] public string OnUnassignmentExpression { get; set; } + [Column("Permissions")] + public string PermissionsJson { get; set; } + [NotMapped] + public FlagPermission Permissions + { + get => FlagPermission.FromFlag(this); + set => PermissionsJson = value?.ToJson(); + } + + public int? DefaultRemoveDays { get; set; } + public virtual IList DeviceFlagAssignments { get; set; } public override string ToString() - { - return Name; - } + => Name; } } diff --git a/Disco.Models/Repository/Device/Flag/DeviceFlagAssignment.cs b/Disco.Models/Repository/Device/Flag/DeviceFlagAssignment.cs index f73f8ec6..a7b7d1b1 100644 --- a/Disco.Models/Repository/Device/Flag/DeviceFlagAssignment.cs +++ b/Disco.Models/Repository/Device/Flag/DeviceFlagAssignment.cs @@ -19,26 +19,28 @@ namespace Disco.Models.Repository public string AddedUserId { get; set; } public DateTime? RemovedDate { get; set; } public string RemovedUserId { get; set; } + public DateTime? RemoveDate { get; set; } + public string RemoveUserId { get; set; } public string Comments { get; set; } public string OnAssignmentExpressionResult { get; set; } public string OnUnassignmentExpressionResult { get; set; } - [ForeignKey(nameof(DeviceFlagId)), InverseProperty("DeviceFlagAssignments")] + [ForeignKey(nameof(DeviceFlagId)), InverseProperty(nameof(Repository.DeviceFlag.DeviceFlagAssignments))] public virtual DeviceFlag DeviceFlag { get; set; } - [ForeignKey(nameof(DeviceSerialNumber)), InverseProperty("DeviceFlagAssignments")] + [ForeignKey(nameof(DeviceSerialNumber)), InverseProperty(nameof(Repository.Device.DeviceFlagAssignments))] public virtual Device Device { get; set; } - [ForeignKey("AddedUserId")] + [ForeignKey(nameof(AddedUserId))] public virtual User AddedUser { get; set; } - [ForeignKey("RemovedUserId")] + [ForeignKey(nameof(RemovedUserId))] public virtual User RemovedUser { get; set; } + [ForeignKey(nameof(RemoveUserId))] + public virtual User RemoveUser { get; set; } public override string ToString() - { - return $"Device Flag Id: {DeviceFlagId}; Device Serial Number: {DeviceSerialNumber}; Added: {AddedDate:s}"; - } + => $"Device Flag Id: {DeviceFlagId}; Device Serial Number: {DeviceSerialNumber}; Added: {AddedDate:s}"; } } diff --git a/Disco.Models/Repository/FlagPermission.cs b/Disco.Models/Repository/FlagPermission.cs new file mode 100644 index 00000000..efd64b24 --- /dev/null +++ b/Disco.Models/Repository/FlagPermission.cs @@ -0,0 +1,121 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Disco.Models.Repository +{ + public class FlagPermission + { + private static readonly FlagPermission DefaultDeviceFlagPermissions = new FlagPermission(FlagType.Device, 0); + private static readonly FlagPermission DefaultUserFlagPermissions = new FlagPermission(FlagType.User, 0); + + [JsonIgnore] + public FlagType FlagType { get; } + [JsonIgnore] + public int FlagId { get; } + public bool Inherit { get; set; } + public HashSet CanShowSubjectIds { get; } + public HashSet CanAssignSubjectIds { get; } + public HashSet CanEditSubjectIds { get; } + public HashSet CanRemoveSubjectIds { get; } + + private FlagPermission(FlagType flagType, int flagId) + { + FlagType = flagType; + FlagId = flagId; + Inherit = true; + CanShowSubjectIds = new HashSet(StringComparer.OrdinalIgnoreCase); + CanAssignSubjectIds = new HashSet(StringComparer.OrdinalIgnoreCase); + CanEditSubjectIds = new HashSet(StringComparer.OrdinalIgnoreCase); + CanRemoveSubjectIds = new HashSet(StringComparer.OrdinalIgnoreCase); + } + + public bool IsDefault() + { + return ReferenceEquals(this, DefaultDeviceFlagPermissions) || + ReferenceEquals(this, DefaultUserFlagPermissions); + } + + public bool IsSimple() + { + return CanShowSubjectIds.SetEquals(CanAssignSubjectIds) && + CanAssignSubjectIds.SetEquals(CanEditSubjectIds) && + CanEditSubjectIds.SetEquals(CanRemoveSubjectIds); + } + + public bool HasSubjects() + { + return CanShowSubjectIds.Count > 0 || + CanAssignSubjectIds.Count > 0 || + CanEditSubjectIds.Count > 0 || + CanRemoveSubjectIds.Count > 0; + } + + public HashSet AllSubjects() + { + var allSubjects = new HashSet(StringComparer.OrdinalIgnoreCase); + allSubjects.UnionWith(CanShowSubjectIds); + allSubjects.UnionWith(CanAssignSubjectIds); + allSubjects.UnionWith(CanEditSubjectIds); + allSubjects.UnionWith(CanRemoveSubjectIds); + return allSubjects; + } + + public static FlagPermission FromFlag(UserFlag userFlag) + { + if (userFlag.PermissionsJson == null) + return DefaultUserFlagPermissions; + + var permission = new FlagPermission(FlagType.User, userFlag.Id); + + JsonConvert.PopulateObject(userFlag.PermissionsJson, permission); + + return permission; + } + + public static FlagPermission FromFlag(DeviceFlag deviceFlag) + { + if (deviceFlag.PermissionsJson == null) + return DefaultDeviceFlagPermissions; + + var permission = new FlagPermission(FlagType.Device, deviceFlag.Id); + + JsonConvert.PopulateObject(deviceFlag.PermissionsJson, permission); + + return permission; + } + + public static FlagPermission Create(UserFlag userFlag, bool inherit, IEnumerable canShow, IEnumerable canAssign, IEnumerable canEdit, IEnumerable canRemove) + { + var permission = new FlagPermission(FlagType.User, userFlag.Id); + return Create(permission, inherit, canShow, canAssign, canEdit, canRemove); + } + + public static FlagPermission Create(DeviceFlag deviceFlag, bool inherit, IEnumerable canShow, IEnumerable canAssign, IEnumerable canEdit, IEnumerable canRemove) + { + var permission = new FlagPermission(FlagType.Device, deviceFlag.Id); + return Create(permission, inherit, canShow, canAssign, canEdit, canRemove); + } + + private static FlagPermission Create(FlagPermission permission, bool inherit, IEnumerable canShow, IEnumerable canAssign, IEnumerable canEdit, IEnumerable canRemove) + { + permission.Inherit = inherit; + if (canShow != null) + permission.CanShowSubjectIds.UnionWith(canShow); + if (canAssign != null) + permission.CanAssignSubjectIds.UnionWith(canAssign); + if (canEdit != null) + permission.CanEditSubjectIds.UnionWith(canEdit); + if (canRemove != null) + permission.CanRemoveSubjectIds.UnionWith(canRemove); + return permission; + } + + public string ToJson() + { + if (IsDefault()) + return null; + return JsonConvert.SerializeObject(this); + } + } +} diff --git a/Disco.Models/Repository/FlagType.cs b/Disco.Models/Repository/FlagType.cs new file mode 100644 index 00000000..9bbed67b --- /dev/null +++ b/Disco.Models/Repository/FlagType.cs @@ -0,0 +1,8 @@ +namespace Disco.Models.Repository +{ + public enum FlagType + { + User = 1, + Device = 2, + } +} diff --git a/Disco.Models/Repository/User/Flag/UserFlag.cs b/Disco.Models/Repository/User/Flag/UserFlag.cs index e5e18fb9..859439e0 100644 --- a/Disco.Models/Repository/User/Flag/UserFlag.cs +++ b/Disco.Models/Repository/User/Flag/UserFlag.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using Disco.Models.Services.Authorization; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Disco.Models.Repository { @@ -27,11 +29,20 @@ namespace Disco.Models.Repository [DataType(DataType.MultilineText)] public string OnUnassignmentExpression { get; set; } + [Column("Permissions")] + public string PermissionsJson { get; set; } + [NotMapped] + public FlagPermission Permissions + { + get => FlagPermission.FromFlag(this); + set => PermissionsJson = value?.ToJson(); + } + + public int? DefaultRemoveDays { get; set; } + public virtual IList UserFlagAssignments { get; set; } public override string ToString() - { - return Name; - } + => Name; } } \ No newline at end of file diff --git a/Disco.Models/Repository/User/Flag/UserFlagAssignment.cs b/Disco.Models/Repository/User/Flag/UserFlagAssignment.cs index 107e0c03..f37a3026 100644 --- a/Disco.Models/Repository/User/Flag/UserFlagAssignment.cs +++ b/Disco.Models/Repository/User/Flag/UserFlagAssignment.cs @@ -19,26 +19,28 @@ namespace Disco.Models.Repository public string AddedUserId { get; set; } public DateTime? RemovedDate { get; set; } public string RemovedUserId { get; set; } + public DateTime? RemoveDate { get; set; } + public string RemoveUserId { get; set; } public string Comments { get; set; } public string OnAssignmentExpressionResult { get; set; } public string OnUnassignmentExpressionResult { get; set; } - [ForeignKey("UserFlagId"), InverseProperty("UserFlagAssignments")] + [ForeignKey(nameof(UserFlagId)), InverseProperty(nameof(Repository.UserFlag.UserFlagAssignments))] public virtual UserFlag UserFlag { get; set; } - [ForeignKey("UserId"), InverseProperty("UserFlagAssignments")] + [ForeignKey(nameof(UserId)), InverseProperty(nameof(Repository.User.UserFlagAssignments))] public virtual User User { get; set; } - [ForeignKey("AddedUserId")] + [ForeignKey(nameof(AddedUserId))] public virtual User AddedUser { get; set; } - [ForeignKey("RemovedUserId")] + [ForeignKey(nameof(RemovedUserId))] public virtual User RemovedUser { get; set; } + [ForeignKey(nameof(RemoveUserId))] + public virtual User RemoveUser { get; set; } public override string ToString() - { - return $"User Flag Id: {UserFlagId}; User Id: {UserId}; Added: {AddedDate:s}"; - } + => $"User Flag Id: {UserFlagId}; User Id: {UserId}; Added: {AddedDate:s}"; } } \ No newline at end of file diff --git a/Disco.Models/UI/Config/DeviceFlag/ConfigDeviceFlagShowModel.cs b/Disco.Models/UI/Config/DeviceFlag/ConfigDeviceFlagShowModel.cs index 03aa27d1..33f6cb19 100644 --- a/Disco.Models/UI/Config/DeviceFlag/ConfigDeviceFlagShowModel.cs +++ b/Disco.Models/UI/Config/DeviceFlag/ConfigDeviceFlagShowModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Disco.Models.Repository; +using System.Collections.Generic; namespace Disco.Models.UI.Config.DeviceFlag { @@ -11,5 +12,7 @@ namespace Disco.Models.UI.Config.DeviceFlag IEnumerable> Icons { get; set; } IEnumerable> ThemeColours { get; set; } + + FlagPermission Permission { get; set; } } } diff --git a/Disco.Models/UI/Config/UserFlag/ConfigUserFlagShowModel.cs b/Disco.Models/UI/Config/UserFlag/ConfigUserFlagShowModel.cs index af582107..9df83259 100644 --- a/Disco.Models/UI/Config/UserFlag/ConfigUserFlagShowModel.cs +++ b/Disco.Models/UI/Config/UserFlag/ConfigUserFlagShowModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using Disco.Models.Repository; +using System.Collections.Generic; namespace Disco.Models.UI.Config.UserFlag { @@ -11,5 +12,7 @@ namespace Disco.Models.UI.Config.UserFlag IEnumerable> Icons { get; set; } IEnumerable> ThemeColours { get; set; } + + FlagPermission Permission { get; set; } } } \ No newline at end of file diff --git a/Disco.Services/Authorization/Claims.cs b/Disco.Services/Authorization/Claims.cs index 6a54270c..1281e6e1 100644 --- a/Disco.Services/Authorization/Claims.cs +++ b/Disco.Services/Authorization/Claims.cs @@ -18,7 +18,7 @@ namespace Disco.Services.Authorization static Claims() { -#region Role Claim Dictionary + #region Role Claim Dictionary _roleClaims = new Dictionary, Action, string, string, bool>>() { { "Config.DeviceCertificate.DownloadCertificates", new Tuple, Action, string, string, bool>(c => c.Config.DeviceCertificate.DownloadCertificates, (c, v) => c.Config.DeviceCertificate.DownloadCertificates = v, "Download Certificates", "Can download certificates", false) }, @@ -242,9 +242,9 @@ namespace Disco.Services.Authorization { "ComputerAccount", new Tuple, Action, string, string, bool>(c => c.ComputerAccount, (c, v) => c.ComputerAccount = v, "Computer Account", "Represents a computer account", true) }, { "DiscoAdminAccount", new Tuple, Action, string, string, bool>(c => c.DiscoAdminAccount, (c, v) => c.DiscoAdminAccount = v, "Disco Administrator Account", "Represents a Disco ICT Administrator account", true) } }; -#endregion + #endregion -#region Role Claim Navigator + #region Role Claim Navigator _claimNavigator = new ClaimNavigatorItem("Claims", "Permissions", "Top-level node for all permissions", false, new List() { new ClaimNavigatorItem("Config", "Configuration", "Permissions related to Disco ICT Configuration", false, new List() { @@ -524,7 +524,7 @@ namespace Disco.Services.Authorization new ClaimNavigatorItem("ComputerAccount", true), new ClaimNavigatorItem("DiscoAdminAccount", true) }); -#endregion + #endregion } public static ClaimNavigatorItem RoleClaimNavigator @@ -532,31 +532,36 @@ namespace Disco.Services.Authorization get { return _claimNavigator; } } - internal static Tuple, Action, string, string, bool> GetClaimDefinition(string ClaimKey) { + internal static Tuple, Action, string, string, bool> GetClaimDefinition(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Func GetClaimAccessor(string ClaimKey) { + public static Func GetClaimAccessor(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef.Item1; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Action GetClaimSetter(string ClaimKey) { + public static Action GetClaimSetter(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef.Item2; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Tuple GetClaimDetails(string ClaimKey) { + public static Tuple GetClaimDetails(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return Tuple.Create(claimDef.Item3, claimDef.Item4, claimDef.Item5); throw new ArgumentException("Unknown Claim Key", "ClaimKey"); } - public static RoleClaims BuildClaims(IEnumerable ClaimKeys){ + public static RoleClaims BuildClaims(IEnumerable ClaimKeys) + { var c = new RoleClaims(); foreach (var claimKey in ClaimKeys) c.Set(claimKey, true); @@ -570,9 +575,10 @@ namespace Disco.Services.Authorization return _roleClaims.Where(rc => rc.Value.Item1(claims)).Select(rc => rc.Key).ToList(); } - public static RoleClaims AdministratorClaims() { + public static RoleClaims AdministratorClaims() + { var c = new RoleClaims(); -#region Set All Administrator Claims + #region Set All Administrator Claims c.Config.DeviceCertificate.DownloadCertificates = true; c.Config.Enrolment.Configure = true; c.Config.Enrolment.DownloadBootstrapper = true; @@ -792,17 +798,19 @@ namespace Disco.Services.Authorization c.User.ShowFlagAssignments = true; c.User.ShowJobs = true; c.DiscoAdminAccount = true; -#endregion + #endregion return c; } - public static RoleClaims ComputerAccountClaims() { - return new RoleClaims() { + public static RoleClaims ComputerAccountClaims() + { + return new RoleClaims() + { ComputerAccount = true }; } -#region Role Claim Constants + #region Role Claim Constants /// Configuration /// Permissions related to Disco ICT Configuration @@ -2099,7 +2107,7 @@ namespace Disco.Services.Authorization /// Represents a Disco ICT Administrator account /// public const string DiscoAdminAccount = "DiscoAdminAccount"; -#endregion + #endregion } public static class ClaimExtensions { diff --git a/Disco.Services/Authorization/Claims.tt b/Disco.Services/Authorization/Claims.tt index f6c0d450..cc1eba8f 100644 --- a/Disco.Services/Authorization/Claims.tt +++ b/Disco.Services/Authorization/Claims.tt @@ -66,17 +66,17 @@ namespace Disco.Services.Authorization static Claims() { -#region Role Claim Dictionary + #region Role Claim Dictionary _roleClaims = new Dictionary, Action, string, string, bool>>() { <#WriteAccessHashes(permissionRoot);#> }; -#endregion + #endregion -#region Role Claim Navigator + #region Role Claim Navigator _claimNavigator = <#WriteNavigator(permissionRoot);#>; -#endregion + #endregion } public static ClaimNavigatorItem RoleClaimNavigator @@ -84,31 +84,36 @@ namespace Disco.Services.Authorization get { return _claimNavigator; } } - internal static Tuple, Action, string, string, bool> GetClaimDefinition(string ClaimKey) { + internal static Tuple, Action, string, string, bool> GetClaimDefinition(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Func GetClaimAccessor(string ClaimKey) { + public static Func GetClaimAccessor(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef.Item1; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Action GetClaimSetter(string ClaimKey) { + public static Action GetClaimSetter(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return claimDef.Item2; throw new ArgumentException("Unknown Claim Key", nameof(ClaimKey)); } - public static Tuple GetClaimDetails(string ClaimKey) { + public static Tuple GetClaimDetails(string ClaimKey) + { if (_roleClaims.TryGetValue(ClaimKey, out var claimDef)) return Tuple.Create(claimDef.Item3, claimDef.Item4, claimDef.Item5); throw new ArgumentException("Unknown Claim Key", "ClaimKey"); } - public static RoleClaims BuildClaims(IEnumerable ClaimKeys){ + public static RoleClaims BuildClaims(IEnumerable ClaimKeys) + { var c = new RoleClaims(); foreach (var claimKey in ClaimKeys) c.Set(claimKey, true); @@ -122,23 +127,26 @@ namespace Disco.Services.Authorization return _roleClaims.Where(rc => rc.Value.Item1(claims)).Select(rc => rc.Key).ToList(); } - public static RoleClaims AdministratorClaims() { + public static RoleClaims AdministratorClaims() + { var c = new RoleClaims(); -#region Set All Administrator Claims + #region Set All Administrator Claims <#WriteAdministratorClaims(permissionRoot);#> -#endregion + #endregion return c; } - public static RoleClaims ComputerAccountClaims() { - return new RoleClaims() { + public static RoleClaims ComputerAccountClaims() + { + return new RoleClaims() + { ComputerAccount = true }; } -#region Role Claim Constants + #region Role Claim Constants <#WritePermissionConsts(permissionRoot);#> -#endregion + #endregion } public static class ClaimExtensions { diff --git a/Disco.Services/Authorization/Roles/RoleCache.cs b/Disco.Services/Authorization/Roles/RoleCache.cs index 48a60607..2a77eceb 100644 --- a/Disco.Services/Authorization/Roles/RoleCache.cs +++ b/Disco.Services/Authorization/Roles/RoleCache.cs @@ -4,6 +4,7 @@ using Disco.Models.Services.Authorization; using Disco.Services.Interop.ActiveDirectory; using Newtonsoft.Json; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -16,41 +17,41 @@ namespace Disco.Services.Authorization.Roles internal const string ClaimsJsonEmpty = "null"; internal static readonly string[] _RequiredAdministratorSubjectIds = new string[] { "Domain Admins" }; - private static List _Cache; + private static ConcurrentDictionary cache; private static RoleToken _AdministratorToken; - internal static void Initialize(DiscoDataContext Database) + internal static void Initialize(DiscoDataContext database) { - MigrateAuthorizationRoles(Database); + MigrateAuthorizationRoles(database); - _Cache = Database.AuthorizationRoles.ToList().Select(ar => RoleToken.FromAuthorizationRole(ar)).ToList(); + cache = new ConcurrentDictionary(database.AuthorizationRoles.ToList().Select(ar => RoleToken.FromAuthorizationRole(ar)).ToDictionary(r => r.Role.Id)); // Add System Roles - AddSystemRoles(Database); + AddSystemRoles(database); } - private static void AddSystemRoles(DiscoDataContext Database) + private static void AddSystemRoles(DiscoDataContext database) { // Disco Administrators _AdministratorToken = RoleToken.FromAuthorizationRole(new AuthorizationRole() { Id = AdministratorsTokenId, Name = "Disco Administrators", - SubjectIds = string.Join(",", GenerateAdministratorSubjectIds(Database)) + SubjectIds = string.Join(",", GenerateAdministratorSubjectIds(database)) }, Claims.AdministratorClaims()); - _Cache.Add(_AdministratorToken); + cache.TryAdd(AdministratorsTokenId, _AdministratorToken); // Computer Accounts - _Cache.Add(RoleToken.FromAuthorizationRole(new AuthorizationRole() + cache.TryAdd(ComputerAccountTokenId, RoleToken.FromAuthorizationRole(new AuthorizationRole() { Id = ComputerAccountTokenId, Name = "Domain Computer Account" }, Claims.ComputerAccountClaims())); } - private static IEnumerable GenerateAdministratorSubjectIds(DiscoDataContext Database) + private static IEnumerable GenerateAdministratorSubjectIds(DiscoDataContext database) { - var configuredSubjectIds = Database.DiscoConfiguration.Administrators.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => ActiveDirectory.ParseDomainAccountId(s)); + var configuredSubjectIds = database.DiscoConfiguration.Administrators.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => ActiveDirectory.ParseDomainAccountId(s)); return RequiredAdministratorSubjectIds .Concat(configuredSubjectIds) @@ -58,94 +59,79 @@ namespace Disco.Services.Authorization.Roles .OrderBy(s => s); } public static IEnumerable RequiredAdministratorSubjectIds - { - get - { - return _RequiredAdministratorSubjectIds.Select(s => ActiveDirectory.ParseDomainAccountId(s)); - } - } + => _RequiredAdministratorSubjectIds.Select(s => ActiveDirectory.ParseDomainAccountId(s)); public static IEnumerable AdministratorSubjectIds - { - get - { - return _AdministratorToken.SubjectIds.ToList(); - } - } + => _AdministratorToken.SubjectIds.ToList(); - public static void UpdateAdministratorSubjectIds(DiscoDataContext Database, IEnumerable SubjectIds) + public static void UpdateAdministratorSubjectIds(DiscoDataContext database, IEnumerable subjectIds) { // Clean - SubjectIds = SubjectIds + subjectIds = subjectIds .Where(s => !string.IsNullOrWhiteSpace(s)) .Concat(RequiredAdministratorSubjectIds) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(s => s); - var subjectIdsString = string.Join(",", SubjectIds); + var subjectIdsString = string.Join(",", subjectIds); // Update Database - Database.DiscoConfiguration.Administrators = subjectIdsString; - Database.SaveChanges(); + database.DiscoConfiguration.Administrators = subjectIdsString; + database.SaveChanges(); // Update State - _AdministratorToken.SubjectIds = SubjectIds.ToList(); - _AdministratorToken.SubjectIdHashes = new HashSet(SubjectIds, StringComparer.OrdinalIgnoreCase); + _AdministratorToken.SubjectIds = subjectIds.ToList(); + _AdministratorToken.SubjectIdHashes = new HashSet(subjectIds, StringComparer.OrdinalIgnoreCase); } /// /// Create a clone of an Authorization Role /// Creates immutable clones to avoid side-effects /// - /// Authorization Role to Clone + /// Authorization Role to Clone /// A copy of the Authorization Role - private static AuthorizationRole CloneAuthoriationRole(AuthorizationRole TemplateRole) + private static AuthorizationRole CloneAuthoriationRole(AuthorizationRole templateRole) { return new AuthorizationRole() { - Id = TemplateRole.Id, - Name = TemplateRole.Name, - ClaimsJson = TemplateRole.ClaimsJson, - SubjectIds = TemplateRole.SubjectIds + Id = templateRole.Id, + Name = templateRole.Name, + ClaimsJson = templateRole.ClaimsJson, + SubjectIds = templateRole.SubjectIds }; } - internal static RoleToken AddRole(AuthorizationRole Role) - { - var token = RoleToken.FromAuthorizationRole(CloneAuthoriationRole(Role)); - _Cache.Add(token); - return token; - } - internal static void RemoveRole(AuthorizationRole Role) { - var token = GetRoleToken(Role.Id); - if (token != null) - _Cache.Remove(token); + cache.TryRemove(Role.Id, out _); } - internal static RoleToken UpdateRole(AuthorizationRole Role) + internal static RoleToken AddOrUpdateRole(AuthorizationRole role) { - RemoveRole(Role); - return AddRole(Role); + var token = RoleToken.FromAuthorizationRole(CloneAuthoriationRole(role)); + cache.AddOrUpdate(token.Role.Id, token, (i, e) => token); + return token; } - internal static RoleToken GetRoleToken(int Id) + internal static RoleToken GetRoleToken(int id) { - return _Cache.FirstOrDefault(t => t.Role.Id == Id); + if (cache.TryGetValue(id, out var result)) + return result; + else + return null; } - internal static RoleToken GetRoleToken(string SecurityGroup) + internal static RoleToken GetRoleToken(string securityGroup) { - return _Cache.FirstOrDefault(t => t.SubjectIdHashes.Contains(SecurityGroup)); + return cache.Values.FirstOrDefault(t => t.SubjectIdHashes.Contains(securityGroup)); } - internal static List GetRoleTokens(IEnumerable SecurityGroup) + internal static List GetRoleTokens(IEnumerable securityGroups) { - return _Cache.Where(t => SecurityGroup.Any(sg => t.SubjectIdHashes.Contains(sg))).Cast().ToList(); + return cache.Values.Where(t => securityGroups.Any(sg => t.SubjectIdHashes.Contains(sg))).Cast().ToList(); } - internal static List GetRoleTokens(IEnumerable SecurityGroup, User User) + internal static List GetRoleTokens(IEnumerable securityGroups, User user) { - var subjectIds = SecurityGroup.Concat(new string[] { User.UserId }); + var subjectIds = securityGroups.Concat(new string[] { user.UserId }); - return _Cache.Where(t => subjectIds.Any(sg => t.SubjectIdHashes.Contains(sg))).Cast().ToList(); + return cache.Values.Where(t => subjectIds.Any(sg => t.SubjectIdHashes.Contains(sg))).Cast().ToList(); } /// diff --git a/Disco.Services/Devices/DeviceFlags/Cache.cs b/Disco.Services/Devices/DeviceFlags/Cache.cs index c680148e..69f8802f 100644 --- a/Disco.Services/Devices/DeviceFlags/Cache.cs +++ b/Disco.Services/Devices/DeviceFlags/Cache.cs @@ -8,7 +8,7 @@ namespace Disco.Services.Devices.DeviceFlags { internal class Cache { - private ConcurrentDictionary _Cache; + private ConcurrentDictionary 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(flags.Select(f => new KeyValuePair(f.Id, f))); + cache = new ConcurrentDictionary(flags.Select(f => new KeyValuePair(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 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 _); } } } diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs index 3c3dcb97..b1aebbd3 100644 --- a/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagExtensions.cs @@ -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) diff --git a/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs b/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs index 1496bda4..286b6d3a 100644 --- a/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs +++ b/Disco.Services/Devices/DeviceFlags/DeviceFlagService.cs @@ -13,7 +13,7 @@ namespace Disco.Services.Devices.DeviceFlags { public static class DeviceFlagService { - private static Cache _cache; + private static Cache cache; internal static Lazy> 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 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 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)); } } } diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index d050b135..f026d40b 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -609,6 +609,7 @@ + diff --git a/Disco.Services/Users/UserFlags/Cache.cs b/Disco.Services/Users/UserFlags/Cache.cs index 984b0c0d..ab2eb601 100644 --- a/Disco.Services/Users/UserFlags/Cache.cs +++ b/Disco.Services/Users/UserFlags/Cache.cs @@ -8,7 +8,7 @@ namespace Disco.Services.Users.UserFlags { internal class Cache { - private ConcurrentDictionary _Cache; + private ConcurrentDictionary cache; public Cache(DiscoDataContext Database) { @@ -26,36 +26,30 @@ namespace Disco.Services.Users.UserFlags var flags = Database.UserFlags.ToList(); // Add Queues to In-Memory Cache - _Cache = new ConcurrentDictionary(flags.Select(f => new KeyValuePair(f.Id, f))); + cache = new ConcurrentDictionary(flags.Select(f => new KeyValuePair(f.Id, (f, f.Permissions)))); } - public UserFlag GetUserFlag(int UserFlagId) + public (UserFlag flag, FlagPermission permission) GetUserFlag(int UserFlagId) { - if (_Cache.TryGetValue(UserFlagId, out var item)) + if (cache.TryGetValue(UserFlagId, out var item)) return item; else - return null; + return (null, null); } - public List GetUserFlags() + public List<(UserFlag flag, FlagPermission permission)> GetUserFlags() { - return _Cache.Values.ToList(); + return cache.Values.ToList(); } public void AddOrUpdate(UserFlag UserFlag) { - _Cache.AddOrUpdate(UserFlag.Id, UserFlag, (key, existingItem) => UserFlag); + var value = (UserFlag, UserFlag.Permissions); + cache.AddOrUpdate(UserFlag.Id, value, (key, existingItem) => value); } - public UserFlag Remove(int UserFlagId) + public void Remove(int UserFlagId) { - if (_Cache.TryRemove(UserFlagId, out var item)) - return item; - else - return null; - } - public UserFlag Remove(UserFlag UserFlag) - { - return Remove(UserFlag.Id); + cache.TryRemove(UserFlagId, out _); } } } diff --git a/Disco.Services/Users/UserFlags/FlagPermissionExtensions.cs b/Disco.Services/Users/UserFlags/FlagPermissionExtensions.cs new file mode 100644 index 00000000..55f4c2a2 --- /dev/null +++ b/Disco.Services/Users/UserFlags/FlagPermissionExtensions.cs @@ -0,0 +1,144 @@ +using Disco.Models.Repository; +using Disco.Services.Authorization; +using Disco.Services.Devices.DeviceFlags; +using Disco.Services.Users; +using Disco.Services.Users.UserFlags; +using System.Collections.Generic; +using System.Linq; + +namespace Disco +{ + public static class FlagPermissionExtensions + { + public static bool CanShow(this FlagPermission permission) + { + var authorization = UserService.CurrentAuthorization; + + // inherited permission + if (permission.Inherit && + authorization.Has(permission.FlagType == FlagType.User ? Claims.User.ShowFlagAssignments : Claims.Device.ShowFlagAssignments)) + { + return true; + } + + // permission override + if (permission != null && ( + permission.CanShowSubjectIds.Contains(authorization.User.UserId) || + permission.CanShowSubjectIds.Overlaps(authorization.GroupMembership) || + permission.CanShowSubjectIds.Overlaps(authorization.RoleTokens.Select(r => $"[{r.Role.Id}]")) + )) + { + return true; + } + + return false; + } + public static bool CanAssign(this FlagPermission permission) + { + var authorization = UserService.CurrentAuthorization; + + // inherited permission + if (permission.Inherit && + authorization.Has(permission.FlagType == FlagType.User ? Claims.User.Actions.AddFlags : Claims.Device.Actions.AddFlags)) + { + return true; + } + + // permission override + if (permission != null && ( + permission.CanAssignSubjectIds.Contains(authorization.User.UserId) || + permission.CanAssignSubjectIds.Overlaps(authorization.GroupMembership) || + permission.CanAssignSubjectIds.Overlaps(authorization.RoleTokens.Select(r => $"[{r.Role.Id}]")) + )) + { + return true; + } + + return false; + } + public static bool CanEdit(this FlagPermission permission) + { + var authorization = UserService.CurrentAuthorization; + + // inherited permission + if (permission.Inherit && + authorization.Has(permission.FlagType == FlagType.User ? Claims.User.Actions.EditFlags : Claims.Device.Actions.EditFlags)) + { + return true; + } + + // permission override + if (permission != null && ( + permission.CanEditSubjectIds.Contains(authorization.User.UserId) || + permission.CanEditSubjectIds.Overlaps(authorization.GroupMembership) || + permission.CanEditSubjectIds.Overlaps(authorization.RoleTokens.Select(r => $"[{r.Role.Id}]")) + )) + { + return true; + } + + return false; + } + public static bool CanRemove(this FlagPermission permission) + { + var authorization = UserService.CurrentAuthorization; + + // inherited permission + if (permission.Inherit && + authorization.Has(permission.FlagType == FlagType.User ? Claims.User.Actions.RemoveFlags : Claims.Device.Actions.RemoveFlags)) + { + return true; + } + + // permission override + if (permission != null && ( + permission.CanRemoveSubjectIds.Contains(authorization.User.UserId) || + permission.CanRemoveSubjectIds.Overlaps(authorization.GroupMembership) || + permission.CanRemoveSubjectIds.Overlaps(authorization.RoleTokens.Select(r => $"[{r.Role.Id}]")) + )) + { + return true; + } + + return false; + } + + public static bool CanShowAny(this IEnumerable assignments) + { + if (assignments == null) + return false; + + foreach (var assignment in assignments) + { + if (assignment.RemovedDate.HasValue) + continue; + + var (_, permission) = UserFlagService.GetUserFlag(assignment.UserFlagId); + + if (permission.CanShow()) + return true; + } + + return false; + } + + public static bool CanShowAny(this IEnumerable assignments) + { + if (assignments == null) + return false; + + foreach (var assignment in assignments) + { + if (assignment.RemovedDate.HasValue) + continue; + + var (_, permission) = DeviceFlagService.GetDeviceFlag(assignment.DeviceFlagId); + + if (permission.CanShow()) + return true; + } + + return false; + } + } +} diff --git a/Disco.Services/Users/UserFlags/UserFlagExtensions.cs b/Disco.Services/Users/UserFlags/UserFlagExtensions.cs index aa77d5ce..60221600 100644 --- a/Disco.Services/Users/UserFlags/UserFlagExtensions.cs +++ b/Disco.Services/Users/UserFlags/UserFlagExtensions.cs @@ -4,6 +4,7 @@ using Disco.Services.Authorization; using Disco.Services.Expressions; using Disco.Services.Logging; using Disco.Services.Users; +using Disco.Services.Users.UserFlags; using System; using System.Collections; using System.Linq; @@ -14,16 +15,18 @@ namespace Disco.Services { #region Edit Comments - public static bool CanEditComments(this UserFlagAssignment fa) + public static bool CanEdit(this UserFlagAssignment fa) { - return UserService.CurrentAuthorization.Has(Claims.User.Actions.EditFlags); + var (_, permission) = UserFlagService.GetUserFlag(fa.UserFlagId); + + return permission.CanEdit(); } - public static void OnEditComments(this UserFlagAssignment fa, string Comments) + public static void OnEdit(this UserFlagAssignment fa, string comments) { - if (!fa.CanEditComments()) + if (!fa.CanEdit()) throw new InvalidOperationException("Editing comments for user flags is denied"); - fa.Comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim(); + fa.Comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim(); } #endregion @@ -33,34 +36,36 @@ namespace Disco.Services if (fa.RemovedDate.HasValue) return false; - return UserService.CurrentAuthorization.Has(Claims.User.Actions.RemoveFlags); + var (_, permission) = UserFlagService.GetUserFlag(fa.UserFlagId); + + return permission.CanRemove(); } - public static void OnRemove(this UserFlagAssignment fa, DiscoDataContext Database, User RemovingUser) + public static void OnRemove(this UserFlagAssignment fa, DiscoDataContext database) { if (!fa.CanRemove()) throw new InvalidOperationException("Removing user flags is denied"); - fa.OnRemoveUnsafe(Database, RemovingUser); + fa.OnRemoveUnsafe(database, UserService.CurrentUser); } - public static void OnRemoveUnsafe(this UserFlagAssignment fa, DiscoDataContext Database, User RemovingUser) + public static void OnRemoveUnsafe(this UserFlagAssignment fa, DiscoDataContext database, User removingUser) { - fa = Database.UserFlagAssignments.First(a => a.Id == fa.Id); - RemovingUser = Database.Users.First(u => u.UserId == RemovingUser.UserId); + fa = database.UserFlagAssignments.First(a => a.Id == fa.Id); + 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.UserFlag.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) @@ -72,58 +77,52 @@ namespace Disco.Services #endregion #region Add - public static bool CanAddUserFlags(this User u) - { - return UserService.CurrentAuthorization.Has(Claims.User.Actions.AddFlags); - } public static bool CanAddUserFlag(this User u, UserFlag flag) { - // Shortcut - if (!u.CanAddUserFlags()) - return false; - // Already has User Flag? if (u.UserFlagAssignments.Any(fa => !fa.RemovedDate.HasValue && fa.UserFlagId == flag.Id)) return false; - return true; + var (_, permission) = UserFlagService.GetUserFlag(flag.Id); + + return permission.CanAssign(); } - public static UserFlagAssignment OnAddUserFlag(this User u, DiscoDataContext Database, UserFlag flag, User AddingUser, string Comments) + public static UserFlagAssignment OnAddUserFlag(this User u, DiscoDataContext database, UserFlag flag, string comments) { if (!u.CanAddUserFlag(flag)) throw new InvalidOperationException("Adding user flag is denied"); - return u.OnAddUserFlagUnsafe(Database, flag, AddingUser, Comments); + return u.OnAddUserFlagUnsafe(database, flag, UserService.CurrentUser, comments); } - public static UserFlagAssignment OnAddUserFlagUnsafe(this User u, DiscoDataContext Database, UserFlag flag, User AddingUser, string Comments) + public static UserFlagAssignment OnAddUserFlagUnsafe(this User u, DiscoDataContext database, UserFlag flag, User addingUser, string comments) { - flag = Database.UserFlags.First(f => f.Id == flag.Id); - u = Database.Users.First(user => user.UserId == u.UserId); - AddingUser = Database.Users.First(user => user.UserId == AddingUser.UserId); + flag = database.UserFlags.First(f => f.Id == flag.Id); + u = database.Users.First(user => user.UserId == u.UserId); + addingUser = database.Users.First(user => user.UserId == addingUser.UserId); var fa = new UserFlagAssignment() { UserFlag = flag, User = u, 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.UserFlagAssignments.Add(fa); + database.UserFlagAssignments.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) diff --git a/Disco.Services/Users/UserFlags/UserFlagService.cs b/Disco.Services/Users/UserFlags/UserFlagService.cs index ae1bc55e..88699bb3 100644 --- a/Disco.Services/Users/UserFlags/UserFlagService.cs +++ b/Disco.Services/Users/UserFlags/UserFlagService.cs @@ -5,6 +5,7 @@ using Disco.Services.Extensions; using Disco.Services.Tasks; using System; using System.Collections.Generic; +using System.Data.Entity; using System.Linq; using System.Reactive.Linq; @@ -12,7 +13,7 @@ namespace Disco.Services.Users.UserFlags { public static class UserFlagService { - private static Cache _cache; + private static Cache cache; internal static Lazy> UserFlagAssignmentRepositoryEvents; static UserFlagService() @@ -24,25 +25,58 @@ namespace Disco.Services.Users.UserFlags RepositoryMonitor.StreamAfterCommit.Where(e => e.EntityType == typeof(UserFlagAssignment) && (e.EventType != RepositoryMonitorEventType.Modified || - e.ModifiedProperties.Contains("RemovedDate")) + e.ModifiedProperties.Contains(nameof(UserFlagAssignment.RemovedDate))) ) ); } public static void Initialize(DiscoDataContext Database) { - _cache = new Cache(Database); + cache = new Cache(Database); // Initialize Managed Groups (if configured) - _cache.GetUserFlags().ForEach(uf => + cache.GetUserFlags().ForEach(uf => { - UserFlagUsersManagedGroup.Initialize(uf); - UserFlagUserDevicesManagedGroup.Initialize(uf); + UserFlagUsersManagedGroup.Initialize(uf.flag); + UserFlagUserDevicesManagedGroup.Initialize(uf.flag); }); } - public static List GetUserFlags() { return _cache.GetUserFlags(); } - public static UserFlag GetUserFlag(int UserFlagId) { return _cache.GetUserFlag(UserFlagId); } + public static IEnumerable<(UserFlag flag, FlagPermission permission)> GetUserFlags() { return cache.GetUserFlags(); } + public static (UserFlag flag, FlagPermission permission) GetUserFlag(int UserFlagId) { return cache.GetUserFlag(UserFlagId); } + + public static UserFlag GetAvailableUserFlag(int userFlagId, User targetUser) + { + var (userFlag, permission) = cache.GetUserFlag(userFlagId); + + if (targetUser.UserFlagAssignments + .Where(a => a.UserFlagId == userFlagId && !a.RemovedDate.HasValue).Any()) + return null; + + if (permission.CanAssign()) + return userFlag; + + return null; + } + + public static IEnumerable GetAvailableUserFlags(User targetUser) + { + var records = cache.GetUserFlags(); + + var usedFlags = targetUser.UserFlagAssignments + .Where(a => !a.RemovedDate.HasValue) + .Select(a => a.UserFlagId) + .ToList(); + + foreach (var (flag, permission) in records) + { + if (usedFlags.Contains(flag.Id)) + continue; + + if (permission.CanAssign()) + yield return flag; + } + } #region User Flag Maintenance public static UserFlag CreateUserFlag(DiscoDataContext Database, string name, string description) @@ -52,7 +86,7 @@ namespace Disco.Services.Users.UserFlags throw new ArgumentException("The User Flag Name is required", nameof(name)); // Name Unique - if (_cache.GetUserFlags().Any(f => f.Name.Equals(name, StringComparison.Ordinal))) + if (cache.GetUserFlags().Any(f => f.flag.Name.Equals(name, StringComparison.Ordinal))) throw new ArgumentException("Another User Flag already exists with that name", nameof(name)); // Clone to break reference @@ -67,7 +101,7 @@ namespace Disco.Services.Users.UserFlags Database.UserFlags.Add(flag); Database.SaveChanges(); - _cache.AddOrUpdate(flag); + cache.AddOrUpdate(flag); return flag; } @@ -78,12 +112,12 @@ namespace Disco.Services.Users.UserFlags throw new ArgumentException("The User Flag Name is required"); // Name Unique - if (_cache.GetUserFlags().Any(f => f.Id != UserFlag.Id && f.Name == UserFlag.Name)) - throw new ArgumentException("Another User Flag already exists with that name", "UserFlag"); + if (cache.GetUserFlags().Any(f => f.flag.Id != UserFlag.Id && f.flag.Name == UserFlag.Name)) + throw new ArgumentException("Another User Flag already exists with that name", nameof(UserFlag)); Database.SaveChanges(); - _cache.AddOrUpdate(UserFlag); + cache.AddOrUpdate(UserFlag); UserFlagUsersManagedGroup.Initialize(UserFlag); UserFlagUserDevicesManagedGroup.Initialize(UserFlag); @@ -113,22 +147,22 @@ namespace Disco.Services.Users.UserFlags Database.SaveChanges(); // Remove from Cache - _cache.Remove(UserFlagId); + cache.Remove(UserFlagId); Status.Finished($"Successfully Deleted User Flag: '{flag.Name}' [{flag.Id}]"); } #endregion #region Bulk Assignment - public static IEnumerable BulkAssignAddUsers(DiscoDataContext Database, UserFlag UserFlag, User Technician, string Comments, List Users, IScheduledTaskStatus Status) + public static IEnumerable BulkAssignAddUsers(DiscoDataContext database, UserFlag userFlag, User techUser, string comments, List users, IScheduledTaskStatus status) { - if (Users.Count > 0) + if (users.Count > 0) { double progressInterval; const int databaseChunkSize = 100; - string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim(); + comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim(); - var addUsers = Users.Where(u => !u.UserFlagAssignments.Any(a => a.UserFlagId == UserFlag.Id && !a.RemovedDate.HasValue)).ToList(); + var addUsers = users.Where(u => !u.UserFlagAssignments.Any(a => a.UserFlagId == userFlag.Id && !a.RemovedDate.HasValue)).ToList(); progressInterval = (double)100 / addUsers.Count; @@ -138,39 +172,39 @@ namespace Disco.Services.Users.UserFlags var chunkResults = chunk.Select((user, index) => { - Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {user.ToString()}"); + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {user}"); - return user.OnAddUserFlag(Database, UserFlag, Technician, comments); + return user.OnAddUserFlagUnsafe(database, userFlag, techUser, comments); }).ToList(); // Save Chunk Items to Database - Database.SaveChanges(); + database.SaveChanges(); return chunkResults; }).Where(fa => fa != null).ToList(); - Status.SetFinishedMessage($"{addUsers.Count} Users/s Added; {(Users.Count - addUsers.Count)} User/s Skipped"); + status.SetFinishedMessage($"{addUsers.Count} Users/s Added; {users.Count - addUsers.Count} User/s Skipped"); return addedUserAssignments; } else { - Status.SetFinishedMessage("No changes found"); + status.SetFinishedMessage("No changes found"); return Enumerable.Empty(); } } - public static IEnumerable BulkAssignOverrideUsers(DiscoDataContext Database, UserFlag UserFlag, User Technician, string Comments, List Users, IScheduledTaskStatus Status) + public static IEnumerable BulkAssignOverrideUsers(DiscoDataContext database, UserFlag userFlag, User techUser, string comments, List users, IScheduledTaskStatus status) { double progressInterval; const int databaseChunkSize = 100; - string comments = string.IsNullOrWhiteSpace(Comments) ? null : Comments.Trim(); + comments = string.IsNullOrWhiteSpace(comments) ? null : comments.Trim(); - Status.UpdateStatus(0, "Calculating assignment changes"); + status.UpdateStatus(0, "Calculating assignment changes"); - var currentAssignments = Database.UserFlagAssignments.Include("User").Where(a => a.UserFlagId == UserFlag.Id && !a.RemovedDate.HasValue).ToList(); - var removeAssignments = currentAssignments.Where(ca => !Users.Any(u => u.UserId.Equals(ca.UserId, StringComparison.OrdinalIgnoreCase))).ToList(); - var addUsers = Users.Where(u => !currentAssignments.Any(ca => ca.UserId.Equals(u.UserId, StringComparison.OrdinalIgnoreCase))).ToList(); + var currentAssignments = database.UserFlagAssignments.Include(a => a.User).Where(a => a.UserFlagId == userFlag.Id && !a.RemovedDate.HasValue).ToList(); + var removeAssignments = currentAssignments.Where(ca => !users.Any(u => u.UserId.Equals(ca.UserId, StringComparison.OrdinalIgnoreCase))).ToList(); + var addUsers = users.Where(u => !currentAssignments.Any(ca => ca.UserId.Equals(u.UserId, StringComparison.OrdinalIgnoreCase))).ToList(); if (removeAssignments.Count > 0 || addUsers.Count > 0) { @@ -184,15 +218,15 @@ namespace Disco.Services.Users.UserFlags var chunkResults = chunk.Select((flagAssignment, index) => { - Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Removing Flag: {flagAssignment.User.ToString()}"); + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Removing Flag: {flagAssignment.User}"); - flagAssignment.OnRemoveUnsafe(Database, Technician); + flagAssignment.OnRemoveUnsafe(database, techUser); return flagAssignment; }).ToList(); // Save Chunk Items to Database - Database.SaveChanges(); + database.SaveChanges(); return chunkResults; }).ToList(); @@ -204,24 +238,24 @@ namespace Disco.Services.Users.UserFlags var chunkResults = chunk.Select((user, index) => { - Status.UpdateStatus((chunkIndexOffset + index) * progressInterval, $"Assigning Flag: {user.ToString()}"); + status.UpdateStatus((chunkIndexOffset + index) * progressInterval, string.Format("Assigning Flag: {0}", user.ToString())); - return user.OnAddUserFlag(Database, UserFlag, Technician, comments); + return user.OnAddUserFlagUnsafe(database, userFlag, techUser, comments); }).ToList(); // Save Chunk Items to Database - Database.SaveChanges(); + database.SaveChanges(); return chunkResults; }).ToList(); - Status.SetFinishedMessage($"{addUsers.Count} Users/s Added; {removeAssignments.Count} User/s Removed; {(Users.Count - addUsers.Count)} User/s Skipped"); + status.SetFinishedMessage($"{addUsers.Count} Users/s Added; {removeAssignments.Count} User/s Removed; {users.Count - addUsers.Count} User/s Skipped"); return addedUserAssignments; } else { - Status.SetFinishedMessage("No changes found"); + status.SetFinishedMessage("No changes found"); return Enumerable.Empty(); } } @@ -229,11 +263,11 @@ namespace Disco.Services.Users.UserFlags public static string RandomUnusedIcon() { - return UIHelpers.RandomIcon(_cache.GetUserFlags().Select(f => f.Icon)); + return UIHelpers.RandomIcon(cache.GetUserFlags().Select(f => f.flag.Icon)); } public static string RandomUnusedThemeColour() { - return UIHelpers.RandomThemeColour(_cache.GetUserFlags().Select(f => f.IconColour)); + return UIHelpers.RandomThemeColour(cache.GetUserFlags().Select(f => f.flag.IconColour)); } } } diff --git a/Disco.Services/Users/UserService.cs b/Disco.Services/Users/UserService.cs index b928629a..44a9e4ba 100644 --- a/Disco.Services/Users/UserService.cs +++ b/Disco.Services/Users/UserService.cs @@ -154,7 +154,7 @@ namespace Disco.Services.Users AuthorizationLog.LogRoleCreated(role, CurrentUserId); // Add to Cache - RoleCache.AddRole(role); + RoleCache.AddOrUpdateRole(role); // Flush User Cache Cache.FlushCache(); @@ -164,7 +164,7 @@ namespace Disco.Services.Users public static void DeleteAuthorizationRole(DiscoDataContext Database, AuthorizationRole Role) { if (Role == null) - throw new ArgumentNullException("Role"); + throw new ArgumentNullException(nameof(Role)); Database.AuthorizationRoles.Remove(Role); Database.SaveChanges(); @@ -180,19 +180,27 @@ namespace Disco.Services.Users public static void UpdateAuthorizationRole(DiscoDataContext Database, AuthorizationRole Role) { if (Role == null) - throw new ArgumentNullException("Role"); + throw new ArgumentNullException(nameof(Role)); if (Database == null) - throw new ArgumentNullException("Database"); + throw new ArgumentNullException(nameof(Database)); Database.SaveChanges(); // Update Role Cache - RoleCache.UpdateRole(Role); + RoleCache.AddOrUpdateRole(Role); // Flush User Cache Cache.FlushCache(); } + public static string GetAuthorizationRoleName(int roleId) + { + var role = RoleCache.GetRoleToken(roleId); + if (role == null) + return "Unknown authorization role"; + return role.Role.Name; + } + public static IEnumerable AdministratorSubjectIds { get diff --git a/Disco.Services/Web/BaseController.cs b/Disco.Services/Web/BaseController.cs index f948da9e..d04ea6f1 100644 --- a/Disco.Services/Web/BaseController.cs +++ b/Disco.Services/Web/BaseController.cs @@ -12,7 +12,13 @@ namespace Disco.Services.Web protected static HttpStatusCodeResult BadRequest(string message = null) => StatusCode(HttpStatusCode.BadRequest, message); - protected static HttpStatusCodeResult StatusCode(HttpStatusCode statusCode, string message = null) - => new HttpStatusCodeResult(statusCode, message); + protected static HttpStatusCodeResult StatusCode(HttpStatusCode statusCode, string statusDescription = null) + => new HttpStatusCodeResult(statusCode, statusDescription); + + protected static HttpNotFoundResult NotFound(string statusDescription = null) + => new HttpNotFoundResult(statusDescription); + + protected static HttpUnauthorizedResult Unauthorized(string statusDescription = null) + => new HttpUnauthorizedResult(statusDescription); } } diff --git a/Disco.Web/Areas/API/Controllers/AuthorizationRoleController.cs b/Disco.Web/Areas/API/Controllers/AuthorizationRoleController.cs index 1a1be917..569472ae 100644 --- a/Disco.Web/Areas/API/Controllers/AuthorizationRoleController.cs +++ b/Disco.Web/Areas/API/Controllers/AuthorizationRoleController.cs @@ -78,14 +78,16 @@ namespace Disco.Web.Areas.API.Controllers } } - private void UpdateClaims(AuthorizationRole AuthorizationRole, string[] ClaimKeys) + private void UpdateClaims(AuthorizationRole AuthorizationRole, string[] claimKeys) { - var proposedClaims = Claims.BuildClaims(ClaimKeys); + claimKeys = claimKeys ?? Array.Empty(); + + var proposedClaims = Claims.BuildClaims(claimKeys); var currentToken = RoleToken.FromAuthorizationRole(AuthorizationRole); var currentClaimKeys = Claims.GetClaimKeys(currentToken.Claims); - var removedClaims = currentClaimKeys.Except(ClaimKeys).ToArray(); - var addedClaims = ClaimKeys.Except(currentClaimKeys).ToArray(); + var removedClaims = currentClaimKeys.Except(claimKeys).ToArray(); + var addedClaims = claimKeys.Except(currentClaimKeys).ToArray(); AuthorizationRole.SetClaims(proposedClaims); UserService.UpdateAuthorizationRole(Database, AuthorizationRole); diff --git a/Disco.Web/Areas/API/Controllers/DeviceFlagAssignmentController.cs b/Disco.Web/Areas/API/Controllers/DeviceFlagAssignmentController.cs index e20b0838..4fcb43df 100644 --- a/Disco.Web/Areas/API/Controllers/DeviceFlagAssignmentController.cs +++ b/Disco.Web/Areas/API/Controllers/DeviceFlagAssignmentController.cs @@ -1,6 +1,5 @@ using Disco.Models.Repository; using Disco.Services; -using Disco.Services.Authorization; using Disco.Services.Web; using System; using System.Data.Entity; @@ -12,7 +11,7 @@ namespace Disco.Web.Areas.API.Controllers public partial class DeviceFlagAssignmentController : AuthorizedDatabaseController { const string pComments = "comments"; - + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult Update(int id, string key, string value = null, bool? redirect = null) { try @@ -21,7 +20,9 @@ namespace Disco.Web.Areas.API.Controllers throw new ArgumentOutOfRangeException(nameof(id)); if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); - var assignment = Database.DeviceFlagAssignments.FirstOrDefault(a => a.Id == id); + var assignment = Database.DeviceFlagAssignments + .Include(a => a.DeviceFlag) + .FirstOrDefault(a => a.Id == id); if (assignment != null) { switch (key.ToLower()) @@ -52,7 +53,7 @@ namespace Disco.Web.Areas.API.Controllers } #region Update Shortcut Methods - [DiscoAuthorizeAny(Claims.Device.Actions.EditFlags)] + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult UpdateComments(int id, string Comments = null, bool? redirect = null) { return Update(id, pComments, Comments, redirect); @@ -60,20 +61,19 @@ namespace Disco.Web.Areas.API.Controllers #endregion #region Update Properties - private void UpdateComments(DeviceFlagAssignment assignment, string Comments) + private void UpdateComments(DeviceFlagAssignment assignment, string comments) { - if (!assignment.CanEditComments()) + if (!assignment.CanEdit()) throw new InvalidOperationException("Editing comments for device flags is denied"); - assignment.OnEditComments(Comments); + assignment.OnEdit(comments); Database.SaveChanges(); } #endregion #region Actions - - [DiscoAuthorizeAny(Claims.Device.Actions.AddFlags)] - public virtual ActionResult AddDevice(int id, string DeviceSerialNumber, string Comments) + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult AddDevice(int id, string deviceSerialNumber, string comments) { Database.Configuration.LazyLoadingEnabled = true; @@ -81,37 +81,35 @@ namespace Disco.Web.Areas.API.Controllers if (flag == null) throw new ArgumentException("Invalid Device Flag Id", nameof(id)); - var device = Database.Devices.Include(u => u.DeviceFlagAssignments).FirstOrDefault(d => d.SerialNumber == DeviceSerialNumber); + var device = Database.Devices.Include(u => u.DeviceFlagAssignments).FirstOrDefault(d => d.SerialNumber == deviceSerialNumber); if (device == null) - throw new ArgumentException("Invalid Device Serial Number", nameof(DeviceSerialNumber)); + throw new ArgumentException("Invalid Device Serial Number", nameof(deviceSerialNumber)); if (!device.CanAddDeviceFlag(flag)) - throw new InvalidOperationException("Adding device flag is denied"); + return Unauthorized("Adding device flag is denied"); - var addingUser = Database.Users.Find(CurrentUser.UserId); - - var assignment = device.OnAddDeviceFlag(Database, flag, addingUser, Comments); + var assignment = device.OnAddDeviceFlag(Database, flag, comments); Database.SaveChanges(); return Redirect($"{Url.Action(MVC.Device.Show(device.SerialNumber))}#DeviceDetailTab-Flags"); } - [DiscoAuthorizeAny(Claims.Device.Actions.RemoveFlags)] + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult RemoveDevice(int id) { Database.Configuration.LazyLoadingEnabled = true; - var assignment = Database.DeviceFlagAssignments.FirstOrDefault(a => a.Id == id); + var assignment = Database.DeviceFlagAssignments + .Include(a => a.DeviceFlag) + .FirstOrDefault(a => a.Id == id); if (assignment == null) throw new ArgumentException("Invalid Device Flag Assignment Id", nameof(id)); if (!assignment.CanRemove()) - throw new InvalidOperationException("Removing device flag assignment is denied"); + return Unauthorized("Removing device flag assignment is denied"); - var removingUser = Database.Users.Find(CurrentUser.UserId); - - assignment.OnRemove(Database, removingUser); + assignment.OnRemove(Database); Database.SaveChanges(); return Redirect($"{Url.Action(MVC.Device.Show(assignment.DeviceSerialNumber))}#DeviceDetailTab-Flags"); @@ -120,4 +118,4 @@ namespace Disco.Web.Areas.API.Controllers #endregion } -} \ No newline at end of file +} diff --git a/Disco.Web/Areas/API/Controllers/DeviceFlagController.cs b/Disco.Web/Areas/API/Controllers/DeviceFlagController.cs index 98766d67..090ec0aa 100644 --- a/Disco.Web/Areas/API/Controllers/DeviceFlagController.cs +++ b/Disco.Web/Areas/API/Controllers/DeviceFlagController.cs @@ -5,7 +5,9 @@ using Disco.Services.Devices.DeviceFlags; using Disco.Services.Exporting; using Disco.Services.Interop.ActiveDirectory; using Disco.Services.Tasks; +using Disco.Services.Users.UserFlags; using Disco.Services.Web; +using Disco.Web.Areas.API.Models.Shared; using Disco.Web.Areas.Config.Models.DeviceFlag; using Disco.Web.Extensions; using System; @@ -467,6 +469,24 @@ namespace Disco.Web.Areas.API.Controllers return RedirectToAction(MVC.Config.Export.Create(savedExport.Id)); } + [DiscoAuthorize(Claims.Config.DeviceFlag.Configure)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult Permission(int id, FlagPermissionModel model = null) + { + var deviceFlag = Database.DeviceFlags.Find(id); + + if (deviceFlag == null) + return NotFound(); + + if (model == null || !model.IsOverride) + deviceFlag.Permissions = null; + else + deviceFlag.Permissions = model.ToFlagPermission(deviceFlag); + + DeviceFlagService.Update(Database, deviceFlag); + + return RedirectToAction(MVC.Config.DeviceFlag.Index(deviceFlag.Id)); + } #endregion } } \ No newline at end of file diff --git a/Disco.Web/Areas/API/Controllers/SystemController.cs b/Disco.Web/Areas/API/Controllers/SystemController.cs index 09fbc256..8f9592b2 100644 --- a/Disco.Web/Areas/API/Controllers/SystemController.cs +++ b/Disco.Web/Areas/API/Controllers/SystemController.cs @@ -4,7 +4,9 @@ using Disco.Services.Authorization; using Disco.Services.Interop.ActiveDirectory; using Disco.Services.Interop.DiscoServices; using Disco.Services.Messaging; +using Disco.Services.Users; using Disco.Services.Web; +using Disco.Web.Areas.API.Models.Shared; using System; using System.Collections.Generic; using System.Drawing; @@ -320,38 +322,64 @@ namespace Disco.Web.Areas.API.Controllers }; } - [DiscoAuthorizeAny(Claims.DiscoAdminAccount, Claims.Config.JobQueue.Configure)] - public virtual ActionResult SearchSubjects(string term) + [DiscoAuthorizeAny(Claims.DiscoAdminAccount, Claims.Config.JobQueue.Configure, Claims.Config.UserFlag.Configure, Claims.Config.DeviceFlag.Configure)] + public virtual ActionResult SearchSubjects(string term, bool includeAuthorizationRoles = false) { - var groupResults = ActiveDirectory.SearchADGroups(term).Cast(); - var userResults = ActiveDirectory.SearchADUserAccounts(term, true).Cast(); + var groupResults = ActiveDirectory.SearchADGroups(term).Select(r => SubjectDescriptorModel.FromActiveDirectoryObject(r)); + var userResults = ActiveDirectory.SearchADUserAccounts(term, true).Select(r => SubjectDescriptorModel.FromActiveDirectoryObject(r)); - var results = groupResults.Concat(userResults).OrderBy(r => r.SamAccountName) - .Select(r => Models.Shared.SubjectDescriptorModel.FromActiveDirectoryObject(r)).ToList(); + IEnumerable roleResults; + if (includeAuthorizationRoles) + { + roleResults = Database.AuthorizationRoles.AsNoTracking().Where(r => r.Name.Contains(term)) + .ToList() + .Select(r => SubjectDescriptorModel.FromAuthorizationRole(r)); + } + else + roleResults = Enumerable.Empty(); + + var results = groupResults.Concat(userResults).Concat(roleResults) + .OrderBy(r => r.Id).ToList(); return Json(results, JsonRequestBehavior.AllowGet); } - [DiscoAuthorizeAny(Claims.Config.UserFlag.Configure)] + [DiscoAuthorizeAny(Claims.DiscoAdminAccount, Claims.Config.DeviceProfile.Configure, Claims.Config.DocumentTemplate.Configure, Claims.Config.Plugin.Configure, Claims.Config.UserFlag.Configure, Claims.Config.DeviceFlag.Configure)] public virtual ActionResult SearchGroupSubjects(string term) { var groupResults = ActiveDirectory.SearchADGroups(term).Cast(); var results = groupResults.OrderBy(r => r.SamAccountName) - .Select(r => Models.Shared.SubjectDescriptorModel.FromActiveDirectoryObject(r)).ToList(); + .Select(r => SubjectDescriptorModel.FromActiveDirectoryObject(r)).ToList(); return Json(results, JsonRequestBehavior.AllowGet); } - [DiscoAuthorizeAny(Claims.DiscoAdminAccount, Claims.Config.JobQueue.Configure)] - public virtual ActionResult Subject(string Id) + [DiscoAuthorizeAny(Claims.DiscoAdminAccount, Claims.Config.JobQueue.Configure, Claims.Config.UserFlag.Configure, Claims.Config.DeviceFlag.Configure)] + public virtual ActionResult Subject(string Id, bool includeAuthorizationRoles = false) { + if (string.IsNullOrWhiteSpace(Id)) + return Json(null, JsonRequestBehavior.AllowGet); + + if (Id.StartsWith("[", StringComparison.Ordinal)) + { + if (includeAuthorizationRoles && int.TryParse(Id.Trim('[', ']'), out var roleId)) + { + var roleName = UserService.GetAuthorizationRoleName(roleId); + if (roleName != null) + { + return Json(SubjectDescriptorModel.FromAuthorizationRole(roleId, roleName), JsonRequestBehavior.AllowGet); + } + } + return Json(null, JsonRequestBehavior.AllowGet); + } + var subject = ActiveDirectory.RetrieveADObject(Id, Quick: true); if (subject == null) return Json(null, JsonRequestBehavior.AllowGet); else - return Json(Models.Shared.SubjectDescriptorModel.FromActiveDirectoryObject(subject), JsonRequestBehavior.AllowGet); + return Json(SubjectDescriptorModel.FromActiveDirectoryObject(subject), JsonRequestBehavior.AllowGet); } [DiscoAuthorizeAny(Claims.Config.UserFlag.Configure, Claims.Config.DeviceFlag.Configure, Claims.Config.DeviceProfile.Configure, Claims.Config.DocumentTemplate.Configure)] diff --git a/Disco.Web/Areas/API/Controllers/UserFlagAssignmentController.cs b/Disco.Web/Areas/API/Controllers/UserFlagAssignmentController.cs index a89e3f2e..b243bba2 100644 --- a/Disco.Web/Areas/API/Controllers/UserFlagAssignmentController.cs +++ b/Disco.Web/Areas/API/Controllers/UserFlagAssignmentController.cs @@ -12,7 +12,7 @@ namespace Disco.Web.Areas.API.Controllers public partial class UserFlagAssignmentController : AuthorizedDatabaseController { const string pComments = "comments"; - + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult Update(int id, string key, string value = null, bool? redirect = null) { try @@ -21,7 +21,9 @@ namespace Disco.Web.Areas.API.Controllers throw new ArgumentOutOfRangeException(nameof(id)); if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); - var userFlagAssignment = Database.UserFlagAssignments.FirstOrDefault(a => a.Id == id); + var userFlagAssignment = Database.UserFlagAssignments + .Include(a => a.UserFlag) + .FirstOrDefault(a => a.Id == id); if (userFlagAssignment != null) { switch (key.ToLower()) @@ -52,7 +54,7 @@ namespace Disco.Web.Areas.API.Controllers } #region Update Shortcut Methods - [DiscoAuthorizeAny(Claims.User.Actions.EditFlags)] + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult UpdateComments(int id, string Comments = null, bool? redirect = null) { return Update(id, pComments, Comments, redirect); @@ -60,19 +62,19 @@ namespace Disco.Web.Areas.API.Controllers #endregion #region Update Properties - private void UpdateComments(UserFlagAssignment userFlagAssignment, string Comments) + private void UpdateComments(UserFlagAssignment userFlagAssignment, string comments) { - if (!userFlagAssignment.CanEditComments()) + if (!userFlagAssignment.CanEdit()) throw new InvalidOperationException("Editing comments for user flags is denied"); - userFlagAssignment.OnEditComments(Comments); + userFlagAssignment.OnEdit(comments); Database.SaveChanges(); } #endregion #region Actions - [DiscoAuthorizeAny(Claims.User.Actions.AddFlags)] + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult AddUser(int id, string UserId, string Comments) { Database.Configuration.LazyLoadingEnabled = true; @@ -86,32 +88,30 @@ namespace Disco.Web.Areas.API.Controllers throw new ArgumentException("Invalid User Id", nameof(UserId)); if (!user.CanAddUserFlag(userFlag)) - throw new InvalidOperationException("Adding user flag is denied"); + return Unauthorized("Adding user flag is denied"); - var addingUser = Database.Users.Find(CurrentUser.UserId); - - var userFlagAssignment = user.OnAddUserFlag(Database, userFlag, addingUser, Comments); + var userFlagAssignment = user.OnAddUserFlag(Database, userFlag, Comments); Database.SaveChanges(); return Redirect($"{Url.Action(MVC.User.Show(user.UserId))}#UserDetailTab-Flags"); } - [DiscoAuthorizeAny(Claims.User.Actions.RemoveFlags)] + [HttpPost, ValidateAntiForgeryToken] public virtual ActionResult RemoveUser(int id) { Database.Configuration.LazyLoadingEnabled = true; - var userFlagAssignment = Database.UserFlagAssignments.FirstOrDefault(a => a.Id == id); + var userFlagAssignment = Database.UserFlagAssignments + .Include(a => a.UserFlag) + .FirstOrDefault(a => a.Id == id); if (userFlagAssignment == null) throw new ArgumentException("Invalid User Flag Assignment Id", nameof(id)); if (!userFlagAssignment.CanRemove()) - throw new InvalidOperationException("Removing user flag assignment is denied"); + return Unauthorized("Removing user flag assignment is denied"); - var removingUser = Database.Users.Find(CurrentUser.UserId); - - userFlagAssignment.OnRemove(Database, removingUser); + userFlagAssignment.OnRemove(Database); Database.SaveChanges(); return Redirect($"{Url.Action(MVC.User.Show(userFlagAssignment.UserId))}#UserDetailTab-Flags"); @@ -120,4 +120,4 @@ namespace Disco.Web.Areas.API.Controllers #endregion } -} \ No newline at end of file +} diff --git a/Disco.Web/Areas/API/Controllers/UserFlagController.cs b/Disco.Web/Areas/API/Controllers/UserFlagController.cs index ccd26005..3b70088b 100644 --- a/Disco.Web/Areas/API/Controllers/UserFlagController.cs +++ b/Disco.Web/Areas/API/Controllers/UserFlagController.cs @@ -6,6 +6,7 @@ using Disco.Services.Interop.ActiveDirectory; using Disco.Services.Tasks; using Disco.Services.Users.UserFlags; using Disco.Services.Web; +using Disco.Web.Areas.API.Models.Shared; using Disco.Web.Areas.Config.Models.UserFlag; using Disco.Web.Extensions; using System; @@ -467,6 +468,24 @@ namespace Disco.Web.Areas.API.Controllers return RedirectToAction(MVC.Config.Export.Create(savedExport.Id)); } + [DiscoAuthorize(Claims.Config.UserFlag.Configure)] + [HttpPost, ValidateAntiForgeryToken] + public virtual ActionResult Permission(int id, FlagPermissionModel model = null) + { + var userFlag = Database.UserFlags.Find(id); + + if (userFlag == null) + return NotFound(); + + if (model == null || !model.IsOverride) + userFlag.Permissions = null; + else + userFlag.Permissions = model.ToFlagPermission(userFlag); + + UserFlagService.Update(Database, userFlag); + + return RedirectToAction(MVC.Config.UserFlag.Index(userFlag.Id)); + } #endregion } } \ No newline at end of file diff --git a/Disco.Web/Areas/API/Models/Shared/FlagPermissionModel.cs b/Disco.Web/Areas/API/Models/Shared/FlagPermissionModel.cs new file mode 100644 index 00000000..58912c38 --- /dev/null +++ b/Disco.Web/Areas/API/Models/Shared/FlagPermissionModel.cs @@ -0,0 +1,21 @@ +using Disco.Models.Repository; +using System.Collections.Generic; + +namespace Disco.Web.Areas.API.Models.Shared +{ + public class FlagPermissionModel + { + public bool IsOverride { get; set; } + public bool Inherit { get; set; } + public List CanShow { get; set; } + public List CanAssign { get; set; } + public List CanEdit { get; set; } + public List CanRemove { get; set; } + + public FlagPermission ToFlagPermission(UserFlag userFlag) + => FlagPermission.Create(userFlag, Inherit, CanShow, CanAssign, CanEdit, CanRemove); + + public FlagPermission ToFlagPermission(DeviceFlag deviceFlag) + => FlagPermission.Create(deviceFlag, Inherit, CanShow, CanAssign, CanEdit, CanRemove); + } +} diff --git a/Disco.Web/Areas/API/Models/Shared/SubjectDescriptorModel.cs b/Disco.Web/Areas/API/Models/Shared/SubjectDescriptorModel.cs index 3699998b..cdd5b2a3 100644 --- a/Disco.Web/Areas/API/Models/Shared/SubjectDescriptorModel.cs +++ b/Disco.Web/Areas/API/Models/Shared/SubjectDescriptorModel.cs @@ -1,4 +1,5 @@ -using Disco.Services.Interop.ActiveDirectory; +using Disco.Models.Repository; +using Disco.Services.Interop.ActiveDirectory; namespace Disco.Web.Areas.API.Models.Shared { @@ -43,5 +44,19 @@ namespace Disco.Web.Areas.API.Models.Shared return item; } + + public static SubjectDescriptorModel FromAuthorizationRole(int roleId, string roleName) + { + return new SubjectDescriptorModel() + { + Id = $"[{roleId}]", + Name = roleName, + Type = "role", + IsGroup = true + }; + } + + public static SubjectDescriptorModel FromAuthorizationRole(AuthorizationRole role) + => FromAuthorizationRole(role.Id, role.Name); } } \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Controllers/DeviceFlagController.cs b/Disco.Web/Areas/Config/Controllers/DeviceFlagController.cs index 9eef3f4b..da77d0a7 100644 --- a/Disco.Web/Areas/Config/Controllers/DeviceFlagController.cs +++ b/Disco.Web/Areas/Config/Controllers/DeviceFlagController.cs @@ -47,6 +47,9 @@ namespace Disco.Web.Areas.Config.Controllers m.ThemeColours = UIHelpers.ThemeColours; } + var (_, permission) = DeviceFlagService.GetDeviceFlag(m.DeviceFlag.Id); + m.Permission = permission; + // UI Extensions UIExtensions.ExecuteExtensions(ControllerContext, m); @@ -121,7 +124,7 @@ namespace Disco.Web.Areas.Config.Controllers var m = new ExportModel() { Options = Database.DiscoConfiguration.DeviceFlags.LastExportOptions, - DeviceFlags = DeviceFlagService.GetDeviceFlags(), + DeviceFlags = DeviceFlagService.GetDeviceFlags().Select(f => f.flag).ToList(), }; m.Fields = ExportFieldsModel.Create(m.Options, DeviceFlagExportOptions.DefaultOptions(), nameof(DeviceFlagExportOptions.CurrentOnly)); diff --git a/Disco.Web/Areas/Config/Controllers/UserFlagController.cs b/Disco.Web/Areas/Config/Controllers/UserFlagController.cs index 56123b8d..854d7368 100644 --- a/Disco.Web/Areas/Config/Controllers/UserFlagController.cs +++ b/Disco.Web/Areas/Config/Controllers/UserFlagController.cs @@ -46,6 +46,9 @@ namespace Disco.Web.Areas.Config.Controllers m.ThemeColours = UIHelpers.ThemeColours; } + var (flag, permission) = UserFlagService.GetUserFlag(m.UserFlag.Id); + m.Permission = permission; + // UI Extensions UIExtensions.ExecuteExtensions(ControllerContext, m); @@ -122,7 +125,7 @@ namespace Disco.Web.Areas.Config.Controllers var m = new ExportModel() { Options = Database.DiscoConfiguration.UserFlags.LastExportOptions, - UserFlags = UserFlagService.GetUserFlags(), + UserFlags = UserFlagService.GetUserFlags().Select(f => f.flag).ToList(), }; m.Fields = ExportFieldsModel.Create(m.Options, UserFlagExportOptions.DefaultOptions(), nameof(UserFlagExportOptions.CurrentOnly)); diff --git a/Disco.Web/Areas/Config/Models/DeviceFlag/ShowModel.cs b/Disco.Web/Areas/Config/Models/DeviceFlag/ShowModel.cs index aeadb7b4..1d49330e 100644 --- a/Disco.Web/Areas/Config/Models/DeviceFlag/ShowModel.cs +++ b/Disco.Web/Areas/Config/Models/DeviceFlag/ShowModel.cs @@ -1,4 +1,5 @@ -using Disco.Models.UI.Config.DeviceFlag; +using Disco.Models.Repository; +using Disco.Models.UI.Config.DeviceFlag; using Disco.Services.Devices.DeviceFlags; using System.Collections.Generic; @@ -16,5 +17,7 @@ namespace Disco.Web.Areas.Config.Models.DeviceFlag public IEnumerable> Icons { get; set; } public IEnumerable> ThemeColours { get; set; } + + public FlagPermission Permission { get; set; } } } diff --git a/Disco.Web/Areas/Config/Models/UserFlag/ShowModel.cs b/Disco.Web/Areas/Config/Models/UserFlag/ShowModel.cs index d2733540..f3a57812 100644 --- a/Disco.Web/Areas/Config/Models/UserFlag/ShowModel.cs +++ b/Disco.Web/Areas/Config/Models/UserFlag/ShowModel.cs @@ -1,4 +1,5 @@ -using Disco.Models.UI.Config.UserFlag; +using Disco.Models.Repository; +using Disco.Models.UI.Config.UserFlag; using Disco.Services.Users.UserFlags; using System.Collections.Generic; @@ -16,5 +17,7 @@ namespace Disco.Web.Areas.Config.Models.UserFlag public IEnumerable> Icons { get; set; } public IEnumerable> ThemeColours { get; set; } + + public FlagPermission Permission { get; set; } } } \ No newline at end of file diff --git a/Disco.Web/Areas/Config/Views/AuthorizationRole/Show.cshtml b/Disco.Web/Areas/Config/Views/AuthorizationRole/Show.cshtml index a41fa141..a7970c74 100644 --- a/Disco.Web/Areas/Config/Views/AuthorizationRole/Show.cshtml +++ b/Disco.Web/Areas/Config/Views/AuthorizationRole/Show.cshtml @@ -237,7 +237,7 @@
- @AjaxHelpers.AjaxLoader() + @AjaxHelpers.AjaxLoader()
\r\n \r\n \r\n \r\n \r\n\r\n\r\n \r\n \r\n \r\n\r\n 0 && Authorization.Has(Claims.Config.DeviceFlag.Export); var hideAdvanced = + Model.Permission.IsDefault() && Model.DeviceFlag.DevicesLinkedGroup == null && Model.DeviceFlag.DeviceUsersLinkedGroup == null && Model.DeviceFlag.OnAssignmentExpression == null && @@ -38,10 +39,11 @@ @if (canConfig) - {@Html.EditorFor(model => model.DeviceFlag.Name) - @AjaxHelpers.AjaxSave() - @AjaxHelpers.AjaxLoader() - - } - else - { - @Model.DeviceFlag.Name - } + + } + else + { + @Model.DeviceFlag.Name + } @@ -227,6 +229,340 @@ } + + + Assignment Permission
+ Override: + + + @if (!Model.Permission.IsDefault()) + { + var permission = Model.Permission; +
+ @if (permission.Inherit) + { + Inheriting from Authorization Roles + } + else + { + Authorization Roles are Ignored + } +
+ if (!permission.HasSubjects()) + { + There are no users/groups associated with this permission override + } + else + { + if (permission.IsSimple()) + { + + + + + + + + @foreach (var subjectId in permission.CanShowSubjectIds) + { + + + + } + +
Users/Groups/Roles
+ @{ + int roleId; + if (subjectId.StartsWith("[") && int.TryParse(subjectId.Trim('[', ']'), out roleId)) + { + @Disco.Services.Users.UserService.GetAuthorizationRoleName(roleId) @subjectId + } + else + { + @subjectId + } + } +
+
+

+ All users/groups/roles can view, assign, edit assignments, and remove assignments for this flag. +

+
+ } + else + { + var subjects = permission.AllSubjects(); + + + + + + + + + + + + @foreach (var subjectId in subjects.OrderBy(s => s)) + { + + + + + + + + } + +
Users/Groups/RolesViewAssignEditRemove
+ @{ + int roleId; + if (subjectId.StartsWith("[") && int.TryParse(subjectId.Trim('[', ']'), out roleId)) + { + @Disco.Services.Users.UserService.GetAuthorizationRoleName(roleId) @subjectId + } + else + { + @subjectId + } + } + + @if (permission.CanShowSubjectIds.Contains(subjectId)) + { + + } + + @if (permission.CanAssignSubjectIds.Contains(subjectId)) + { + + } + + @if (permission.CanEditSubjectIds.Contains(subjectId)) + { + + } + + @if (permission.CanRemoveSubjectIds.Contains(subjectId)) + { + + } +
+ } + + } + } + @if (canConfig) + { + var permission = Model.Permission; + + +
+ @using (Html.BeginForm(MVC.API.DeviceFlag.Permission(Model.DeviceFlag.Id))) + { + @Html.AntiForgeryToken() + +
+ +
+
+ + + + + + + + + + + + @{ + var subjects = permission.AllSubjects(); + + foreach (var subjectId in subjects.OrderBy(s => s)) + { + + + + + + + + } + } + +
User/Group/RoleViewAssignEditRemove
@subjectId + + + + + + + +
+
+
+ + +
+ } +
+ + } +
+

+ Flag actions are normally authorized globally by + @if (Authorization.Has(Claims.DiscoAdminAccount)) + { + Authorization Roles. + } + else + { + Authorization Roles. + } + Overriding individual flag permissions allows for targeted authorization. +

+
+ + On Assignment
Expression: diff --git a/Disco.Web/Areas/Config/Views/DeviceFlag/Show.generated.cs b/Disco.Web/Areas/Config/Views/DeviceFlag/Show.generated.cs index eeebef49..c22c1ba0 100644 --- a/Disco.Web/Areas/Config/Views/DeviceFlag/Show.generated.cs +++ b/Disco.Web/Areas/Config/Views/DeviceFlag/Show.generated.cs @@ -76,6 +76,7 @@ namespace Disco.Web.Areas.Config.Views.DeviceFlag var canExportAll = Model.TotalAssignmentCount > 0 && Authorization.Has(Claims.Config.DeviceFlag.Export); var hideAdvanced = + Model.Permission.IsDefault() && Model.DeviceFlag.DevicesLinkedGroup == null && Model.DeviceFlag.DeviceUsersLinkedGroup == null && Model.DeviceFlag.OnAssignmentExpression == null && @@ -90,15 +91,15 @@ WriteLiteral("\r\n(hideAdvanced ? " Config_HideAdvanced" : null + #line 26 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" +, Tuple.Create(Tuple.Create("", 1531), Tuple.Create(hideAdvanced ? " Config_HideAdvanced" : null #line default #line hidden -, 1490), false) +, 1531), false) ); WriteLiteral(" style=\"width: 550px\""); @@ -112,7 +113,7 @@ WriteLiteral(">\r\n Id:\r\n \r\n \ WriteLiteral(" "); - #line 32 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" + #line 33 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" Write(Html.DisplayFor(model => model.DeviceFlag.Id)); @@ -122,61 +123,62 @@ WriteLiteral("\r\n \r\n \r\n \r\n " Name:\r\n \r\n \r\n"); - #line 40 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" + #line 41 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" #line default #line hidden - #line 40 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" + #line 41 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" if (canConfig) { - - #line default - #line hidden - - #line 41 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - Write(Html.EditorFor(model => model.DeviceFlag.Name)); - - - #line default - #line hidden - - #line 41 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - - - - #line default - #line hidden - - #line 42 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - Write(AjaxHelpers.AjaxSave()); - - - #line default - #line hidden - - #line 42 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - - + #line default #line hidden #line 43 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - Write(AjaxHelpers.AjaxLoader()); + Write(Html.EditorFor(model => model.DeviceFlag.Name)); #line default #line hidden #line 43 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" - + + + + #line default + #line hidden + + #line 44 "..\..\Areas\Config\Views\DeviceFlag\Show.cshtml" + Write(AjaxHelpers.AjaxSave()); #line default #line hidden -WriteLiteral("