[update] Refactoring database logic and optimizing queries

This commit is contained in:
acite
2025-09-17 18:47:12 +08:00
parent 8465ec5b2a
commit 40c041444a
14 changed files with 192 additions and 116 deletions

View File

@@ -11,7 +11,19 @@
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment=""> <list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Abyss/Abyss.csproj" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Abyss.csproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/LiveController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Model/ResourceAttribute.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/ResourceAttribute.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Model/Task.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Task.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Model/User.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/User.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Model/UserCreating.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/UserCreating.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Abyss/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Program.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -210,6 +222,11 @@
<workItem from="1757782027930" duration="308000" /> <workItem from="1757782027930" duration="308000" />
<workItem from="1757830765557" duration="1218000" /> <workItem from="1757830765557" duration="1218000" />
<workItem from="1757862781213" duration="341000" /> <workItem from="1757862781213" duration="341000" />
<workItem from="1757918235256" duration="1000" />
<workItem from="1758040123892" duration="21000" />
<workItem from="1758040188148" duration="1000" />
<workItem from="1758049713959" duration="86000" />
<workItem from="1758084310862" duration="14054000" />
</task> </task>
<servers /> <servers />
</component> </component>

View File

@@ -13,6 +13,10 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSec.Cryptography" Version="25.4.0" /> <PackageReference Include="NSec.Cryptography" Version="25.4.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" /> <PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.11" />
<PackageReference Include="SQLitePCLRaw.core" Version="3.0.2" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.11" />
<PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="3.0.2" />
<PackageReference Include="Standart.Hash.xxHash" Version="4.0.5" /> <PackageReference Include="Standart.Hash.xxHash" Version="4.0.5" />
<PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" /> <PackageReference Include="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" />
</ItemGroup> </ItemGroup>

View File

@@ -11,7 +11,7 @@ public class LiveController(ILogger<LiveController> logger, ResourceService rs,
public readonly string LiveFolder = Path.Combine(config.MediaRoot, "Live"); public readonly string LiveFolder = Path.Combine(config.MediaRoot, "Live");
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task<IActionResult> AddLive(string id, string token, string owner) public async Task<IActionResult> AddLive(string id, string token, int owner)
{ {
var d = Helpers.SafePathCombine(LiveFolder, [id]); var d = Helpers.SafePathCombine(LiveFolder, [id]);
if (d == null) return StatusCode(403, new { message = "403 Denied" }); if (d == null) return StatusCode(403, new { message = "403 Denied" });

View File

@@ -60,7 +60,7 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
.Select(x => JsonConvert.DeserializeObject<Video>(x)).ToArray(); .Select(x => JsonConvert.DeserializeObject<Video>(x)).ToArray();
return Ok(sv.Zip(r, (x, y) => (x, y)).NaturalSort(x => x.x.name).Select(x => x.y).ToArray()); return Ok(sv.Zip(r, (x, y) => (x, y)).NaturalSort(x => x.x!.name).Select(x => x.y).ToArray());
} }
[HttpGet("{klass}/{id}")] [HttpGet("{klass}/{id}")]

View File

@@ -42,7 +42,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
public IActionResult Validate(string token) public IActionResult Validate(string token)
{ {
var u = _user.Validate(token, Ip); var u = _user.Validate(token, Ip);
if (u == null) if (u == -1)
{ {
return StatusCode(401, new { message = "Invalid" }); return StatusCode(401, new { message = "Invalid" });
} }
@@ -54,7 +54,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
public IActionResult Destroy(string token) public IActionResult Destroy(string token)
{ {
var u = _user.Validate(token, Ip); var u = _user.Validate(token, Ip);
if (u == null) if (u == -1)
{ {
return StatusCode(401, new { message = "Invalid" }); return StatusCode(401, new { message = "Invalid" });
} }
@@ -81,14 +81,14 @@ public class UserController(UserService user, ILogger<UserController> logger) :
return StatusCode(403, new { message = "Denied" }); return StatusCode(403, new { message = "Denied" });
// Valid parent && Privilege // Valid parent && Privilege
var ou = await _user.QueryUser(_user.Validate(r, Ip) ?? ""); var ou = await _user.QueryUser(_user.Validate(r, Ip));
if(creating.Parent != (_user.Validate(r, Ip) ?? "") || creating.Privilege > ou?.Privilege) if(creating.Privilege > ou?.Privilege || ou == null)
return StatusCode(403, new { message = "Denied" }); return StatusCode(403, new { message = "Denied" });
await _user.CreateUser(new User() await _user.CreateUser(new User
{ {
Name = creating.Name, Username = creating.Name,
Parent = _user.Validate(r, Ip) ?? "", ParentId = ou.Uuid,
Privilege = creating.Privilege, Privilege = creating.Privilege,
PublicKey = creating.PublicKey, PublicKey = creating.PublicKey,
} ); } );
@@ -101,7 +101,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
public async Task<IActionResult> Open(string user, [FromQuery] string token, [FromQuery] string? bindIp = null) public async Task<IActionResult> Open(string user, [FromQuery] string token, [FromQuery] string? bindIp = null)
{ {
var caller = _user.Validate(token, Ip); var caller = _user.Validate(token, Ip);
if (caller == null || caller != "root") if (caller != 1)
{ {
return StatusCode(403, new { message = "Access forbidden" }); return StatusCode(403, new { message = "Access forbidden" });
} }
@@ -114,7 +114,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? Ip : bindIp; var ipToBind = string.IsNullOrWhiteSpace(bindIp) ? Ip : bindIp;
var t = _user.CreateToken(user, ipToBind, TimeSpan.FromHours(1)); var t = _user.CreateToken(target.Uuid, ipToBind, TimeSpan.FromHours(1));
_logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user, ipToBind, Ip); _logger.LogInformation("Root created 1h token for {User}, bound to {BindIp}, request from {ReqIp}", user, ipToBind, Ip);
return Ok(new { token = t, user, boundIp = ipToBind }); return Ok(new { token = t, user, boundIp = ipToBind });

View File

@@ -4,7 +4,6 @@ using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Abyss.Components.Static; using Abyss.Components.Static;
using Abyss.Model; using Abyss.Model;
using Microsoft.Extensions.Caching.Memory;
using SQLite; using SQLite;
using System.IO.Hashing; using System.IO.Hashing;
@@ -21,19 +20,15 @@ public class ResourceService
{ {
private readonly ILogger<ResourceService> _logger; private readonly ILogger<ResourceService> _logger;
private readonly ConfigureService _config; private readonly ConfigureService _config;
private readonly IMemoryCache _cache;
private readonly UserService _user; private readonly UserService _user;
private readonly SQLiteAsyncConnection _database; private readonly SQLiteAsyncConnection _database;
private static readonly Regex PermissionRegex = private static readonly Regex PermissionRegex = new("^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache, public ResourceService(ILogger<ResourceService> logger, ConfigureService config, UserService user)
UserService user)
{ {
_logger = logger; _logger = logger;
_config = config; _config = config;
_cache = cache;
_user = user; _user = user;
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); _database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
@@ -42,13 +37,13 @@ public class ResourceService
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks"); var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
if (tasksPath != null) if (tasksPath != null)
{ {
InsertRaRow(tasksPath, "root", "rw,r-,r-", true).Wait(); InsertRaRow(tasksPath, 1, "rw,r-,r-", true).Wait();
} }
var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live"); var livePath = Helpers.SafePathCombine(_config.MediaRoot, "Live");
if (livePath != null) if (livePath != null)
{ {
InsertRaRow(livePath, "root", "rw,r-,r-", true).Wait(); InsertRaRow(livePath, 1, "rw,r-,r-", true).Wait();
} }
} }
@@ -57,12 +52,12 @@ public class ResourceService
{ {
var b = Encoding.UTF8.GetBytes(path); var b = Encoding.UTF8.GetBytes(path);
var r = XxHash128.Hash(b, 0x11451419); var r = XxHash128.Hash(b, 0x11451419);
return Convert.ToBase64String(r ?? []); return Convert.ToBase64String(r);
} }
public async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip) private async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip)
{ {
if (paths == null || paths.Length == 0) if (paths.Length == 0)
{ {
_logger.LogError("ValidAll called with empty path set"); _logger.LogError("ValidAll called with empty path set");
return false; return false;
@@ -74,7 +69,7 @@ public class ResourceService
var relPaths = new List<string>(paths.Length); var relPaths = new List<string>(paths.Length);
foreach (var p in paths) foreach (var p in paths)
{ {
if (p == null || !p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase)) if (!p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogError($"Path outside media root or null: {p}"); _logger.LogError($"Path outside media root or null: {p}");
return false; return false;
@@ -84,22 +79,24 @@ public class ResourceService
} }
// 2. validate token and user once // 2. validate token and user once
string? username = _user.Validate(token, ip); int uuid = _user.Validate(token, ip);
if (username == null) if (uuid == -1)
{ {
_logger.LogError($"Invalid token: {token}"); _logger.LogError($"Invalid token: {token}");
return false; return false;
} }
User? user = await _user.QueryUser(username); User? user = await _user.QueryUser(uuid);
if (user == null || user.Name != username) if (user == null || user.Uuid != uuid)
{ {
_logger.LogError($"Verification failed: {token}"); _logger.LogError($"Verification failed: {token}");
return false; return false;
} }
// 3. build uid -> required ops map (avoid duplicate Uid calculations) // 3. build uid -> required ops map (avoid duplicate Uid calculations)
var uidToOps = new Dictionary<string, HashSet<OperationType>>(StringComparer.OrdinalIgnoreCase); var uidToOps = new Dictionary<string, HashSet<OperationType>>(StringComparer.OrdinalIgnoreCase);
var uidToExampleRelPath =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); // for better logging
foreach (var rel in relPaths) foreach (var rel in relPaths)
{ {
var parts = rel var parts = rel
@@ -117,6 +114,7 @@ public class ResourceService
{ {
ops = new HashSet<OperationType>(); ops = new HashSet<OperationType>();
uidToOps[uidDir] = ops; uidToOps[uidDir] = ops;
uidToExampleRelPath[uidDir] = subPath;
} }
ops.Add(OperationType.Read); ops.Add(OperationType.Read);
@@ -129,16 +127,40 @@ public class ResourceService
{ {
resOps = new HashSet<OperationType>(); resOps = new HashSet<OperationType>();
uidToOps[uidRes] = resOps; uidToOps[uidRes] = resOps;
uidToExampleRelPath[uidRes] = resourcePath;
} }
resOps.Add(type); resOps.Add(type);
} }
// 4. batch query DB for all UIDs // 4. batch query DB for all UIDs using parameterized IN (...) and chunking to respect SQLite param limits
var uidsNeeded = uidToOps.Keys.ToList(); var uidsNeeded = uidToOps.Keys.ToList();
var rasList = await _database.Table<ResourceAttribute>() var rasList = new List<ResourceAttribute>();
.Where(r => uidsNeeded.Contains(r.Uid))
.ToListAsync(); const int sqliteMaxVariableNumber = 900; // keep below default 999 for safety
if (uidsNeeded.Count > 0)
{
if (uidsNeeded.Count <= sqliteMaxVariableNumber)
{
var placeholders = string.Join(",", uidsNeeded.Select(_ => "?"));
var queryArgs = uidsNeeded.Cast<object>().ToArray();
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
var chunkResult = await _database.QueryAsync<ResourceAttribute>(sql, queryArgs);
rasList.AddRange(chunkResult);
}
else
{
for (int i = 0; i < uidsNeeded.Count; i += sqliteMaxVariableNumber)
{
var chunk = uidsNeeded.Skip(i).Take(sqliteMaxVariableNumber).ToList();
var placeholders = string.Join(",", chunk.Select(_ => "?"));
var queryArgs = chunk.Cast<object>().ToArray();
var sql = $"SELECT * FROM ResourceAttributes WHERE Uid IN ({placeholders})";
var chunkResult = await _database.QueryAsync<ResourceAttribute>(sql, queryArgs);
rasList.AddRange(chunkResult);
}
}
}
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase); var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
@@ -148,10 +170,11 @@ public class ResourceService
foreach (var kv in uidToOps) foreach (var kv in uidToOps)
{ {
var uid = kv.Key; var uid = kv.Key;
if (!raDict.TryGetValue(uid, out var ra) || ra == null) if (!raDict.TryGetValue(uid, out var ra))
{ {
// find an example path string for logging would require reverse map; keep uid for clarity var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid);
_logger.LogError($"Permission check failed (missing resource attribute): User: {username}, Uid: {uid}"); _logger.LogError(
$"Permission check failed (missing resource attribute): User: {uuid}, Resource: {examplePath}, Uid: {uid}");
return false; return false;
} }
@@ -166,7 +189,9 @@ public class ResourceService
if (!ok) if (!ok)
{ {
_logger.LogError($"Permission check failed: User: {username}, Uid: {uid}, Type: {op}"); var examplePath = uidToExampleRelPath.TryGetValue(uid, out var p) ? p : uid;
_logger.LogError(
$"Permission check failed: User: {uuid}, Resource: {examplePath}, Uid: {uid}, Type: {op}");
return false; return false;
} }
} }
@@ -175,6 +200,7 @@ public class ResourceService
return true; return true;
} }
public async Task<bool> Valid(string path, string token, OperationType type, string ip) public async Task<bool> Valid(string path, string token, OperationType type, string ip)
{ {
// Path is abs path here, due to Helpers.SafePathCombine // Path is abs path here, due to Helpers.SafePathCombine
@@ -183,16 +209,16 @@ public class ResourceService
path = Path.GetRelativePath(_config.MediaRoot, path); path = Path.GetRelativePath(_config.MediaRoot, path);
string? username = _user.Validate(token, ip); int uuid = _user.Validate(token, ip);
if (username == null) if (uuid == -1)
{ {
// No permission granted for invalid tokens // No permission granted for invalid tokens
_logger.LogError($"Invalid token: {token}"); _logger.LogError($"Invalid token: {token}");
return false; return false;
} }
User? user = await _user.QueryUser(username); User? user = await _user.QueryUser(uuid);
if (user == null || user.Name != username) if (user == null || user.Uuid != uuid)
{ {
_logger.LogError($"Verification failed: {token}"); _logger.LogError($"Verification failed: {token}");
return false; // Two-factor authentication return false; // Two-factor authentication
@@ -205,34 +231,38 @@ public class ResourceService
{ {
var subPath = Path.Combine(parts.Take(i + 1).ToArray()); var subPath = Path.Combine(parts.Take(i + 1).ToArray());
var uidDir = Uid(subPath); var uidDir = Uid(subPath);
var raDir = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uidDir) var raDir = await _database
.Table<ResourceAttribute>()
.Where(r => r.Uid == uidDir)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (raDir == null) if (raDir == null)
{ {
_logger.LogError($"Permission denied: {username} has no read access to parent directory {subPath}"); _logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}");
return false; return false;
} }
if (!await CheckPermission(user, raDir, OperationType.Read)) if (!await CheckPermission(user, raDir, OperationType.Read))
{ {
_logger.LogError($"Permission denied: {username} has no read access to parent directory {subPath}"); _logger.LogError($"Permission denied: {uuid} has no read access to parent directory {subPath}");
return false; return false;
} }
} }
var uid = Uid(path); var uid = Uid(path);
ResourceAttribute? ra = await _database.Table<ResourceAttribute>().Where(r => r.Uid == uid) ResourceAttribute? ra = await _database
.Table<ResourceAttribute>()
.Where(r => r.Uid == uid)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (ra == null) if (ra == null)
{ {
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} "); _logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
return false; return false;
} }
var l = await CheckPermission(user, ra, type); var l = await CheckPermission(user, ra, type);
if (!l) if (!l)
{ {
_logger.LogError($"Permission check failed: User: {username}, Resource: {path}, Type: {type.ToString()} "); _logger.LogError($"Permission check failed: User: {uuid}, Resource: {path}, Type: {type.ToString()} ");
} }
return l; return l;
@@ -250,7 +280,7 @@ public class ResourceService
var owner = await _user.QueryUser(ra.Owner); var owner = await _user.QueryUser(ra.Owner);
if (owner == null) return false; if (owner == null) return false;
bool isOwner = ra.Owner == user.Name; bool isOwner = ra.Owner == user.Uuid;
bool isPeer = !isOwner && user.Privilege == owner.Privilege; bool isPeer = !isOwner && user.Privilege == owner.Privilege;
bool isOther = !isOwner && !isPeer; bool isOther = !isOwner && !isPeer;
@@ -267,7 +297,7 @@ public class ResourceService
case OperationType.Write: case OperationType.Write:
return currentPerm.Contains('w') || (user.Privilege > owner.Privilege); return currentPerm.Contains('w') || (user.Privilege > owner.Privilege);
case OperationType.Security: case OperationType.Security:
return (isOwner && currentPerm.Contains('w')) || user.Name == "root"; return (isOwner && currentPerm.Contains('w')) || user.Uuid == 1;
default: default:
return false; return false;
} }
@@ -300,17 +330,25 @@ public class ResourceService
return await Valid(path, token, OperationType.Write, ip); return await Valid(path, token, OperationType.Write, ip);
} }
public async Task<bool> Initialize(string path, string token, string username, string ip) public async Task<bool> Initialize(string path, string token, string owner, string ip)
{
var u = await _user.QueryUser(owner);
if (u == null || u.Uuid == -1) return false;
return await Initialize(path, token, u.Uuid, ip);
}
public async Task<bool> Initialize(string path, string token, int owner, string ip)
{ {
// TODO: Use a more elegant Debug mode // TODO: Use a more elegant Debug mode
if (_config.DebugMode == "Debug") if (_config.DebugMode == "Debug")
goto debug; goto debug;
// 1. Authorization: Verify the operation is performed by 'root' // 1. Authorization: Verify the operation is performed by 'root'
var requester = _user.Validate(token, ip); var requester = _user.Validate(token, ip);
if (requester != "root") if (requester != 1)
{ {
_logger.LogWarning( _logger.LogWarning(
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources."); $"Permission denied: Non-root user '{requester}' attempted to initialize resources.");
return false; return false;
} }
@@ -322,10 +360,10 @@ public class ResourceService
return false; return false;
} }
var ownerUser = await _user.QueryUser(username); var ownerUser = await _user.QueryUser(owner);
if (ownerUser == null) if (ownerUser == null)
{ {
_logger.LogError($"Initialization failed: Owner user '{username}' does not exist."); _logger.LogError($"Initialization failed: Owner user '{owner}' does not exist.");
return false; return false;
} }
@@ -349,8 +387,7 @@ public class ResourceService
newResources.Add(new ResourceAttribute newResources.Add(new ResourceAttribute
{ {
Uid = uid, Uid = uid,
Name = currentPath, Owner = owner,
Owner = username,
Permission = "rw,--,--" Permission = "rw,--,--"
}); });
} }
@@ -361,7 +398,7 @@ public class ResourceService
{ {
await _database.InsertAllAsync(newResources); await _database.InsertAllAsync(newResources);
_logger.LogInformation( _logger.LogInformation(
$"Successfully initialized {newResources.Count} new resources under '{path}' for user '{username}'."); $"Successfully initialized {newResources.Count} new resources under '{path}' for user '{owner}'.");
} }
else else
{ {
@@ -391,10 +428,9 @@ public class ResourceService
public async Task<bool> Exclude(string path, string token, string ip) public async Task<bool> Exclude(string path, string token, string ip)
{ {
var requester = _user.Validate(token, ip); var requester = _user.Validate(token, ip);
if (requester != "root") if (requester != 1)
{ {
_logger.LogWarning( _logger.LogWarning($"Permission denied: Non-root user '{requester}' attempted to exclude resource '{path}'.");
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
return false; return false;
} }
@@ -429,13 +465,13 @@ public class ResourceService
} }
} }
public async Task<bool> Include(string path, string token, string ip, string owner, string permission) public async Task<bool> Include(string path, string token, string ip, int owner, string permission)
{ {
var requester = _user.Validate(token, ip); var requester = _user.Validate(token, ip);
if (requester != "root") if (requester != 1)
{ {
_logger.LogWarning( _logger.LogWarning(
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to include resource '{path}'."); $"Permission denied: Non-root user '{requester}' attempted to include resource '{path}'.");
return false; return false;
} }
@@ -467,7 +503,6 @@ public class ResourceService
var newResource = new ResourceAttribute var newResource = new ResourceAttribute
{ {
Uid = uid, Uid = uid,
Name = relPath,
Owner = owner, Owner = owner,
Permission = permission Permission = permission
}; };
@@ -553,9 +588,8 @@ public class ResourceService
return false; return false;
} }
} }
public async Task<bool> Chown(string path, string token, int owner, string ip)
public async Task<bool> Chown(string path, string token, string owner, string ip)
{ {
if (!await Valid(path, token, OperationType.Security, ip)) if (!await Valid(path, token, OperationType.Security, ip))
return false; return false;
@@ -601,7 +635,7 @@ public class ResourceService
} }
} }
private async Task<bool> InsertRaRow(string fullPath, string owner, string permission, bool update = false) private async Task<bool> InsertRaRow(string fullPath, int owner, string permission, bool update = false)
{ {
if (!PermissionRegex.IsMatch(permission)) if (!PermissionRegex.IsMatch(permission))
{ {
@@ -615,7 +649,6 @@ public class ResourceService
return await _database.InsertOrReplaceAsync(new ResourceAttribute() return await _database.InsertOrReplaceAsync(new ResourceAttribute()
{ {
Uid = Uid(path), Uid = Uid(path),
Name = path,
Owner = owner, Owner = owner,
Permission = permission, Permission = permission,
}) == 1; }) == 1;
@@ -624,7 +657,6 @@ public class ResourceService
return await _database.InsertAsync(new ResourceAttribute() return await _database.InsertAsync(new ResourceAttribute()
{ {
Uid = Uid(path), Uid = Uid(path),
Name = path,
Owner = owner, Owner = owner,
Permission = permission, Permission = permission,
}) == 1; }) == 1;

View File

@@ -61,7 +61,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
if(!await rs.Valid(VideoFolder, token, OperationType.Write, ip)) if(!await rs.Valid(VideoFolder, token, OperationType.Write, ip))
return null; return null;
var u = user.Validate(token, ip); var u = user.Validate(token, ip);
if(u == null) if(u == -1)
return null; return null;
var r = new TaskCreationResponse() var r = new TaskCreationResponse()
@@ -74,7 +74,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
Directory.CreateDirectory(Path.Combine(TaskFolder, r.Id.ToString(), "gallery")); Directory.CreateDirectory(Path.Combine(TaskFolder, r.Id.ToString(), "gallery"));
// It shouldn't be a problem to spell it directly like this, as all the parameters are generated by myself // It shouldn't be a problem to spell it directly like this, as all the parameters are generated by myself
Task v = new Task() Task v = new Task
{ {
Name = creation.Name, Name = creation.Name,
Owner = u, Owner = u,
@@ -83,7 +83,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
Type = TaskType.Video Type = TaskType.Video
}; };
await System.IO.File.WriteAllTextAsync( await File.WriteAllTextAsync(
Path.Combine(TaskFolder, r.Id.ToString(), "task.json"), Path.Combine(TaskFolder, r.Id.ToString(), "task.json"),
JsonConvert.SerializeObject(v, Formatting.Indented)); JsonConvert.SerializeObject(v, Formatting.Indented));
@@ -239,7 +239,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
return -1; return -1;
} }
} }
catch (Exception ex) catch (Exception)
{ {
return -1; return -1;
} }

View File

@@ -14,22 +14,20 @@ namespace Abyss.Components.Services;
public class UserService public class UserService
{ {
private readonly ILogger<UserService> _logger; private readonly ILogger<UserService> _logger;
private readonly ConfigureService _config;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly SQLiteAsyncConnection _database; private readonly SQLiteAsyncConnection _database;
public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache) public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache)
{ {
_logger = logger; _logger = logger;
_config = config;
_cache = cache; _cache = cache;
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create); _database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync<User>().Wait(); _database.CreateTableAsync<User>().Wait();
var rootUser = _database.Table<User>().Where(x => x.Name == "root").FirstOrDefaultAsync().Result; var rootUser = _database.Table<User>().Where(x => x.Uuid == 1).FirstOrDefaultAsync().Result;
if (_config.DebugMode == "Debug") if (config.DebugMode == "Debug")
_cache.Set("root", $"root@127.0.0.1", DateTimeOffset.Now.AddHours(1)); _cache.Set("abyss", $"1@127.0.0.1", DateTimeOffset.Now.AddHours(1));
// Test token, can only be used locally. Will be destroyed in one hour. // Test token, can only be used locally. Will be destroyed in one hour.
if (rootUser == null) if (rootUser == null)
@@ -50,8 +48,9 @@ public class UserService
Console.WriteLine("key: '" + privateKeyBase64 + "'"); Console.WriteLine("key: '" + privateKeyBase64 + "'");
_database.InsertAsync(new User() _database.InsertAsync(new User()
{ {
Name = "root", Uuid = 1,
Parent = "root", Username = "root",
ParentId = 1,
PublicKey = publicKeyBase64, PublicKey = publicKeyBase64,
Privilege = 1145141919, Privilege = 1145141919,
}).Wait(); }).Wait();
@@ -61,15 +60,16 @@ public class UserService
} }
public async Task<string?> Challenge(string user) public async Task<string?> Challenge(string user)
{ {
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync(); var u = await _database.Table<User>().Where(x => x.Username == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists if (u == null) // Error: User not exists
return null; return null;
if (_cache.TryGetValue(u.Name, out var challenge)) // The previous challenge has not yet expired
_cache.Remove(u.Name); if (_cache.TryGetValue(u.Uuid, out _)) // The previous challenge has not yet expired
_cache.Remove(u.Uuid);
var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32))); var c = Convert.ToBase64String(Encoding.UTF8.GetBytes(GenerateRandomAsciiString(32)));
_cache.Set(u.Name,c, DateTimeOffset.Now.AddMinutes(1)); _cache.Set(u.Uuid, c, DateTimeOffset.Now.AddMinutes(1));
return c; return c;
} }
@@ -77,12 +77,13 @@ public class UserService
// but the source that obtains the token must be the same as the source that uses the token in the future // but the source that obtains the token must be the same as the source that uses the token in the future
public async Task<string?> Verify(string user, string response, string ip) public async Task<string?> Verify(string user, string response, string ip)
{ {
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync(); var u = await _database.Table<User>().Where(x => x.Username == user).FirstOrDefaultAsync();
if (u == null) // Error: User not exists if (u == null) // Error: User not exists
{ {
return null; return null;
} }
if (_cache.TryGetValue(u.Name, out string? challenge))
if (_cache.TryGetValue(u.Uuid, out string? challenge))
{ {
bool isVerified = VerifySignature( bool isVerified = VerifySignature(
PublicKey.Import( PublicKey.Import(
@@ -95,16 +96,16 @@ public class UserService
if (!isVerified) if (!isVerified)
{ {
// Verification failed, set the challenge string to random to prevent duplicate verification // Verification failed, set the challenge string to random to prevent duplicate verification
_cache.Set(u.Name, $"failed : {GenerateRandomAsciiString(32)}", DateTimeOffset.Now.AddMinutes(1)); _cache.Set(u.Uuid, $"failed : {GenerateRandomAsciiString(32)}", DateTimeOffset.Now.AddMinutes(1));
return null; return null;
} }
else else
{ {
// Remove the challenge string and create a session // Remove the challenge string and create a session
_cache.Remove(u.Name); _cache.Remove(u.Uuid);
var s = GenerateRandomAsciiString(64); var s = GenerateRandomAsciiString(64);
_cache.Set(s, $"{u.Name}@{ip}", DateTimeOffset.Now.AddDays(1)); _cache.Set(s, $"{u.Uuid}@{ip}", DateTimeOffset.Now.AddDays(1));
_logger.LogInformation($"Verified {u.Name}@{ip}"); _logger.LogInformation($"Verified {u.Uuid}@{ip}, Name: {u.Username}");
return s; return s;
} }
} }
@@ -112,7 +113,9 @@ public class UserService
return null; return null;
} }
public string? Validate(string token, string ip) // Id >= 1 : Success, Uid
// Id == -1: Failed
public int Validate(string token, string ip)
{ {
if (_cache.TryGetValue(token, out string? userAndIp)) if (_cache.TryGetValue(token, out string? userAndIp))
{ {
@@ -120,13 +123,13 @@ public class UserService
{ {
_logger.LogError($"Token used from another Host: {token}"); _logger.LogError($"Token used from another Host: {token}");
Destroy(token); Destroy(token);
return null; return -1;
} }
// _logger.LogInformation($"Validated {userAndIp}"); // _logger.LogInformation($"Validated {userAndIp}");
return userAndIp?.Split('@')[0]; return Convert.ToInt32(userAndIp?.Split('@')[0]);
} }
_logger.LogWarning($"Validation failed {token}"); _logger.LogWarning($"Validation failed {token}");
return null; return -1;
} }
public void Destroy(string token) public void Destroy(string token)
@@ -134,16 +137,24 @@ public class UserService
_cache.Remove(token); _cache.Remove(token);
} }
public async Task<User?> QueryUser(string user) public async Task<User?> QueryUser(int uid)
{ {
var u = await _database.Table<User>().Where(x => x.Name == user).FirstOrDefaultAsync(); if (uid == -1)
return null;
var u = await _database.Table<User>().Where(x => x.Uuid == uid).FirstOrDefaultAsync();
return u;
}
public async Task<User?> QueryUser(string username)
{
var u = await _database.Table<User>().Where(x => x.Username == username).FirstOrDefaultAsync();
return u; return u;
} }
public async Task CreateUser(User user) public async Task CreateUser(User user)
{ {
await _database.InsertAsync(user); await _database.InsertAsync(user);
_logger.LogInformation($"Created user: {user.Name}, Parent: {user.Parent}, Privilege: {user.Privilege}"); _logger.LogInformation($"Created user: {user.Username}, Uid: {user.Uuid}, Parent: {user.ParentId}, Privilege: {user.Privilege}");
} }
static Key GenerateKeyPair() static Key GenerateKeyPair()
@@ -195,23 +206,23 @@ public class UserService
if (VerifySignature(pubKey, data, signature)) if (VerifySignature(pubKey, data, signature))
{ {
_logger.LogInformation($"Signature verified using user {u.Name}"); _logger.LogInformation($"Signature verified using user {u.Username}");
return true; return true;
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, $"Failed to import public key for {u.Name}"); _logger.LogWarning(ex, $"Failed to import public key for {u.Username}");
} }
} }
return false; return false;
} }
public string CreateToken(string user, string ip, TimeSpan lifetime) public string CreateToken(int uid, string ip, TimeSpan lifetime)
{ {
var token = GenerateRandomAsciiString(64); var token = GenerateRandomAsciiString(64);
_cache.Set(token, $"{user}@{ip}", DateTimeOffset.Now.Add(lifetime)); _cache.Set(token, $"{uid}@{ip}", DateTimeOffset.Now.Add(lifetime));
_logger.LogInformation($"Created token for {user}@{ip}, valid {lifetime.TotalMinutes} minutes"); _logger.LogInformation($"Created token for {uid}@{ip}, valid {lifetime.TotalMinutes} minutes");
return token; return token;
} }
} }

View File

@@ -73,8 +73,6 @@ public static class Helpers
{ {
return PathType.AccessDenied; return PathType.AccessDenied;
} }
return PathType.NotFound;
} }
} }

View File

@@ -1,9 +1,17 @@
using SQLite;
namespace Abyss.Model; namespace Abyss.Model;
[Table("ResourceAttributes")]
public class ResourceAttribute public class ResourceAttribute
{ {
public string Uid { get; set; } = "@"; [PrimaryKey, AutoIncrement]
public string Name { get; set; } = "@"; public int Id { get; set; }
public string Owner { get; set; } = "@";
[Unique, NotNull]
public string Uid { get; init; } = "@";
[NotNull]
public int Owner { get; set; }
[NotNull]
public string Permission { get; set; } = "--,--,--"; public string Permission { get; set; } = "--,--,--";
} }

View File

@@ -9,7 +9,7 @@ public enum TaskType
public class Task public class Task
{ {
public uint Id; public uint Id;
public string Owner = ""; public int Owner;
public string Class = ""; public string Class = "";
public string Name = ""; public string Name = "";
public TaskType Type; public TaskType Type;

View File

@@ -1,9 +1,18 @@
using SQLite;
namespace Abyss.Model; namespace Abyss.Model;
[Table("Users")]
public class User public class User
{ {
public string Name { get; set; } = ""; [PrimaryKey, AutoIncrement]
public string Parent { get; set; } = ""; public int Uuid { get; set; }
[Unique, NotNull]
public string Username { get; set; } = "";
[NotNull]
public int ParentId { get; set; }
[NotNull]
public string PublicKey { get; set; } = ""; public string PublicKey { get; set; } = "";
[NotNull]
public int Privilege { get; set; } public int Privilege { get; set; }
} }

View File

@@ -4,7 +4,6 @@ public class UserCreating
{ {
public string Response { get; set; } = ""; public string Response { get; set; } = "";
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Parent { get; set; } = "";
public string PublicKey { get; set; } = ""; public string PublicKey { get; set; } = "";
public int Privilege { get; set; } public int Privilege { get; set; }
} }

View File

@@ -38,8 +38,6 @@ public class Program
}; };
}); });
builder.Services.BuildServiceProvider().GetRequiredService<UserService>();
var app = builder.Build(); var app = builder.Build();
// app.UseHttpsRedirection(); // app.UseHttpsRedirection();