Implemented a basic permission system base
This commit is contained in:
@@ -46,6 +46,7 @@ public class DataContext : DbContext
|
|||||||
public DbSet<WebSpace> WebSpaces { get; set; }
|
public DbSet<WebSpace> WebSpaces { get; set; }
|
||||||
public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
|
public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
|
||||||
public DbSet<IpBan> IpBans { get; set; }
|
public DbSet<IpBan> IpBans { get; set; }
|
||||||
|
public DbSet<PermissionGroup> PermissionGroups { get; set; }
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
8
Moonlight/App/Database/Entities/PermissionGroup.cs
Normal file
8
Moonlight/App/Database/Entities/PermissionGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
|
public class PermissionGroup
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public byte[] Permissions { get; set; } = Array.Empty<byte>();
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Moonlight.App.Models.Misc;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Moonlight.App.Models.Misc;
|
||||||
|
|
||||||
namespace Moonlight.App.Database.Entities;
|
namespace Moonlight.App.Database.Entities;
|
||||||
|
|
||||||
@@ -39,6 +40,8 @@ public class User
|
|||||||
public bool TotpEnabled { get; set; } = false;
|
public bool TotpEnabled { get; set; } = false;
|
||||||
public string TotpSecret { get; set; } = "";
|
public string TotpSecret { get; set; } = "";
|
||||||
public DateTime TokenValidTime { get; set; } = DateTime.UtcNow;
|
public DateTime TokenValidTime { get; set; } = DateTime.UtcNow;
|
||||||
|
public byte[] Permissions { get; set; } = Array.Empty<byte>();
|
||||||
|
public PermissionGroup? PermissionGroup { get; set; }
|
||||||
|
|
||||||
// Discord
|
// Discord
|
||||||
public ulong DiscordId { get; set; }
|
public ulong DiscordId { get; set; }
|
||||||
|
|||||||
1109
Moonlight/App/Database/Migrations/20230715095531_AddPermissions.Designer.cs
generated
Normal file
1109
Moonlight/App/Database/Migrations/20230715095531_AddPermissions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPermissions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<byte[]>(
|
||||||
|
name: "Permissions",
|
||||||
|
table: "Users",
|
||||||
|
type: "longblob",
|
||||||
|
nullable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Permissions",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1139
Moonlight/App/Database/Migrations/20230715214550_AddPermissionGroup.Designer.cs
generated
Normal file
1139
Moonlight/App/Database/Migrations/20230715214550_AddPermissionGroup.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Moonlight.App.Database.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPermissionGroup : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "PermissionGroupId",
|
||||||
|
table: "Users",
|
||||||
|
type: "int",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PermissionGroups",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "int", nullable: false)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||||
|
Name = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Permissions = table.Column<byte[]>(type: "longblob", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PermissionGroups", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_PermissionGroupId",
|
||||||
|
table: "Users",
|
||||||
|
column: "PermissionGroupId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Users_PermissionGroups_PermissionGroupId",
|
||||||
|
table: "Users",
|
||||||
|
column: "PermissionGroupId",
|
||||||
|
principalTable: "PermissionGroups",
|
||||||
|
principalColumn: "Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Users_PermissionGroups_PermissionGroupId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PermissionGroups");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Users_PermissionGroupId",
|
||||||
|
table: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PermissionGroupId",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -475,6 +475,25 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
b.ToTable("NotificationClients");
|
b.ToTable("NotificationClients");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Moonlight.App.Database.Entities.PermissionGroup", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longblob");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PermissionGroups");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.Revoke", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -798,6 +817,13 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int?>("PermissionGroupId")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Permissions")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longblob");
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
b.Property<int>("Rating")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -845,6 +871,8 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
|
|
||||||
b.HasIndex("CurrentSubscriptionId");
|
b.HasIndex("CurrentSubscriptionId");
|
||||||
|
|
||||||
|
b.HasIndex("PermissionGroupId");
|
||||||
|
|
||||||
b.ToTable("Users");
|
b.ToTable("Users");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1049,7 +1077,13 @@ namespace Moonlight.App.Database.Migrations
|
|||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("CurrentSubscriptionId");
|
.HasForeignKey("CurrentSubscriptionId");
|
||||||
|
|
||||||
|
b.HasOne("Moonlight.App.Database.Entities.PermissionGroup", "PermissionGroup")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PermissionGroupId");
|
||||||
|
|
||||||
b.Navigation("CurrentSubscription");
|
b.Navigation("CurrentSubscription");
|
||||||
|
|
||||||
|
b.Navigation("PermissionGroup");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>
|
||||||
|
|||||||
88
Moonlight/App/Helpers/BitHelper.cs
Normal file
88
Moonlight/App/Helpers/BitHelper.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class BitHelper
|
||||||
|
{
|
||||||
|
public static bool ReadBit(byte[] byteArray, int bitIndex)
|
||||||
|
{
|
||||||
|
if (bitIndex < 0)
|
||||||
|
throw new ArgumentOutOfRangeException("bitIndex");
|
||||||
|
|
||||||
|
int byteIndex = bitIndex / 8;
|
||||||
|
if (byteIndex >= byteArray.Length)
|
||||||
|
throw new ArgumentOutOfRangeException("bitIndex");
|
||||||
|
|
||||||
|
int bitNumber = bitIndex % 8;
|
||||||
|
byte mask = (byte)(1 << bitNumber);
|
||||||
|
|
||||||
|
return (byteArray[byteIndex] & mask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] WriteBit(byte[] byteArray, int bitIndex, bool value)
|
||||||
|
{
|
||||||
|
if (bitIndex < 0)
|
||||||
|
throw new ArgumentOutOfRangeException("bitIndex");
|
||||||
|
|
||||||
|
int byteIndex = bitIndex / 8;
|
||||||
|
byte[] resultArray;
|
||||||
|
|
||||||
|
if (byteIndex >= byteArray.Length)
|
||||||
|
{
|
||||||
|
// Create a new array with increased size and copy elements from old array
|
||||||
|
resultArray = new byte[byteIndex + 1];
|
||||||
|
Array.Copy(byteArray, resultArray, byteArray.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Create a new array and copy elements from old array
|
||||||
|
resultArray = new byte[byteArray.Length];
|
||||||
|
Array.Copy(byteArray, resultArray, byteArray.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int bitNumber = bitIndex % 8;
|
||||||
|
byte mask = (byte)(1 << bitNumber);
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
resultArray[byteIndex] |= mask; // Set the bit to 1
|
||||||
|
else
|
||||||
|
resultArray[byteIndex] &= (byte)~mask; // Set the bit to 0
|
||||||
|
|
||||||
|
return resultArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] OverwriteByteArrays(byte[] targetArray, byte[] overwriteArray)
|
||||||
|
{
|
||||||
|
int targetLength = targetArray.Length;
|
||||||
|
int overwriteLength = overwriteArray.Length;
|
||||||
|
|
||||||
|
int maxLength = Math.Max(targetLength, overwriteLength);
|
||||||
|
|
||||||
|
byte[] resultArray = new byte[maxLength];
|
||||||
|
|
||||||
|
for (int i = 0; i < maxLength; i++)
|
||||||
|
{
|
||||||
|
byte targetByte = i < targetLength ? targetArray[i] : (byte)0;
|
||||||
|
byte overwriteByte = i < overwriteLength ? overwriteArray[i] : (byte)0;
|
||||||
|
|
||||||
|
for (int j = 0; j < 8; j++)
|
||||||
|
{
|
||||||
|
bool overwriteBit = (overwriteByte & (1 << j)) != 0;
|
||||||
|
if (i < targetLength)
|
||||||
|
{
|
||||||
|
bool targetBit = (targetByte & (1 << j)) != 0;
|
||||||
|
if (overwriteBit)
|
||||||
|
{
|
||||||
|
targetByte = targetBit ? (byte)(targetByte | (1 << j)) : (byte)(targetByte & ~(1 << j));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (overwriteBit)
|
||||||
|
{
|
||||||
|
targetByte |= (byte)(1 << j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArray[i] = targetByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Moonlight/App/Perms/Permission.cs
Normal file
10
Moonlight/App/Perms/Permission.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Moonlight.App.Perms;
|
||||||
|
|
||||||
|
public class Permission
|
||||||
|
{
|
||||||
|
public int Index { get; set; } = 0;
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
|
||||||
|
public static implicit operator int(Permission permission) => permission.Index;
|
||||||
|
}
|
||||||
11
Moonlight/App/Perms/PermissionRequired.cs
Normal file
11
Moonlight/App/Perms/PermissionRequired.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Moonlight.App.Perms;
|
||||||
|
|
||||||
|
public class PermissionRequired : Attribute
|
||||||
|
{
|
||||||
|
public string Name { get; private set; }
|
||||||
|
|
||||||
|
public PermissionRequired(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Moonlight/App/Perms/PermissionStorage.cs
Normal file
40
Moonlight/App/Perms/PermissionStorage.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Perms;
|
||||||
|
|
||||||
|
public class PermissionStorage
|
||||||
|
{
|
||||||
|
public byte[] Data;
|
||||||
|
public bool IsReadyOnly;
|
||||||
|
|
||||||
|
public PermissionStorage(byte[] data, bool isReadyOnly = false)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
IsReadyOnly = isReadyOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool this[Permission permission]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return BitHelper.ReadBit(Data, permission.Index);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Verbose("Error reading permissions. (Can be intentional)");
|
||||||
|
Logger.Verbose(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (IsReadyOnly)
|
||||||
|
throw new ReadOnlyException();
|
||||||
|
|
||||||
|
Data = BitHelper.WriteBit(Data, permission.Index, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Moonlight/App/Perms/Permissions.cs
Normal file
39
Moonlight/App/Perms/Permissions.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
namespace Moonlight.App.Perms;
|
||||||
|
|
||||||
|
public static class Permissions
|
||||||
|
{
|
||||||
|
public static Permission AdminDashboard = new()
|
||||||
|
{
|
||||||
|
Index = 0,
|
||||||
|
Name = "Admin dashboard",
|
||||||
|
Description = "See basic information about growth and status of the moonlight instance"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Permission? FromString(string name)
|
||||||
|
{
|
||||||
|
var type = typeof(Permissions);
|
||||||
|
|
||||||
|
var field = type
|
||||||
|
.GetFields()
|
||||||
|
.FirstOrDefault(x => x.FieldType == typeof(Permission) && x.Name == name);
|
||||||
|
|
||||||
|
if (field != null)
|
||||||
|
{
|
||||||
|
var value = field.GetValue(null);
|
||||||
|
return value as Permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Permission[] GetAllPermissions()
|
||||||
|
{
|
||||||
|
var type = typeof(Permissions);
|
||||||
|
|
||||||
|
return type
|
||||||
|
.GetFields()
|
||||||
|
.Where(x => x.FieldType == typeof(Permission))
|
||||||
|
.Select(x => (x.GetValue(null) as Permission)!)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
using JWT.Algorithms;
|
using JWT.Algorithms;
|
||||||
using JWT.Builder;
|
using JWT.Builder;
|
||||||
using JWT.Exceptions;
|
using JWT.Exceptions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
using Moonlight.App.Models.Misc;
|
using Moonlight.App.Models.Misc;
|
||||||
|
using Moonlight.App.Perms;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using UAParser;
|
using UAParser;
|
||||||
|
|
||||||
@@ -12,16 +14,20 @@ namespace Moonlight.App.Services.Sessions;
|
|||||||
|
|
||||||
public class IdentityService
|
public class IdentityService
|
||||||
{
|
{
|
||||||
private readonly UserRepository UserRepository;
|
private readonly Repository<User> UserRepository;
|
||||||
private readonly CookieService CookieService;
|
private readonly CookieService CookieService;
|
||||||
private readonly IHttpContextAccessor HttpContextAccessor;
|
private readonly IHttpContextAccessor HttpContextAccessor;
|
||||||
private readonly string Secret;
|
private readonly string Secret;
|
||||||
|
|
||||||
private User? UserCache;
|
private User? UserCache;
|
||||||
|
|
||||||
|
public PermissionStorage Permissions { get; private set; }
|
||||||
|
public PermissionStorage UserPermissions { get; private set; }
|
||||||
|
public PermissionStorage GroupPermissions { get; private set; }
|
||||||
|
|
||||||
public IdentityService(
|
public IdentityService(
|
||||||
CookieService cookieService,
|
CookieService cookieService,
|
||||||
UserRepository userRepository,
|
Repository<User> userRepository,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ConfigService configService)
|
ConfigService configService)
|
||||||
{
|
{
|
||||||
@@ -41,6 +47,8 @@ public class IdentityService
|
|||||||
if (UserCache != null)
|
if (UserCache != null)
|
||||||
return UserCache;
|
return UserCache;
|
||||||
|
|
||||||
|
ConstructPermissions();
|
||||||
|
|
||||||
var token = "none";
|
var token = "none";
|
||||||
|
|
||||||
// Load token via http context if available
|
// Load token via http context if available
|
||||||
@@ -101,7 +109,8 @@ public class IdentityService
|
|||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
Logger.Warn($"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker");
|
Logger.Warn(
|
||||||
|
$"Cannot find user with the id '{userid}' in the database. Maybe the user has been deleted or a token has been successfully faked by a hacker");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,15 +123,17 @@ public class IdentityService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var iatD = DateTimeOffset.FromUnixTimeSeconds(iat).ToUniversalTime().DateTime;
|
var iatD = DateTimeOffset.FromUnixTimeSeconds(iat).ToUniversalTime().DateTime;
|
||||||
|
|
||||||
if (iatD < user.TokenValidTime)
|
if (iatD < user.TokenValidTime)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
UserCache = user;
|
UserCache = user;
|
||||||
|
|
||||||
|
ConstructPermissions();
|
||||||
|
|
||||||
user.LastIp = GetIp();
|
user.LastIp = GetIp();
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
|
|
||||||
return UserCache;
|
return UserCache;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@@ -138,11 +149,11 @@ public class IdentityService
|
|||||||
if (HttpContextAccessor.HttpContext == null)
|
if (HttpContextAccessor.HttpContext == null)
|
||||||
return "N/A";
|
return "N/A";
|
||||||
|
|
||||||
if(HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
|
if (HttpContextAccessor.HttpContext.Request.Headers.ContainsKey("X-Real-IP"))
|
||||||
{
|
{
|
||||||
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
|
return HttpContextAccessor.HttpContext.Request.Headers["X-Real-IP"]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
|
return HttpContextAccessor.HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +172,7 @@ public class IdentityService
|
|||||||
|
|
||||||
return "Moonlight App " + version;
|
return "Moonlight App " + version;
|
||||||
}
|
}
|
||||||
|
|
||||||
var uaParser = Parser.GetDefault();
|
var uaParser = Parser.GetDefault();
|
||||||
var info = uaParser.Parse(userAgent);
|
var info = uaParser.Parse(userAgent);
|
||||||
|
|
||||||
@@ -172,4 +183,48 @@ public class IdentityService
|
|||||||
return "UserAgent not present";
|
return "UserAgent not present";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SavePermissions()
|
||||||
|
{
|
||||||
|
if (UserCache != null)
|
||||||
|
{
|
||||||
|
UserCache.Permissions = UserPermissions.Data;
|
||||||
|
UserRepository.Update(UserCache);
|
||||||
|
ConstructPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConstructPermissions()
|
||||||
|
{
|
||||||
|
if (UserCache == null)
|
||||||
|
{
|
||||||
|
UserPermissions = new(Array.Empty<byte>());
|
||||||
|
GroupPermissions = new(Array.Empty<byte>(), true);
|
||||||
|
Permissions = new(Array.Empty<byte>(), true);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = UserRepository
|
||||||
|
.Get()
|
||||||
|
.Include(x => x.PermissionGroup)
|
||||||
|
.First(x => x.Id == UserCache.Id);
|
||||||
|
|
||||||
|
UserPermissions = new PermissionStorage(user.Permissions);
|
||||||
|
|
||||||
|
if (user.PermissionGroup == null)
|
||||||
|
GroupPermissions = new PermissionStorage(Array.Empty<byte>(), true);
|
||||||
|
else
|
||||||
|
GroupPermissions = new PermissionStorage(user.PermissionGroup.Permissions, true);
|
||||||
|
|
||||||
|
Logger.Debug($"{UserPermissions[Perms.Permissions.AdminDashboard]} {GroupPermissions[Perms.Permissions.AdminDashboard]}");
|
||||||
|
|
||||||
|
Permissions = new PermissionStorage(BitHelper.OverwriteByteArrays(
|
||||||
|
UserPermissions.Data,
|
||||||
|
GroupPermissions.Data),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,19 @@
|
|||||||
|
|
||||||
<Router AppAssembly="@typeof(BlazorApp).Assembly">
|
<Router AppAssembly="@typeof(BlazorApp).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
<CascadingValue TValue="Type" Name="TargetPageType" Value="routeData.PageType">
|
||||||
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||||
|
</CascadingValue>
|
||||||
</Found>
|
</Found>
|
||||||
<NotFound>
|
<NotFound>
|
||||||
<PageTitle>Not found</PageTitle>
|
<PageTitle>Not found</PageTitle>
|
||||||
<LayoutView Layout="@typeof(NotFoundLayout)">
|
<LayoutView Layout="@typeof(NotFoundLayout)">
|
||||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
<NotFoundAlert />
|
||||||
</LayoutView>
|
</LayoutView>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
13
Moonlight/Shared/Components/Alerts/NoPermissionAlert.razor
Normal file
13
Moonlight/Shared/Components/Alerts/NoPermissionAlert.razor
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<div class="mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="d-flex justify-content-center pt-5">
|
||||||
|
<img height="300" width="300" src="/assets/media/svg/warning.svg" alt="Warning"/>
|
||||||
|
</div>
|
||||||
|
<span class="card-title text-center fs-3">
|
||||||
|
<TL>You have no permission to access this resource</TL>
|
||||||
|
</span>
|
||||||
|
<p class="card-body text-center fs-4 text-gray-800">
|
||||||
|
<TL>You have no permission to access this resource. This attempt has been logged ;)</TL>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="bullet me-5"></span> <TL>The resource was deleted</TL>
|
<span class="bullet me-5"></span> <TL>The resource was deleted</TL>
|
||||||
</li>
|
</li>
|
||||||
<li class="d-flex align-items-center py-2">
|
<li class="d-flex align-items-center py-2">
|
||||||
<span class="bullet me-5"></span> <TL>You have to permission to access this resource</TL>
|
<span class="bullet me-5"></span> <TL>You have no permission to access this resource</TL>
|
||||||
</li>
|
</li>
|
||||||
<li class="d-flex align-items-center py-2">
|
<li class="d-flex align-items-center py-2">
|
||||||
<span class="bullet me-5"></span> <TL>You may have entered invalid data</TL>
|
<span class="bullet me-5"></span> <TL>You may have entered invalid data</TL>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
@using Moonlight.App.Services
|
|
||||||
|
|
||||||
<div class="card card-flush w-lg-650px py-5">
|
|
||||||
<div class="card-body py-15 py-lg-20">
|
|
||||||
<h1 class="fw-bolder text-gray-900 mb-5"><TL>Setup complete</TL></h1>
|
|
||||||
<div class="fw-semibold fs-6 text-gray-500 mb-8">
|
|
||||||
<TL>It looks like this moonlight instance is ready to go</TL>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
86
Moonlight/Shared/Components/Partials/PermissionEditor.razor
Normal file
86
Moonlight/Shared/Components/Partials/PermissionEditor.razor
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
@using Moonlight.App.Services.Interop
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
@using Moonlight.App.Perms
|
||||||
|
|
||||||
|
@inject ModalService ModalService
|
||||||
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
|
<div id="permissionEditor" class="modal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
<TL>Edit permissions</TL>
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
@if (Enabled)
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle table-row-dashed fs-6 gy-5">
|
||||||
|
<tbody class="text-gray-600 fw-semibold">
|
||||||
|
@foreach (var permission in Permissions.GetAllPermissions())
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td class="text-gray-800">
|
||||||
|
@(permission.Name)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@(permission.Description)
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="form-check form-switch form-check-custom form-check-solid">
|
||||||
|
<input class="form-check-input" type="checkbox" @bind="Storage[permission]"/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<TL>Close</TL>
|
||||||
|
</button>
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Save"))"
|
||||||
|
WorkingText="@(SmartTranslateService.Translate("Saving"))"
|
||||||
|
CssClasses="btn-primary"
|
||||||
|
OnClick="Save">
|
||||||
|
</WButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public byte[] InitialData { get; set; } = Array.Empty<byte>();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<byte[], Task>? OnSave { get; set; }
|
||||||
|
|
||||||
|
private bool Enabled = false;
|
||||||
|
private PermissionStorage Storage;
|
||||||
|
|
||||||
|
public async Task Launch()
|
||||||
|
{
|
||||||
|
Enabled = true;
|
||||||
|
Storage = new(InitialData);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
await ModalService.Show("permissionEditor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Save()
|
||||||
|
{
|
||||||
|
OnSave?.Invoke(Storage.Data);
|
||||||
|
|
||||||
|
await ModalService.Hide("permissionEditor");
|
||||||
|
Enabled = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
@using Moonlight.App.Services.Sessions
|
||||||
|
@using Moonlight.App.Perms
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.Shared.Components.Alerts
|
||||||
|
|
||||||
|
@inject IdentityService IdentityService
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
@if (Allowed)
|
||||||
|
{
|
||||||
|
@ChildContent
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<NoPermissionAlert />
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter(Name = "TargetPageType")]
|
||||||
|
public Type TargetPageType { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public RenderFragment ChildContent { get; set; }
|
||||||
|
|
||||||
|
private bool Allowed = false;
|
||||||
|
|
||||||
|
protected override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
var attributes = TargetPageType.GetCustomAttributes(true);
|
||||||
|
var permAttrs = attributes
|
||||||
|
.Where(x => x.GetType() == typeof(PermissionRequired))
|
||||||
|
.Select(x => x as PermissionRequired)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
Allowed = true;
|
||||||
|
|
||||||
|
foreach (var permissionRequired in permAttrs)
|
||||||
|
{
|
||||||
|
var permission = Permissions.FromString(permissionRequired!.Name);
|
||||||
|
|
||||||
|
if (permission == null)
|
||||||
|
{
|
||||||
|
Allowed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IdentityService.Permissions[permission])
|
||||||
|
{
|
||||||
|
Allowed = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Allowed)
|
||||||
|
{
|
||||||
|
Logger.Warn($"{IdentityService.GetIp()} has tried to access {NavigationManager.Uri} without permission", "security");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,7 +94,9 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@Body
|
<RenderPermissionChecker>
|
||||||
|
@Body
|
||||||
|
</RenderPermissionChecker>
|
||||||
|
|
||||||
<RatingPopup/>
|
<RatingPopup/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Helpers
|
@using Moonlight.App.Helpers
|
||||||
@using Moonlight.App.Models.Misc
|
@using Moonlight.App.Models.Misc
|
||||||
|
@using Moonlight.App.Perms
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Newtonsoft.Json
|
@using Newtonsoft.Json
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
@inject DomainRepository DomainRepository
|
@inject DomainRepository DomainRepository
|
||||||
@inject ConfigService ConfigService
|
@inject ConfigService ConfigService
|
||||||
|
|
||||||
|
@attribute [PermissionRequired(nameof(Permissions.AdminDashboard))]
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@using Moonlight.App.Services.Interop
|
@using Moonlight.App.Services.Interop
|
||||||
@using Moonlight.App.Services.Sessions
|
@using Moonlight.App.Services.Sessions
|
||||||
|
|
||||||
@inject UserRepository UserRepository
|
@inject Repository<User> UserRepository
|
||||||
@inject UserService UserService
|
@inject UserService UserService
|
||||||
@inject SessionServerService SessionServerService
|
@inject SessionServerService SessionServerService
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
@@ -91,6 +91,10 @@
|
|||||||
<a href="/admin/users" class="btn btn-danger me-3">
|
<a href="/admin/users" class="btn btn-danger me-3">
|
||||||
<TL>Cancel</TL>
|
<TL>Cancel</TL>
|
||||||
</a>
|
</a>
|
||||||
|
<WButton Text="@(SmartTranslateService.Translate("Edit permissions"))"
|
||||||
|
CssClasses="btn-primary me-3"
|
||||||
|
OnClick="EditPermissions">
|
||||||
|
</WButton>
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Update"))"
|
<WButton Text="@(SmartTranslateService.Translate("Update"))"
|
||||||
WorkingText="@(SmartTranslateService.Translate("Updating"))"
|
WorkingText="@(SmartTranslateService.Translate("Updating"))"
|
||||||
CssClasses="btn-success"
|
CssClasses="btn-success"
|
||||||
@@ -149,6 +153,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<PermissionEditor @ref="PermissionEditor" OnSave="SavePermissions" />
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
</OnlyAdmin>
|
</OnlyAdmin>
|
||||||
|
|
||||||
@@ -158,9 +164,10 @@
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
private User? User;
|
private User? User;
|
||||||
|
|
||||||
private string NewPassword = "";
|
private string NewPassword = "";
|
||||||
|
|
||||||
|
private PermissionEditor PermissionEditor;
|
||||||
|
|
||||||
private Task Load(LazyLoader arg)
|
private Task Load(LazyLoader arg)
|
||||||
{
|
{
|
||||||
User = UserRepository.Get().FirstOrDefault(x => x.Id == Id);
|
User = UserRepository.Get().FirstOrDefault(x => x.Id == Id);
|
||||||
@@ -195,4 +202,20 @@
|
|||||||
|
|
||||||
await ToastService.Success(SmartTranslateService.Translate("Successfully updated password"));
|
await ToastService.Success(SmartTranslateService.Translate("Successfully updated password"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EditPermissions()
|
||||||
|
{
|
||||||
|
PermissionEditor.InitialData = User!.Permissions;
|
||||||
|
await PermissionEditor.Launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SavePermissions(byte[] data)
|
||||||
|
{
|
||||||
|
User!.Permissions = data;
|
||||||
|
UserRepository.Update(User);
|
||||||
|
|
||||||
|
await SessionServerService.ReloadUserSessions(User);
|
||||||
|
|
||||||
|
await ToastService.Success(SmartTranslateService.Translate("Successfully updated user"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
1
Moonlight/wwwroot/assets/media/svg/warning.svg
vendored
Normal file
1
Moonlight/wwwroot/assets/media/svg/warning.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.7 KiB |
Reference in New Issue
Block a user