[fix] Bulk query permission
This commit is contained in:
9
.idea/.idea.Abyss/.idea/workspace.xml
generated
9
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -13,8 +13,8 @@
|
||||
<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$/Abyss/Components/Controllers/Media/ImageController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.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/Services/AbyssService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/AbyssService.cs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Tools/AbyssStream.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Tools/AbyssStream.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/Properties/launchSettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -99,7 +99,7 @@
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager" selected="Publish to folder.Publish Abyss to folder">
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.Abyss: http">
|
||||
<configuration name="Publish Abyss to folder x86" type="DotNetFolderPublish" factoryName="Publish to folder">
|
||||
<riderPublish configuration="Release" platform="Any CPU" produce_single_file="true" ready_to_run="true" self_contained="true" target_folder="/opt/security/https/server" target_framework="net9.0" uuid_high="3690631506471504162" uuid_low="-4858628519588143325">
|
||||
<runtimes>
|
||||
@@ -206,7 +206,8 @@
|
||||
<workItem from="1757694833696" duration="11000" />
|
||||
<workItem from="1757695721386" duration="749000" />
|
||||
<workItem from="1757702942841" duration="32000" />
|
||||
<workItem from="1757735249561" duration="4543000" />
|
||||
<workItem from="1757735249561" duration="5523000" />
|
||||
<workItem from="1757742881713" duration="2180000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -55,18 +55,16 @@ public class ImageController(ILogger<ImageController> logger, ResourceService rs
|
||||
|
||||
var db = id.Select(x => Helpers.SafePathCombine(ImageFolder, [x, "summary.json"])).ToArray();
|
||||
if (db.Any(x => x == null))
|
||||
return StatusCode(403, new { message = "403 Denied" });
|
||||
return BadRequest();
|
||||
|
||||
var rb = db.Select(x => rs.Get(x!, token, Ip)).ToArray();
|
||||
bool[] results = await Task.WhenAll(rb);
|
||||
|
||||
if(results.Any(x => !x))
|
||||
if(!await rs.GetAll(db!, token, Ip))
|
||||
return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
var rc = db.Select(x => System.IO.File.ReadAllTextAsync(x!)).ToArray();
|
||||
string[] rcs = await Task.WhenAll(rc);
|
||||
var rjs = rcs.Select(JsonConvert.DeserializeObject<Comic>).Select(x => x!).ToArray();
|
||||
|
||||
return Ok(rcs);
|
||||
return Ok(JsonConvert.SerializeObject(rjs));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/bookmark")]
|
||||
|
||||
@@ -84,16 +84,14 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
||||
if(db.Any(x => x == null))
|
||||
return BadRequest();
|
||||
|
||||
var rb = db.Select(x => rs.Get(x!, token, Ip)).ToArray();
|
||||
bool[] results = await Task.WhenAll(rb);
|
||||
|
||||
if(results.Any(x => !x))
|
||||
if(!await rs.GetAll(db!, token, Ip))
|
||||
return StatusCode(403, new { message = "403 Denied" });
|
||||
|
||||
var rc = db.Select(x => System.IO.File.ReadAllTextAsync(x!)).ToArray();
|
||||
string[] rcs = await Task.WhenAll(rc);
|
||||
var rjs = rcs.Select(JsonConvert.DeserializeObject<Video>).Select(x => x!).ToList();
|
||||
|
||||
return Ok(rcs);
|
||||
return Ok(JsonConvert.SerializeObject(rjs));
|
||||
}
|
||||
|
||||
[HttpGet("{klass}/{id}/cover")]
|
||||
|
||||
@@ -60,6 +60,121 @@ public class ResourceService
|
||||
return Convert.ToBase64String(r ?? []);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidAll(string[] paths, string token, OperationType type, string ip)
|
||||
{
|
||||
if (paths == null || paths.Length == 0)
|
||||
{
|
||||
_logger.LogError("ValidAll called with empty path set");
|
||||
return false;
|
||||
}
|
||||
|
||||
var mediaRootFull = Path.GetFullPath(_config.MediaRoot);
|
||||
|
||||
// 1. basic path checks & normalize to relative
|
||||
var relPaths = new List<string>(paths.Length);
|
||||
foreach (var p in paths)
|
||||
{
|
||||
if (p == null || !p.StartsWith(mediaRootFull, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError($"Path outside media root or null: {p}");
|
||||
return false;
|
||||
}
|
||||
|
||||
relPaths.Add(Path.GetRelativePath(_config.MediaRoot, Path.GetFullPath(p)));
|
||||
}
|
||||
|
||||
// 2. validate token and user once
|
||||
string? username = _user.Validate(token, ip);
|
||||
if (username == null)
|
||||
{
|
||||
_logger.LogError($"Invalid token: {token}");
|
||||
return false;
|
||||
}
|
||||
|
||||
User? user = await _user.QueryUser(username);
|
||||
if (user == null || user.Name != username)
|
||||
{
|
||||
_logger.LogError($"Verification failed: {token}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. build uid -> required ops map (avoid duplicate Uid calculations)
|
||||
var uidToOps = new Dictionary<string, HashSet<OperationType>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var rel in relPaths)
|
||||
{
|
||||
var parts = rel
|
||||
.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar },
|
||||
StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToArray();
|
||||
|
||||
// parents (each prefix) require Read
|
||||
for (int i = 0; i < parts.Length - 1; i++)
|
||||
{
|
||||
var subPath = Path.Combine(parts.Take(i + 1).ToArray());
|
||||
var uidDir = Uid(subPath);
|
||||
if (!uidToOps.TryGetValue(uidDir, out var ops))
|
||||
{
|
||||
ops = new HashSet<OperationType>();
|
||||
uidToOps[uidDir] = ops;
|
||||
}
|
||||
|
||||
ops.Add(OperationType.Read);
|
||||
}
|
||||
|
||||
// resource itself requires requested 'type'
|
||||
var resourcePath = (parts.Length == 0) ? string.Empty : Path.Combine(parts);
|
||||
var uidRes = Uid(resourcePath);
|
||||
if (!uidToOps.TryGetValue(uidRes, out var resOps))
|
||||
{
|
||||
resOps = new HashSet<OperationType>();
|
||||
uidToOps[uidRes] = resOps;
|
||||
}
|
||||
|
||||
resOps.Add(type);
|
||||
}
|
||||
|
||||
// 4. batch query DB for all UIDs
|
||||
var uidsNeeded = uidToOps.Keys.ToList();
|
||||
var rasList = await _database.Table<ResourceAttribute>()
|
||||
.Where(r => uidsNeeded.Contains(r.Uid))
|
||||
.ToListAsync();
|
||||
|
||||
var raDict = rasList.ToDictionary(r => r.Uid, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 5. check each uid once per required operation (cache results per uid+op)
|
||||
var permCache = new Dictionary<(string uid, OperationType op), bool>(); // avoid repeated CheckPermission
|
||||
|
||||
foreach (var kv in uidToOps)
|
||||
{
|
||||
var uid = kv.Key;
|
||||
if (!raDict.TryGetValue(uid, out var ra) || ra == null)
|
||||
{
|
||||
// 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}");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var op in kv.Value)
|
||||
{
|
||||
var key = (uid, op);
|
||||
if (!permCache.TryGetValue(key, out var ok))
|
||||
{
|
||||
ok = await CheckPermission(user, ra, op);
|
||||
permCache[key] = ok;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
_logger.LogError($"Permission check failed: User: {username}, Uid: {uid}, Type: {op}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> Valid(string path, string token, OperationType type, string ip)
|
||||
{
|
||||
// Path is abs path here, due to Helpers.SafePathCombine
|
||||
@@ -175,6 +290,11 @@ public class ResourceService
|
||||
return await Valid(path, token, OperationType.Read, ip);
|
||||
}
|
||||
|
||||
public async Task<bool> GetAll(string[] path, string token, string ip)
|
||||
{
|
||||
return await ValidAll(path, token, OperationType.Read, ip);
|
||||
}
|
||||
|
||||
public async Task<bool> Update(string path, string token, string ip)
|
||||
{
|
||||
return await Valid(path, token, OperationType.Write, ip);
|
||||
@@ -273,7 +393,8 @@ public class ResourceService
|
||||
var requester = _user.Validate(token, ip);
|
||||
if (requester != "root")
|
||||
{
|
||||
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
|
||||
_logger.LogWarning(
|
||||
$"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to exclude resource '{path}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"MEDIA_ROOT" : "/storage",
|
||||
"ALLOWED_PORTS" : "3000"
|
||||
"ALLOWED_PORTS" : "3000",
|
||||
"DEBUG_MODE": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user