Implemented a basic permission system base

This commit is contained in:
Marcel Baumgartner
2023-07-16 02:21:53 +02:00
parent f852df5807
commit 178ff36e86
24 changed files with 2847 additions and 27 deletions

View File

@@ -46,6 +46,7 @@ public class DataContext : DbContext
public DbSet<WebSpace> WebSpaces { get; set; }
public DbSet<SupportChatMessage> SupportChatMessages { get; set; }
public DbSet<IpBan> IpBans { get; set; }
public DbSet<PermissionGroup> PermissionGroups { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{

View 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>();
}

View File

@@ -1,4 +1,5 @@
using Moonlight.App.Models.Misc;
using System.ComponentModel.DataAnnotations;
using Moonlight.App.Models.Misc;
namespace Moonlight.App.Database.Entities;
@@ -39,6 +40,8 @@ public class User
public bool TotpEnabled { get; set; } = false;
public string TotpSecret { get; set; } = "";
public DateTime TokenValidTime { get; set; } = DateTime.UtcNow;
public byte[] Permissions { get; set; } = Array.Empty<byte>();
public PermissionGroup? PermissionGroup { get; set; }
// Discord
public ulong DiscordId { get; set; }

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -475,6 +475,25 @@ namespace Moonlight.App.Database.Migrations
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 =>
{
b.Property<int>("Id")
@@ -798,6 +817,13 @@ namespace Moonlight.App.Database.Migrations
.IsRequired()
.HasColumnType("longtext");
b.Property<int?>("PermissionGroupId")
.HasColumnType("int");
b.Property<byte[]>("Permissions")
.IsRequired()
.HasColumnType("longblob");
b.Property<int>("Rating")
.HasColumnType("int");
@@ -845,6 +871,8 @@ namespace Moonlight.App.Database.Migrations
b.HasIndex("CurrentSubscriptionId");
b.HasIndex("PermissionGroupId");
b.ToTable("Users");
});
@@ -1049,7 +1077,13 @@ namespace Moonlight.App.Database.Migrations
.WithMany()
.HasForeignKey("CurrentSubscriptionId");
b.HasOne("Moonlight.App.Database.Entities.PermissionGroup", "PermissionGroup")
.WithMany()
.HasForeignKey("PermissionGroupId");
b.Navigation("CurrentSubscription");
b.Navigation("PermissionGroup");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.WebSpace", b =>

View 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;
}
}

View 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;
}

View 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;
}
}

View 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);
}
}
}

View 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();
}
}

View File

@@ -2,9 +2,11 @@
using JWT.Algorithms;
using JWT.Builder;
using JWT.Exceptions;
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Helpers;
using Moonlight.App.Models.Misc;
using Moonlight.App.Perms;
using Moonlight.App.Repositories;
using UAParser;
@@ -12,16 +14,20 @@ namespace Moonlight.App.Services.Sessions;
public class IdentityService
{
private readonly UserRepository UserRepository;
private readonly Repository<User> UserRepository;
private readonly CookieService CookieService;
private readonly IHttpContextAccessor HttpContextAccessor;
private readonly string Secret;
private User? UserCache;
public PermissionStorage Permissions { get; private set; }
public PermissionStorage UserPermissions { get; private set; }
public PermissionStorage GroupPermissions { get; private set; }
public IdentityService(
CookieService cookieService,
UserRepository userRepository,
Repository<User> userRepository,
IHttpContextAccessor httpContextAccessor,
ConfigService configService)
{
@@ -41,6 +47,8 @@ public class IdentityService
if (UserCache != null)
return UserCache;
ConstructPermissions();
var token = "none";
// Load token via http context if available
@@ -101,7 +109,8 @@ public class IdentityService
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;
}
@@ -114,15 +123,17 @@ public class IdentityService
}
var iatD = DateTimeOffset.FromUnixTimeSeconds(iat).ToUniversalTime().DateTime;
if (iatD < user.TokenValidTime)
return null;
UserCache = user;
ConstructPermissions();
user.LastIp = GetIp();
UserRepository.Update(user);
return UserCache;
}
catch (Exception e)
@@ -138,11 +149,11 @@ public class IdentityService
if (HttpContextAccessor.HttpContext == null)
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.Connection.RemoteIpAddress!.ToString();
}
@@ -161,7 +172,7 @@ public class IdentityService
return "Moonlight App " + version;
}
var uaParser = Parser.GetDefault();
var info = uaParser.Parse(userAgent);
@@ -172,4 +183,48 @@ public class IdentityService
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
);
}
}