[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">
<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>

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

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