[update] Refactoring database logic and optimizing queries
This commit is contained in:
19
.idea/.idea.Abyss/.idea/workspace.xml
generated
19
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -11,7 +11,19 @@
|
||||
<component name="ChangeListManager">
|
||||
<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$/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -210,6 +222,11 @@
|
||||
<workItem from="1757782027930" duration="308000" />
|
||||
<workItem from="1757830765557" duration="1218000" />
|
||||
<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>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NSec.Cryptography" Version="25.4.0" />
|
||||
<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="System.IO.Hashing" Version="10.0.0-preview.7.25380.108" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -11,7 +11,7 @@ public class LiveController(ILogger<LiveController> logger, ResourceService rs,
|
||||
public readonly string LiveFolder = Path.Combine(config.MediaRoot, "Live");
|
||||
|
||||
[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]);
|
||||
if (d == null) return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
@@ -60,7 +60,7 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
||||
.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}")]
|
||||
|
||||
@@ -42,7 +42,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
|
||||
public IActionResult Validate(string token)
|
||||
{
|
||||
var u = _user.Validate(token, Ip);
|
||||
if (u == null)
|
||||
if (u == -1)
|
||||
{
|
||||
return StatusCode(401, new { message = "Invalid" });
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public class UserController(UserService user, ILogger<UserController> logger) :
|
||||
public IActionResult Destroy(string token)
|
||||
{
|
||||
var u = _user.Validate(token, Ip);
|
||||
if (u == null)
|
||||
if (u == -1)
|
||||
{
|
||||
return StatusCode(401, new { message = "Invalid" });
|
||||
}
|
||||
@@ -81,14 +81,14 @@ public class UserController(UserService user, ILogger<UserController> logger) :
|
||||
return StatusCode(403, new { message = "Denied" });
|
||||
|
||||
// Valid parent && Privilege
|
||||
var ou = await _user.QueryUser(_user.Validate(r, Ip) ?? "");
|
||||
if(creating.Parent != (_user.Validate(r, Ip) ?? "") || creating.Privilege > ou?.Privilege)
|
||||
var ou = await _user.QueryUser(_user.Validate(r, Ip));
|
||||
if(creating.Privilege > ou?.Privilege || ou == null)
|
||||
return StatusCode(403, new { message = "Denied" });
|
||||
|
||||
await _user.CreateUser(new User()
|
||||
await _user.CreateUser(new User
|
||||
{
|
||||
Name = creating.Name,
|
||||
Parent = _user.Validate(r, Ip) ?? "",
|
||||
Username = creating.Name,
|
||||
ParentId = ou.Uuid,
|
||||
Privilege = creating.Privilege,
|
||||
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)
|
||||
{
|
||||
var caller = _user.Validate(token, Ip);
|
||||
if (caller == null || caller != "root")
|
||||
if (caller != 1)
|
||||
{
|
||||
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 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);
|
||||
return Ok(new { token = t, user, boundIp = ipToBind });
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Abyss.Components.Static;
|
||||
using Abyss.Model;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using SQLite;
|
||||
using System.IO.Hashing;
|
||||
|
||||
@@ -21,19 +20,15 @@ public class ResourceService
|
||||
{
|
||||
private readonly ILogger<ResourceService> _logger;
|
||||
private readonly ConfigureService _config;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly UserService _user;
|
||||
private readonly SQLiteAsyncConnection _database;
|
||||
|
||||
private static readonly Regex PermissionRegex =
|
||||
new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
||||
private static readonly Regex PermissionRegex = new("^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
||||
|
||||
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
|
||||
UserService user)
|
||||
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, UserService user)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_cache = cache;
|
||||
_user = user;
|
||||
|
||||
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||
@@ -42,13 +37,13 @@ public class ResourceService
|
||||
var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
|
||||
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");
|
||||
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 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");
|
||||
return false;
|
||||
@@ -74,7 +69,7 @@ public class ResourceService
|
||||
var relPaths = new List<string>(paths.Length);
|
||||
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}");
|
||||
return false;
|
||||
@@ -84,15 +79,15 @@ public class ResourceService
|
||||
}
|
||||
|
||||
// 2. validate token and user once
|
||||
string? username = _user.Validate(token, ip);
|
||||
if (username == null)
|
||||
int uuid = _user.Validate(token, ip);
|
||||
if (uuid == -1)
|
||||
{
|
||||
_logger.LogError($"Invalid token: {token}");
|
||||
return false;
|
||||
}
|
||||
|
||||
User? user = await _user.QueryUser(username);
|
||||
if (user == null || user.Name != username)
|
||||
User? user = await _user.QueryUser(uuid);
|
||||
if (user == null || user.Uuid != uuid)
|
||||
{
|
||||
_logger.LogError($"Verification failed: {token}");
|
||||
return false;
|
||||
@@ -100,6 +95,8 @@ public class ResourceService
|
||||
|
||||
// 3. build uid -> required ops map (avoid duplicate Uid calculations)
|
||||
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)
|
||||
{
|
||||
var parts = rel
|
||||
@@ -117,6 +114,7 @@ public class ResourceService
|
||||
{
|
||||
ops = new HashSet<OperationType>();
|
||||
uidToOps[uidDir] = ops;
|
||||
uidToExampleRelPath[uidDir] = subPath;
|
||||
}
|
||||
|
||||
ops.Add(OperationType.Read);
|
||||
@@ -129,16 +127,40 @@ public class ResourceService
|
||||
{
|
||||
resOps = new HashSet<OperationType>();
|
||||
uidToOps[uidRes] = resOps;
|
||||
uidToExampleRelPath[uidRes] = resourcePath;
|
||||
}
|
||||
|
||||
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 rasList = await _database.Table<ResourceAttribute>()
|
||||
.Where(r => uidsNeeded.Contains(r.Uid))
|
||||
.ToListAsync();
|
||||
var rasList = new List<ResourceAttribute>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -148,10 +170,11 @@ public class ResourceService
|
||||
foreach (var kv in uidToOps)
|
||||
{
|
||||
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
|
||||
_logger.LogError($"Permission check failed (missing resource attribute): User: {username}, Uid: {uid}");
|
||||
var examplePath = uidToExampleRelPath.GetValueOrDefault(uid, uid);
|
||||
_logger.LogError(
|
||||
$"Permission check failed (missing resource attribute): User: {uuid}, Resource: {examplePath}, Uid: {uid}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -166,7 +189,9 @@ public class ResourceService
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -175,6 +200,7 @@ public class ResourceService
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Valid(string path, string token, OperationType type, string ip)
|
||||
{
|
||||
// Path is abs path here, due to Helpers.SafePathCombine
|
||||
@@ -183,16 +209,16 @@ public class ResourceService
|
||||
|
||||
path = Path.GetRelativePath(_config.MediaRoot, path);
|
||||
|
||||
string? username = _user.Validate(token, ip);
|
||||
if (username == null)
|
||||
int uuid = _user.Validate(token, ip);
|
||||
if (uuid == -1)
|
||||
{
|
||||
// No permission granted for invalid tokens
|
||||
_logger.LogError($"Invalid token: {token}");
|
||||
return false;
|
||||
}
|
||||
|
||||
User? user = await _user.QueryUser(username);
|
||||
if (user == null || user.Name != username)
|
||||
User? user = await _user.QueryUser(uuid);
|
||||
if (user == null || user.Uuid != uuid)
|
||||
{
|
||||
_logger.LogError($"Verification failed: {token}");
|
||||
return false; // Two-factor authentication
|
||||
@@ -205,34 +231,38 @@ public class ResourceService
|
||||
{
|
||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
var l = await CheckPermission(user, ra, type);
|
||||
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;
|
||||
@@ -250,7 +280,7 @@ public class ResourceService
|
||||
var owner = await _user.QueryUser(ra.Owner);
|
||||
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 isOther = !isOwner && !isPeer;
|
||||
|
||||
@@ -267,7 +297,7 @@ public class ResourceService
|
||||
case OperationType.Write:
|
||||
return currentPerm.Contains('w') || (user.Privilege > owner.Privilege);
|
||||
case OperationType.Security:
|
||||
return (isOwner && currentPerm.Contains('w')) || user.Name == "root";
|
||||
return (isOwner && currentPerm.Contains('w')) || user.Uuid == 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -300,17 +330,25 @@ public class ResourceService
|
||||
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
|
||||
if (_config.DebugMode == "Debug")
|
||||
goto debug;
|
||||
// 1. Authorization: Verify the operation is performed by 'root'
|
||||
var requester = _user.Validate(token, ip);
|
||||
if (requester != "root")
|
||||
if (requester != 1)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -322,10 +360,10 @@ public class ResourceService
|
||||
return false;
|
||||
}
|
||||
|
||||
var ownerUser = await _user.QueryUser(username);
|
||||
var ownerUser = await _user.QueryUser(owner);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -349,8 +387,7 @@ public class ResourceService
|
||||
newResources.Add(new ResourceAttribute
|
||||
{
|
||||
Uid = uid,
|
||||
Name = currentPath,
|
||||
Owner = username,
|
||||
Owner = owner,
|
||||
Permission = "rw,--,--"
|
||||
});
|
||||
}
|
||||
@@ -361,7 +398,7 @@ public class ResourceService
|
||||
{
|
||||
await _database.InsertAllAsync(newResources);
|
||||
_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
|
||||
{
|
||||
@@ -391,10 +428,9 @@ public class ResourceService
|
||||
public async Task<bool> Exclude(string path, string token, string ip)
|
||||
{
|
||||
var requester = _user.Validate(token, ip);
|
||||
if (requester != "root")
|
||||
if (requester != 1)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
|
||||
_logger.LogWarning($"Permission denied: Non-root user '{requester}' attempted to exclude resource '{path}'.");
|
||||
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);
|
||||
if (requester != "root")
|
||||
if (requester != 1)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -467,7 +503,6 @@ public class ResourceService
|
||||
var newResource = new ResourceAttribute
|
||||
{
|
||||
Uid = uid,
|
||||
Name = relPath,
|
||||
Owner = owner,
|
||||
Permission = permission
|
||||
};
|
||||
@@ -554,8 +589,7 @@ public class ResourceService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Chown(string path, string token, string owner, string ip)
|
||||
public async Task<bool> Chown(string path, string token, int owner, string ip)
|
||||
{
|
||||
if (!await Valid(path, token, OperationType.Security, ip))
|
||||
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))
|
||||
{
|
||||
@@ -615,7 +649,6 @@ public class ResourceService
|
||||
return await _database.InsertOrReplaceAsync(new ResourceAttribute()
|
||||
{
|
||||
Uid = Uid(path),
|
||||
Name = path,
|
||||
Owner = owner,
|
||||
Permission = permission,
|
||||
}) == 1;
|
||||
@@ -624,7 +657,6 @@ public class ResourceService
|
||||
return await _database.InsertAsync(new ResourceAttribute()
|
||||
{
|
||||
Uid = Uid(path),
|
||||
Name = path,
|
||||
Owner = owner,
|
||||
Permission = permission,
|
||||
}) == 1;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
|
||||
if(!await rs.Valid(VideoFolder, token, OperationType.Write, ip))
|
||||
return null;
|
||||
var u = user.Validate(token, ip);
|
||||
if(u == null)
|
||||
if(u == -1)
|
||||
return null;
|
||||
|
||||
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"));
|
||||
// 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,
|
||||
Owner = u,
|
||||
@@ -83,7 +83,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
|
||||
Type = TaskType.Video
|
||||
};
|
||||
|
||||
await System.IO.File.WriteAllTextAsync(
|
||||
await File.WriteAllTextAsync(
|
||||
Path.Combine(TaskFolder, r.Id.ToString(), "task.json"),
|
||||
JsonConvert.SerializeObject(v, Formatting.Indented));
|
||||
|
||||
@@ -239,7 +239,7 @@ public class TaskService(ILogger<TaskService> logger, ConfigureService config, R
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -14,22 +14,20 @@ namespace Abyss.Components.Services;
|
||||
public class UserService
|
||||
{
|
||||
private readonly ILogger<UserService> _logger;
|
||||
private readonly ConfigureService _config;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly SQLiteAsyncConnection _database;
|
||||
|
||||
public UserService(ILogger<UserService> logger, ConfigureService config, IMemoryCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_cache = cache;
|
||||
|
||||
_database = new SQLiteAsyncConnection(config.UserDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||
_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")
|
||||
_cache.Set("root", $"root@127.0.0.1", DateTimeOffset.Now.AddHours(1));
|
||||
if (config.DebugMode == "Debug")
|
||||
_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.
|
||||
|
||||
if (rootUser == null)
|
||||
@@ -50,8 +48,9 @@ public class UserService
|
||||
Console.WriteLine("key: '" + privateKeyBase64 + "'");
|
||||
_database.InsertAsync(new User()
|
||||
{
|
||||
Name = "root",
|
||||
Parent = "root",
|
||||
Uuid = 1,
|
||||
Username = "root",
|
||||
ParentId = 1,
|
||||
PublicKey = publicKeyBase64,
|
||||
Privilege = 1145141919,
|
||||
}).Wait();
|
||||
@@ -61,15 +60,16 @@ public class UserService
|
||||
}
|
||||
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
|
||||
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)));
|
||||
_cache.Set(u.Name,c, DateTimeOffset.Now.AddMinutes(1));
|
||||
_cache.Set(u.Uuid, c, DateTimeOffset.Now.AddMinutes(1));
|
||||
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
|
||||
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
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_cache.TryGetValue(u.Name, out string? challenge))
|
||||
|
||||
if (_cache.TryGetValue(u.Uuid, out string? challenge))
|
||||
{
|
||||
bool isVerified = VerifySignature(
|
||||
PublicKey.Import(
|
||||
@@ -95,16 +96,16 @@ public class UserService
|
||||
if (!isVerified)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove the challenge string and create a session
|
||||
_cache.Remove(u.Name);
|
||||
_cache.Remove(u.Uuid);
|
||||
var s = GenerateRandomAsciiString(64);
|
||||
_cache.Set(s, $"{u.Name}@{ip}", DateTimeOffset.Now.AddDays(1));
|
||||
_logger.LogInformation($"Verified {u.Name}@{ip}");
|
||||
_cache.Set(s, $"{u.Uuid}@{ip}", DateTimeOffset.Now.AddDays(1));
|
||||
_logger.LogInformation($"Verified {u.Uuid}@{ip}, Name: {u.Username}");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +113,9 @@ public class UserService
|
||||
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))
|
||||
{
|
||||
@@ -120,13 +123,13 @@ public class UserService
|
||||
{
|
||||
_logger.LogError($"Token used from another Host: {token}");
|
||||
Destroy(token);
|
||||
return null;
|
||||
return -1;
|
||||
}
|
||||
// _logger.LogInformation($"Validated {userAndIp}");
|
||||
return userAndIp?.Split('@')[0];
|
||||
return Convert.ToInt32(userAndIp?.Split('@')[0]);
|
||||
}
|
||||
_logger.LogWarning($"Validation failed {token}");
|
||||
return null;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Destroy(string token)
|
||||
@@ -134,16 +137,24 @@ public class UserService
|
||||
_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;
|
||||
}
|
||||
|
||||
public async Task CreateUser(User 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()
|
||||
@@ -195,23 +206,23 @@ public class UserService
|
||||
|
||||
if (VerifySignature(pubKey, data, signature))
|
||||
{
|
||||
_logger.LogInformation($"Signature verified using user {u.Name}");
|
||||
_logger.LogInformation($"Signature verified using user {u.Username}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
public string CreateToken(string user, string ip, TimeSpan lifetime)
|
||||
public string CreateToken(int uid, string ip, TimeSpan lifetime)
|
||||
{
|
||||
var token = GenerateRandomAsciiString(64);
|
||||
_cache.Set(token, $"{user}@{ip}", DateTimeOffset.Now.Add(lifetime));
|
||||
_logger.LogInformation($"Created token for {user}@{ip}, valid {lifetime.TotalMinutes} minutes");
|
||||
_cache.Set(token, $"{uid}@{ip}", DateTimeOffset.Now.Add(lifetime));
|
||||
_logger.LogInformation($"Created token for {uid}@{ip}, valid {lifetime.TotalMinutes} minutes");
|
||||
return token;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,6 @@ public static class Helpers
|
||||
{
|
||||
return PathType.AccessDenied;
|
||||
}
|
||||
|
||||
return PathType.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
using SQLite;
|
||||
|
||||
namespace Abyss.Model;
|
||||
|
||||
[Table("ResourceAttributes")]
|
||||
public class ResourceAttribute
|
||||
{
|
||||
public string Uid { get; set; } = "@";
|
||||
public string Name { get; set; } = "@";
|
||||
public string Owner { get; set; } = "@";
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Unique, NotNull]
|
||||
public string Uid { get; init; } = "@";
|
||||
[NotNull]
|
||||
public int Owner { get; set; }
|
||||
[NotNull]
|
||||
public string Permission { get; set; } = "--,--,--";
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public enum TaskType
|
||||
public class Task
|
||||
{
|
||||
public uint Id;
|
||||
public string Owner = "";
|
||||
public int Owner;
|
||||
public string Class = "";
|
||||
public string Name = "";
|
||||
public TaskType Type;
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using SQLite;
|
||||
|
||||
namespace Abyss.Model;
|
||||
|
||||
[Table("Users")]
|
||||
public class User
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Parent { get; set; } = "";
|
||||
[PrimaryKey, AutoIncrement]
|
||||
public int Uuid { get; set; }
|
||||
[Unique, NotNull]
|
||||
public string Username { get; set; } = "";
|
||||
[NotNull]
|
||||
public int ParentId { get; set; }
|
||||
[NotNull]
|
||||
public string PublicKey { get; set; } = "";
|
||||
[NotNull]
|
||||
public int Privilege { get; set; }
|
||||
}
|
||||
@@ -4,7 +4,6 @@ public class UserCreating
|
||||
{
|
||||
public string Response { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Parent { get; set; } = "";
|
||||
public string PublicKey { get; set; } = "";
|
||||
public int Privilege { get; set; }
|
||||
}
|
||||
@@ -38,8 +38,6 @@ public class Program
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.BuildServiceProvider().GetRequiredService<UserService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// app.UseHttpsRedirection();
|
||||
|
||||
Reference in New Issue
Block a user