Compare commits

..

10 Commits

44 changed files with 771 additions and 228 deletions

View File

@ -1,7 +1,7 @@
<Project>
<Import Project="dependencies.props" />
<PropertyGroup>
<Version>1.2.4</Version>
<Version>1.2.5</Version>
<Authors>Tudor Stanciu</Authors>
<Company>STA</Company>
<PackageTags>NetworkResurrector</PackageTags>

View File

@ -170,4 +170,16 @@
• The "Netmash.Security.Authentication.Tuitio" nuget package has been upgraded in backend.
</Content>
</Note>
<Note>
<Version>1.2.5</Version>
<Date>2023-04-12 23:58</Date>
<Content>
Permissions and authorizations
• Permissions and authorizations at the user role level have been added to the application.
• The "Netmash.Security.Authentication.Tuitio" nuget package has been upgraded in backend.
• Added cache and authorization policies.
• Added authorization rules based on permissions.
• Display fake sensitive information for guest users.
</Content>
</Note>
</ReleaseNotes>

View File

@ -11,8 +11,10 @@
<NBBPackageVersion>6.0.30</NBBPackageVersion>
<EntityFrameworkCorePackageVersion>6.0.1</EntityFrameworkCorePackageVersion>
<NetmashExtensionsSwaggerPackageVersion>1.0.7</NetmashExtensionsSwaggerPackageVersion>
<NetmashTuitioAuthenticationPackageVersion>2.2.0</NetmashTuitioAuthenticationPackageVersion>
<NetmashTuitioAuthenticationPackageVersion>2.2.1</NetmashTuitioAuthenticationPackageVersion>
<NetmashDatabaseMigrationPackageVersion>1.2.0</NetmashDatabaseMigrationPackageVersion>
<NetmashExtensionsCachingPackageVersion>1.0.1</NetmashExtensionsCachingPackageVersion>
<CorreoPublishedLanguage>1.0.1</CorreoPublishedLanguage>
<ScrutorPackageVersion>4.2.2</ScrutorPackageVersion>
</PropertyGroup>
</Project>

View File

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using NetworkResurrector.Api.Application.Services;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Application.Services.Decorators;
namespace NetworkResurrector.Api.Application
{
@ -9,6 +10,9 @@ namespace NetworkResurrector.Api.Application
public static void AddApplicationServices(this IServiceCollection services)
{
services.AddSingleton<INotificationService, NotificationService>();
services.AddScoped<IUserContext, UserContext>();
services.Decorate<IUserContext, UserContextCache>();
services.AddScoped<IUserService, UserService>();
}
}
}

View File

@ -0,0 +1,25 @@
using NetworkResurrector.Api.Application.Helpers;
using NetworkResurrector.Api.Application.Queries;
using System;
using System.Linq;
namespace NetworkResurrector.Api.Application.Extensions
{
internal static class ModelExtensions
{
public static void Fake(this GetMachines.Model[] models)
{
var count = models.Length;
var ips = DataFaker.GenerateIPv4AddressesInSubnet(count).ToArray();
foreach (var model in models)
{
model.MachineName = DataFaker.GenerateName();
model.FullMachineName = $"{model.MachineName}.DEMO.LAB";
model.IPv4Address = ips[Array.IndexOf(models, model)];
model.MACAddress = Randomizer.Randomize(model.MACAddress);
model.Description = $"{model.FullMachineName} description";
}
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace NetworkResurrector.Api.Application.Helpers
{
internal static class DataFaker
{
private static readonly Random Random = new Random();
private static readonly string[] Names = { "Apollo", "Athena", "Cronus", "Diana", "Hermes", "Jupiter", "Mars", "Mercury", "Neptune", "Saturn", "Uranus", "Venus" };
private static readonly string[] Codes = { "Node", "Hypervisor", "Srv", "WebSrv", "SqlSrv", "Host", "Agent" };
public static string GenerateName()
{
var codeIndex = Random.Next(Codes.Length);
var nameIndex = Random.Next(Names.Length);
return $"{Names[nameIndex]}{Codes[codeIndex]}";
}
public static IEnumerable<string> GenerateIPv4AddressesInSubnet(int count)
{
Random random = new Random();
byte[] subnet = new byte[4];
random.NextBytes(subnet);
subnet[0] = (byte)((subnet[0] & 0xF0) | 0x0A); // set first octet to 10.x.x.x (private IP range)
var addresses = new List<string>();
for (int i = 0; i < count; i++)
{
byte[] ip = new byte[4];
random.NextBytes(ip);
ip[0] = subnet[0];
ip[1] = subnet[1];
addresses.Add(string.Join(".", ip));
}
return addresses;
}
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Reflection;
namespace NetworkResurrector.Api.Application.Helpers
{
internal static class Randomizer
{
private static readonly Random random = new Random();
public static string Randomize(string originalString)
{
var newString = new char[originalString.Length];
for (int i = 0; i < originalString.Length; i++)
{
if (char.IsLetter(originalString[i]))
{
if (char.IsUpper(originalString[i]))
{
newString[i] = GetRandomUpperLetter();
}
else
{
newString[i] = GetRandomLowerLetter();
}
}
else if (char.IsDigit(originalString[i]))
{
newString[i] = GetRandomDigit();
}
else
{
newString[i] = originalString[i];
}
}
var newStringValue = new string(newString);
return newStringValue;
}
public static void ReplaceStringProperties(object obj)
{
if (obj == null) return;
var objectType = obj.GetType();
var properties = objectType.GetProperties();
if (obj is string)
{
var originalString = (string)obj;
var newStringValue = Randomize(originalString);
if (newStringValue != originalString)
{
objectType.InvokeMember("value", BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty, Type.DefaultBinder, obj, new object[] { newStringValue });
}
}
else if (obj is Array array)
{
foreach (var item in array)
{
ReplaceStringProperties(item);
}
}
else
{
foreach (var property in properties)
{
if (property.PropertyType == typeof(string))
{
var originalString = (string)property.GetValue(obj);
var newString = new char[originalString.Length];
for (int i = 0; i < originalString.Length; i++)
{
if (char.IsLetter(originalString[i]))
{
if (char.IsUpper(originalString[i]))
{
newString[i] = GetRandomUpperLetter();
}
else
{
newString[i] = GetRandomLowerLetter();
}
}
else if (char.IsDigit(originalString[i]))
{
newString[i] = GetRandomDigit();
}
else
{
newString[i] = originalString[i];
}
}
var newStringValue = new string(newString);
if (newStringValue != originalString)
{
property.SetValue(obj, newStringValue);
}
}
else
{
ReplaceStringProperties(property.GetValue(obj));
}
}
}
}
private static char GetRandomUpperLetter()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return chars[random.Next(chars.Length)];
}
private static char GetRandomLowerLetter()
{
const string chars = "abcdefghijklmnopqrstuvwxyz";
return chars[random.Next(chars.Length)];
}
private static char GetRandomDigit()
{
const string chars = "0123456789";
return chars[random.Next(chars.Length)];
}
}
}

View File

@ -11,9 +11,12 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsPackageVersion)" />
<PackageReference Include="Netmash.Extensions.Caching" Version="$(NetmashExtensionsCachingPackageVersion)" />
<PackageReference Include="Netmash.Security.Authentication.Tuitio" Version="$(NetmashTuitioAuthenticationPackageVersion)" />
<PackageReference Include="NBB.Messaging.Abstractions" Version="$(NBBPackageVersion)" />
<PackageReference Include="NetworkResurrector.Agent.Wrapper" Version="1.1.0" />
<PackageReference Include="NetworkResurrector.Server.Wrapper" Version="1.1.1" />
<PackageReference Include="Scrutor" Version="$(ScrutorPackageVersion)" />
</ItemGroup>
<ItemGroup>

View File

@ -1,5 +1,7 @@
using AutoMapper;
using MediatR;
using NetworkResurrector.Api.Application.Extensions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Repositories;
using System.Threading;
using System.Threading.Tasks;
@ -24,18 +26,24 @@ namespace NetworkResurrector.Api.Application.Queries
{
private readonly INetworkRepository _repository;
private readonly IMapper _mapper;
private readonly IUserService _userService;
public QueryHandler(INetworkRepository repository, IMapper mapper)
public QueryHandler(INetworkRepository repository, IMapper mapper, IUserService userService)
{
_repository = repository;
_mapper = mapper;
_repository=repository;
_mapper=mapper;
_userService=userService;
}
public async Task<Model[]> Handle(Query request, CancellationToken cancellationToken)
{
var isGuest = await _userService.GetIsGuest();
var machines = await _repository.GetMachines();
var result = _mapper.Map<Model[]>(machines);
if (isGuest)
result.Fake();
return result;
}
}

View File

@ -0,0 +1,40 @@
using MediatR;
using Netmash.Security.Authentication.Tuitio.Abstractions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Repositories;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Queries
{
public class GetUserPermissions
{
public class Query : IRequest<Model> { }
public class Model
{
public IEnumerable<string> Permissions { get; set; }
}
public class QueryHandler : IRequestHandler<Query, Model>
{
private readonly IUserContext _userContext;
public QueryHandler(IUserContext userContext)
{
_userContext=userContext;
}
public async Task<Model> Handle(Query request, CancellationToken cancellationToken)
{
var permissions = await _userContext.GetUserPermissions();
return new Model()
{
Permissions = permissions
};
}
}
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services.Abstractions
{
public interface IUserContext
{
Task<IEnumerable<string>> GetUserPermissions();
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services.Abstractions
{
public interface IUserService
{
Task<bool> GetIsGuest();
}
}

View File

@ -0,0 +1,35 @@
using Netmash.Extensions.Caching.Services;
using Netmash.Security.Authentication.Tuitio.Abstractions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services.Decorators
{
internal class UserContextCache : IUserContext
{
private readonly IUserContextAccessor _userContextAccessor;
private readonly IUserContext _inner;
private readonly ICacheService _cache;
public UserContextCache(IUserContextAccessor userContextAccessor, IUserContext inner, ICacheService cache)
{
_userContextAccessor=userContextAccessor;
_inner=inner;
_cache=cache;
}
public async Task<IEnumerable<string>> GetUserPermissions()
{
var key = $"UserPermissions_{_userContextAccessor.UserId}";
var result = await _cache.GetAsync<IEnumerable<string>>(key);
if (result == null)
{
result = await _inner.GetUserPermissions();
await _cache.SetAsync(key, result);
}
return result;
}
}
}

View File

@ -0,0 +1,28 @@
using Netmash.Security.Authentication.Tuitio.Abstractions;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Repositories;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services
{
internal class UserContext : IUserContext
{
private readonly IUserContextAccessor _userContext;
private readonly ISecurityRepository _securityRepository;
public UserContext(IUserContextAccessor userContext, ISecurityRepository securityRepository)
{
_userContext=userContext;
_securityRepository=securityRepository;
}
public async Task<IEnumerable<string>> GetUserPermissions()
{
var roles = _userContext.UserRoles.Select(r => r.id);
var permissions = await _securityRepository.GetUserPermissionCodes(roles);
return permissions;
}
}
}

View File

@ -0,0 +1,24 @@
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Domain.Constants;
using System.Linq;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Application.Services
{
internal class UserService : IUserService
{
private readonly IUserContext _userContext;
public UserService(IUserContext userContext)
{
_userContext=userContext;
}
public async Task<bool> GetIsGuest()
{
var permissions = await _userContext.GetUserPermissions();
var isGuest = permissions == null || permissions.Contains(PermissionCodes.GUEST_ACCESS);
return isGuest;
}
}
}

View File

@ -1,7 +1,9 @@
using Microsoft.EntityFrameworkCore;
using NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration;
using NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration.Power;
using NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration.Security;
using NetworkResurrector.Api.Domain.Entities;
using NetworkResurrector.Api.Domain.Entities.Security;
namespace NetworkResurrector.Api.Domain.Data.DbContexts
{
@ -9,6 +11,8 @@ namespace NetworkResurrector.Api.Domain.Data.DbContexts
{
public DbSet<Machine> Machines { get; set; }
public DbSet<Entities.Power.PowerActionConfiguration> PowerActionConfigurations { get; set; }
public DbSet<UserRoleAuthorization> UserRoleAuthorizations { get; set; }
public DbSet<PermissionHierarchy> PermissionHierarchies { get; set; }
public NetworkDbContext(DbContextOptions<NetworkDbContext> options)
: base(options)
@ -26,6 +30,9 @@ namespace NetworkResurrector.Api.Domain.Data.DbContexts
modelBuilder.ApplyConfiguration(new PowerActionPerformerConfiguration());
modelBuilder.ApplyConfiguration(new PowerActionConfigurationEf());
modelBuilder.ApplyConfiguration(new MachineAgentConfiguration());
modelBuilder.ApplyConfiguration(new PermissionConfiguration());
modelBuilder.ApplyConfiguration(new PermissionHierarchyConfiguration());
modelBuilder.ApplyConfiguration(new UserRoleAuthorizationConfiguration());
}
}
}

View File

@ -12,6 +12,7 @@ namespace NetworkResurrector.Api.Domain.Data
public static void AddDataAccess(this IServiceCollection services)
{
services.AddScoped<INetworkRepository, NetworkRepository>();
services.AddScoped<ISecurityRepository, SecurityRepository>();
services
.AddDbContextPool<NetworkDbContext>(

View File

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using NetworkResurrector.Api.Domain.Entities.Security;
namespace NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration.Security
{
class PermissionConfiguration : IEntityTypeConfiguration<Permission>
{
public void Configure(EntityTypeBuilder<Permission> builder)
{
builder.ToTable("Permission").HasKey(key => key.PermissionId);
builder.Property(z => z.PermissionId).ValueGeneratedOnAdd();
builder.HasMany(z => z.Children).WithOne().HasForeignKey(z => z.ParentPermissionId);
}
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using NetworkResurrector.Api.Domain.Entities.Security;
namespace NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration.Security
{
class PermissionHierarchyConfiguration : IEntityTypeConfiguration<PermissionHierarchy>
{
public void Configure(EntityTypeBuilder<PermissionHierarchy> builder)
{
builder.ToTable("PermissionHierarchy").HasKey(p => new { p.ParentPermissionId, p.ChildPermissionId });
builder.HasOne(z => z.Child).WithMany().HasForeignKey(p => p.ChildPermissionId);
}
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using NetworkResurrector.Api.Domain.Entities.Security;
namespace NetworkResurrector.Api.Domain.Data.EntityTypeConfiguration.Security
{
class UserRoleAuthorizationConfiguration : IEntityTypeConfiguration<UserRoleAuthorization>
{
public void Configure(EntityTypeBuilder<UserRoleAuthorization> builder)
{
builder.ToTable("UserRoleAuthorization").HasKey(z => new { z.UserRoleId, z.PermissionId });
builder.HasOne(z => z.Permission).WithMany().HasForeignKey(z => z.PermissionId);
}
}
}

View File

@ -22,6 +22,12 @@
<None Update="Scripts\1.0.3\02.Insert wake and ping configs for the rest of machines.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Scripts\1.2.5\01.Permission tables.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Scripts\1.2.5\02.UserRoleAuthorization table.sql">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,80 @@
using Microsoft.EntityFrameworkCore;
using NetworkResurrector.Api.Domain.Data.DbContexts;
using NetworkResurrector.Api.Domain.Entities.Security;
using NetworkResurrector.Api.Domain.Repositories;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Domain.Data.Repositories
{
public class SecurityRepository : ISecurityRepository
{
private readonly NetworkDbContext _dbContext;
public SecurityRepository(NetworkDbContext dbContext)
{
_dbContext=dbContext;
}
private async Task BuildChildrenHierarchy(Permission permission)
{
var children = await _dbContext.PermissionHierarchies
.Include(z => z.Child)
.Where(z => z.ParentPermissionId == permission.PermissionId)
.AsNoTracking()
.ToArrayAsync();
if (children == null || !children.Any())
return;
permission.Children = children;
foreach (var element in permission.Children)
{
await BuildChildrenHierarchy(element.Child);
}
}
private IEnumerable<string> GetPermissionCodes(IEnumerable<Permission> permissions)
{
var result = new List<string>();
foreach (var permission in permissions)
{
result.Add(permission.PermissionCode);
if (permission.Children != null && permission.Children.Count > 0)
{
var children = permission.Children.Select(ch => ch.Child);
var childrenCodes = GetPermissionCodes(children);
result.AddRange(childrenCodes);
}
}
return result.Distinct();
}
private async Task<IEnumerable<Permission>> GetUserPermissions(IEnumerable<int> roles)
{
var authorizations = await _dbContext.UserRoleAuthorizations
.Include(z => z.Permission)
.AsNoTracking()
.Where(z => roles.Contains(z.UserRoleId) && z.Active)
.ToArrayAsync();
var permissions = authorizations.Select(z => z.Permission);
foreach (var permission in permissions)
{
await BuildChildrenHierarchy(permission);
}
return permissions;
}
public async Task<IEnumerable<string>> GetUserPermissionCodes(IEnumerable<int> roles)
{
var permissions = await GetUserPermissions(roles);
var codes = GetPermissionCodes(permissions);
return codes;
}
}
}

View File

@ -0,0 +1,50 @@
if not exists (select top 1 1 from sys.objects where name = 'Permission' and type = 'U')
begin
create table Permission
(
PermissionId int identity(1, 1) constraint PK_Permission primary key,
PermissionCode varchar(50) not null,
PermissionName varchar(100) not null,
PermissionDescription varchar(300) not null
)
end
if not exists (select top 1 1 from sys.objects where name = 'PermissionHierarchy' and type = 'U')
begin
create table PermissionHierarchy
(
ParentPermissionId int not null constraint FK_PermissionHierarchy_Permission_Parent foreign key references Permission(PermissionId),
ChildPermissionId int not null constraint FK_PermissionHierarchy_Permission_Child foreign key references Permission(PermissionId)
constraint PK_PermissionHierarchy primary key (ParentPermissionId, ChildPermissionId)
)
end
if not exists (select top 1 1 from Permission)
begin
insert into Permission(PermissionCode, PermissionName, PermissionDescription)
values ('VIEW_DASHBOARD', 'View dashboard', 'The user with this permission can view the dashboard.'),
('MANAGE_USERS', 'Manage users', 'The user with this permission can assign permissions to users.'),
('MANAGE_SETTINGS', 'Manage settings', 'The user with this permission can manage the application settings.'),
('VIEW_MACHINES', 'View machines', 'The user with this permission can view machines. He cannot start or stop a machine.'),
('MANAGE_MACHINES', 'Manage machines', 'The user with this permission can add, edit or delete machines.'),
('OPERATE_MACHINES', 'Operate machines', 'The user with this permission can operate machines. He can start or stop machines.'),
('GUEST_ACCESS', 'Guest access', 'The user with this permission can view the application in a read-only mode and with all data anonymized.')
end
if not exists (select top 1 1 from PermissionHierarchy)
begin
declare @view_machines_permission_id int,
@manage_machines_permission_id int,
@operate_machines_permission_id int,
@guest_access_permission_id int
select @view_machines_permission_id = PermissionId from Permission where PermissionCode = 'VIEW_MACHINES'
select @manage_machines_permission_id = PermissionId from Permission where PermissionCode = 'MANAGE_MACHINES'
select @operate_machines_permission_id = PermissionId from Permission where PermissionCode = 'OPERATE_MACHINES'
select @guest_access_permission_id = PermissionId from Permission where PermissionCode = 'GUEST_ACCESS'
insert into PermissionHierarchy (ParentPermissionId, ChildPermissionId)
values (@manage_machines_permission_id, @view_machines_permission_id),
(@operate_machines_permission_id, @view_machines_permission_id),
(@guest_access_permission_id, @view_machines_permission_id)
end

View File

@ -0,0 +1,10 @@
if not exists (select top 1 1 from sys.objects where name = 'UserRoleAuthorization' and type = 'U')
begin
create table UserRoleAuthorization
(
UserRoleId int not null,
PermissionId int not null constraint FK_UserRoleAuthorization_Permission foreign key references Permission(PermissionId),
Active bit not null,
constraint PK_UserRoleAuthorization primary key (PermissionId, UserRoleId)
)
end

View File

@ -1,10 +0,0 @@
namespace NetworkResurrector.Api.Domain.Abstractions
{
public interface IUserService
{
bool UserIsLoggedIn { get; }
string GetUserId();
string GetUserName();
bool UserIsGuest();
}
}

View File

@ -0,0 +1,14 @@
namespace NetworkResurrector.Api.Domain.Constants
{
public struct PermissionCodes
{
public const string
VIEW_DASHBOARD = "VIEW_DASHBOARD",
MANAGE_USERS = "MANAGE_USERS",
MANAGE_SETTINGS = "MANAGE_SETTINGS",
VIEW_MACHINES = "VIEW_MACHINES",
MANAGE_MACHINES = "MANAGE_MACHINES",
OPERATE_MACHINES = "OPERATE_MACHINES",
GUEST_ACCESS = "GUEST_ACCESS";
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace NetworkResurrector.Api.Domain.Entities.Security
{
public class Permission
{
public int PermissionId { get; set; }
public string PermissionCode { get; set; }
public string PermissionName { get; set; }
public string PermissionDescription { get; set; }
public ICollection<PermissionHierarchy> Children { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace NetworkResurrector.Api.Domain.Entities.Security
{
public class PermissionHierarchy
{
public int ParentPermissionId { get; set; }
public int ChildPermissionId { get; set; }
public Permission Child { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace NetworkResurrector.Api.Domain.Entities.Security
{
public class UserRoleAuthorization
{
public int UserRoleId { get; set; }
public int PermissionId { get; set; }
public bool Active { get; set; }
public Permission Permission { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Domain.Repositories
{
public interface ISecurityRepository
{
Task<IEnumerable<string>> GetUserPermissionCodes(IEnumerable<int> roles);
}
}

View File

@ -0,0 +1,9 @@
namespace NetworkResurrector.Api.Authorization.Constants
{
public struct Policies
{
public const string
OperateMachines = "OPERATE_MACHINES",
ViewMachines = "VIEW_MACHINES";
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using NetworkResurrector.Api.Authorization.Constants;
using NetworkResurrector.Api.Authorization.Handlers;
using NetworkResurrector.Api.Authorization.Requirements;
namespace NetworkResurrector.Api.Authorization
{
public static class DependencyInjectionExtensions
{
public static void AddLocalAuthorization(this IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy(Policies.OperateMachines, policy =>
{
policy.Requirements.Add(new OperateMachinesRequirement());
});
options.AddPolicy(Policies.ViewMachines, policy =>
{
policy.Requirements.Add(new ViewMachinesRequirement());
});
});
services.AddScoped<IAuthorizationHandler, PermissionsBasedAuthorizationHandler>();
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Authorization;
using NetworkResurrector.Api.Application.Services.Abstractions;
using NetworkResurrector.Api.Authorization.Requirements;
using NetworkResurrector.Api.Extensions;
using System.Linq;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Authorization.Handlers
{
public class PermissionsBasedAuthorizationHandler : AuthorizationHandler<IPermissionsBasedAuthorizationRequirement>
{
private readonly IUserContext _userContext;
public PermissionsBasedAuthorizationHandler(IUserContext userContext)
{
_userContext=userContext;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IPermissionsBasedAuthorizationRequirement requirement)
{
var permissions = await _userContext.GetUserPermissions();
var condition1 = requirement.AllRequired.IsNullOrEmpty() || requirement.AllRequired.All(permission => permissions.Contains(permission));
var condition2 = requirement.OneOf.IsNullOrEmpty() || requirement.OneOf.Any(permission => permissions.Contains(permission));
if (condition1 && condition2)
{
context.Succeed(requirement);
}
}
}
}

View File

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
namespace NetworkResurrector.Api.Authorization.Requirements
{
public interface IPermissionsBasedAuthorizationRequirement : IAuthorizationRequirement
{
string[] AllRequired { get; }
string[] OneOf { get; }
}
}

View File

@ -0,0 +1,12 @@
using NetworkResurrector.Api.Domain.Constants;
using System;
namespace NetworkResurrector.Api.Authorization.Requirements
{
public class OperateMachinesRequirement : IPermissionsBasedAuthorizationRequirement
{
public string[] AllRequired => new string[] { PermissionCodes.OPERATE_MACHINES };
public string[] OneOf => Array.Empty<string>();
}
}

View File

@ -0,0 +1,11 @@
using NetworkResurrector.Api.Domain.Constants;
using System;
namespace NetworkResurrector.Api.Authorization.Requirements
{
public class ViewMachinesRequirement : IPermissionsBasedAuthorizationRequirement
{
public string[] AllRequired => Array.Empty<string>();
public string[] OneOf => new string[] { PermissionCodes.VIEW_MACHINES };
}
}

View File

@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetworkResurrector.Api.Application.Queries;
using NetworkResurrector.Api.Authorization.Constants;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Controllers
@ -19,6 +20,7 @@ namespace NetworkResurrector.Api.Controllers
}
[HttpGet("machines")]
[Authorize(Policy = Policies.ViewMachines)]
public async Task<IActionResult> GetMachines([FromRoute] GetMachines.Query query)
{
var result = await _mediator.Send(query);

View File

@ -1,12 +1,13 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetworkResurrector.Api.Authorization.Constants;
using NetworkResurrector.Api.PublishedLanguage.Commands;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Controllers
{
[Authorize]
[Authorize(Policy = Policies.OperateMachines)]
[ApiController]
[Route("resurrector")]
public class ResurrectorController : ControllerBase

View File

@ -0,0 +1,28 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetworkResurrector.Api.Application.Queries;
using System.Threading.Tasks;
namespace NetworkResurrector.Api.Controllers
{
[Authorize]
[ApiController]
[Route("security")]
public class SecurityController : ControllerBase
{
private readonly IMediator _mediator;
public SecurityController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("permissions")]
public async Task<IActionResult> GetUserPermissions([FromRoute] GetUserPermissions.Query query)
{
var result = await _mediator.Send(query);
return Ok(result);
}
}
}

View File

@ -4,5 +4,8 @@
{
public static string Nullify(this string value)
=> string.IsNullOrWhiteSpace(value) ? null : value;
public static bool IsNullOrEmpty(this string[] array)
=> array == null || array.Length == 0;
}
}

View File

@ -3,6 +3,7 @@ using MediatR.Pipeline;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Netmash.Extensions.Caching;
using Netmash.Extensions.Swagger;
using Netmash.Extensions.Swagger.Constants;
using Netmash.Infrastructure.DatabaseMigration;
@ -10,9 +11,8 @@ using Netmash.Infrastructure.DatabaseMigration.Constants;
using Netmash.Security.Authentication.Tuitio;
using NetworkResurrector.Agent.Wrapper;
using NetworkResurrector.Api.Application;
using NetworkResurrector.Api.Domain.Abstractions;
using NetworkResurrector.Api.Authorization;
using NetworkResurrector.Api.Domain.Data;
using NetworkResurrector.Api.Services;
using NetworkResurrector.Server.Wrapper;
using Newtonsoft.Json;
@ -28,8 +28,11 @@ namespace NetworkResurrector.Api.Extensions
// Add basic authentication
services.AddTuitioAuthentication(configuration.GetSection("Tuitio")["BaseAddress"]);
services.AddHttpContextAccessor();
services.AddScoped<IUserService, UserService>();
// Add authorization
services.AddLocalAuthorization();
// Add cache service
services.AddCacheService();
// MediatR
services.AddMediatR(typeof(Application.Queries.GetMachines).Assembly);

View File

@ -20,7 +20,6 @@
<PackageReference Include="NBB.Messaging.Nats" Version="$(NBBPackageVersion)" />
<PackageReference Include="Netmash.Extensions.Swagger" Version="$(NetmashExtensionsSwaggerPackageVersion)" />
<PackageReference Include="Netmash.Infrastructure.DatabaseMigration" Version="$(NetmashDatabaseMigrationPackageVersion)" />
<PackageReference Include="Netmash.Security.Authentication.Tuitio" Version="$(NetmashTuitioAuthenticationPackageVersion)" />
<PackageReference Include="Serilog.AspNetCore" Version="$(SerilogPackageVersion)" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="$(SerilogSinksMSSqlServerPackageVersion)" />
<PackageReference Include="Serilog.Sinks.Seq" Version="$(SerilogSinksSeqPackageVersion)" />

View File

@ -1,161 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ReleaseNotes>
<Note>
<Version>1.0.0</Version>
<Date>2020-11-28 18:15</Date>
<Content>
System initialization
Simple .net core 3.1 console application that serve as an REST API through witch a user can execute wake or shutdown commands on machines from his network.
Has been added Wake on LAN support and "wake" route in the API controller.
</Content>
</Note>
<Note>
<Version>1.0.1</Version>
<Date>2020-11-29 22:14</Date>
<Content>
New functionalities added
Has been added "ping" and "shutdown" support. Routes with same names have also been added to the controller.
</Content>
</Note>
<Note>
<Version>1.0.2</Version>
<Date>2020-12-20 23:00</Date>
<Content>
Changes in the system structure
Replaced Swagger and MediatR implementations with inhouse nuget packages.
Integration with Tuitio.
</Content>
</Note>
<Note>
<Version>1.0.3</Version>
<Date>2022-01-03 08:52</Date>
<Content>
Added NetworkResurrector.Agent service
Upgrade all services to net5.0
NetworkResurrector.Agent is a service that will be installed on a host machine with the scope to execute operations like shutdown, restart, etc directly.
The system will be able to execute these operations in multiple ways. This is just the first one and all of them are handled by the NetworkResurrector.Api. For example, if the user wants to shutdown a clean Windows machine, he can use the agent and configure the API to call it, but in case of a proxmox machine, the API can be configured to execute a http request directly to the OS with the shutdown action (without the need for the agent).
</Content>
</Note>
<Note>
<Version>1.0.4</Version>
<Date>2022-01-14 01:22</Date>
<Content>
NetworkResurrector.Agent improvements
• Multiple operations were implemented in the agent: Shutdown, Restart, Sleep, LockLogout and Cancel. The "Cancel" action can cancel one of the previously operations programmed with a delay.
• Added NetworkResurrector.Agent.Wrapper nuget package. It provides an easy and efficient method for a developer to connect another system written in .NET to this agent.
</Content>
</Note>
<Note>
<Version>1.0.5</Version>
<Date>2022-01-18 21:43</Date>
<Content>
NetworkResurrector.Server.Wrapper nuget package
• Added NetworkResurrector.Server.Wrapper nuget package. It provides an easy and efficient method for a developer to connect another system written in .NET to NetworkResurrector.Server.
• Added logic where the http headers from the caller's request are automatically passed to the request sent to this server.
</Content>
</Note>
<Note>
<Version>1.0.6</Version>
<Date>2022-06-19 08:18</Date>
<Content>
Implemented Netmash.Infrastructure.DatabaseMigration
• Through this nuget package, support was added for the automatic running of new sql scripts at system startup.
• The current sql scripts have been corrected or updated to meet the migrator rules.
</Content>
</Note>
<Note>
<Version>1.0.7</Version>
<Date>2022-11-30 23:21</Date>
<Content>
Code cleanup and refactoring
• Preparing to make the project open source on my Gitea instance.
• Removed secrets from source code and from git history
• Exposing two new methods "/ping" and "/version" in a new controller "/system".
</Content>
</Note>
<Note>
<Version>1.1.0</Version>
<Date>2023-01-29 00:31</Date>
<Content>
Massive improvements
• .NET 6 upgrade
• Nuget packages upgrade
• Added Seq logging
• Added messaging and published notifications from command handlers
• Refactoring and code cleanup
• Added README.md file
</Content>
</Note>
<Note>
<Version>1.1.1</Version>
<Date>2023-02-02 19:03</Date>
<Content>
Retouches after the last upgrade
• Nuget packages upgrade
• Small fixes
</Content>
</Note>
<Note>
<Version>1.1.2</Version>
<Date>2023-02-04 11:14</Date>
<Content>
Tuitio latest updates
• Tuitio nuget packages upgrade
• Tuitio configuration changes
• Many frontend developments
• Tuitio client NPM package integration: @flare/tuitio-react-client
• Added license file
• Login with enter key
</Content>
</Note>
<Note>
<Version>1.1.3</Version>
<Date>2023-03-18 02:49</Date>
<Content>
Tuitio latest changes
• Account logout method and the latest changes published by Tuitio were implemented
• Netmash.Security.Authentication.Tuitio nuget package upgrade
</Content>
</Note>
<Note>
<Version>1.2.0</Version>
<Date>2023-03-19 14:56</Date>
<Content>
Massive frontend developments
• Complete UI refactoring
• A complete menu has been added to the application.
• The theme and the switch between dark and light mode have been implemented.
• Axios upgrade.
• The ugly and useless stepper has been removed from the machines page.
</Content>
</Note>
<Note>
<Version>1.2.1</Version>
<Date>2023-03-19 20:11</Date>
<Content>
Frontend developments
• Added sensitive info toggle.
• Apply mask on sensitive information.
• Mask machine logs.
</Content>
</Note>
<Note>
<Version>1.2.2</Version>
<Date>2023-03-19 23:14</Date>
<Content>
Added user profile page
• The data on the page is extracted from the Tuitio token.
</Content>
</Note>
<Note>
<Version>1.2.3</Version>
<Date>2023-03-25 02:26</Date>
<Content>
Important developments in frontend
• Machines view modes
• New menu entries: About, Administration and System
• About page. It contains information about the system and the release notes.
• New methods have been added to the API for reading the system version and release notes.
</Content>
</Note>
</ReleaseNotes>

View File

@ -1,46 +0,0 @@
using Microsoft.AspNetCore.Http;
using NetworkResurrector.Api.Domain.Abstractions;
using System;
using System.Linq;
using System.Security.Claims;
namespace NetworkResurrector.Api.Services
{
public class UserService : IUserService
{
private readonly IHttpContextAccessor _httpAccessor;
public UserService(IHttpContextAccessor httpAccessor)
{
_httpAccessor = httpAccessor;
}
public bool UserIsLoggedIn => _httpAccessor.HttpContext.User != null;
public string GetUserId()
{
var userId = _httpAccessor.HttpContext.User?.Claims.FirstOrDefault(z => z.Type == ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
throw new Exception("User id could not be retrieved from claims.");
return userId;
}
public string GetUserName()
{
var userName = _httpAccessor.HttpContext.User?.Claims.FirstOrDefault(z => z.Type == ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(userName))
throw new Exception("User name could not be retrieved from claims.");
return userName;
}
public bool UserIsGuest()
{
var userIsGuest = _httpAccessor.HttpContext.User?.Claims.FirstOrDefault(z => z.Type == Netmash.Security.Authentication.Tuitio.Constants.ClaimTypes.IsGuestUser)?.Value;
return !string.IsNullOrEmpty(userIsGuest) && bool.TrueString == userIsGuest;
}
}
}