summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Services/SecurityService.cs61
-rw-r--r--Todo.md1
-rw-r--r--Util/Extensions.cs33
-rw-r--r--Util/LimitedConcurrencyTaskScheduler.cs (renamed from Util.cs)32
-rw-r--r--Util/MemoryCache.cs98
5 files changed, 174 insertions, 51 deletions
diff --git a/Services/SecurityService.cs b/Services/SecurityService.cs
index 6e5ecb8..f0ebd70 100644
--- a/Services/SecurityService.cs
+++ b/Services/SecurityService.cs
@@ -1,5 +1,5 @@
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
+using HyperBooru.Util;
+using Microsoft.EntityFrameworkCore;
using System.Data;
namespace HyperBooru.Services;
@@ -7,25 +7,34 @@ namespace HyperBooru.Services;
public class SecurityService {
private IDbContextFactory<HBContext> dbFactory;
- private Group[] groups;
- private Acl[] acls;
+ private MemoryCache<int, HBPrincipal> principalCache;
+ private MemoryCache<int, Acl> aclCache;
public SecurityService(IDbContextFactory<HBContext> dbFactory) {
this.dbFactory = dbFactory;
- Reload();
- }
-
- public void Reload() {
- using var db = dbFactory.CreateDbContext();
- groups = db.Groups
- .Include(g => g.MemberOf)
- .ToArray();
+ // TODO: preload the principal cache
+ principalCache = new() {
+ MaxItems = 10_000,
+ MaxAge = TimeSpan.FromMinutes(10),
+ DataSource = (int id) => {
+ using var db = dbFactory.CreateDbContext();
+ return db.Principals
+ .Include(p => p.MemberOf)
+ .FirstOrDefault(p => p.ObjectId == id);
+ }
+ };
- acls = db.Acls
- .Include(a => a.Rules)
- .ThenInclude(r => r.Principal)
- .ToArray();
+ aclCache = new() {
+ MaxItems = 1000,
+ MaxAge = TimeSpan.FromMinutes(10),
+ DataSource = (int id) => {
+ using var db = dbFactory.CreateDbContext();
+ return db.Acls
+ .Include(a => a.Rules)
+ .FirstOrDefault(a => a.ObjectId == id);
+ }
+ };
}
public IEnumerable<HBObject> Filter(
@@ -46,6 +55,13 @@ public class SecurityService {
T permissions) where T : Enum =>
Filter(objects, principal, permissions);
+ /// <summary>
+ /// Resolve the specified ACL and return a bitmask representing
+ /// all the permissions the specified principal has.
+ /// </summary>
+ /// <param name="acl">
+ /// ACL to resolve (returns a bitmask consisting of all 1's if this field is null)
+ /// </param>
private ulong GetPermissions(Acl? acl, HBPrincipal principal) {
if(acl is null)
return ulong.MaxValue;
@@ -75,18 +91,25 @@ public class SecurityService {
return permissions;
}
+ /// <summary>
+ /// Recursively get all groups of which the specified principal
+ /// is a member, including implicit memberships.
+ /// </summary>
private List<Group> GetGroupMemberShip(HBPrincipal principal) {
var groups = principal.MemberOf.ToList();
while(true) {
- var toAdd = this.groups
- .Where(g => !groups.Contains(g))
+ var toAdd = groups
+ .SelectMany(g => g.MemberOf)
+ .Select(g => g.ObjectId)
+ .Where(id => !groups.Select(g => g.ObjectId).Contains(id))
.ToArray();
if(toAdd.Count() == 0)
break;
- groups.AddRange(toAdd);
+ foreach(var id in toAdd)
+ groups.Add((Group) principalCache[id]);
}
return groups;
diff --git a/Todo.md b/Todo.md
index 7d334a6..f48c10a 100644
--- a/Todo.md
+++ b/Todo.md
@@ -30,3 +30,4 @@
- [ ] Journaled operations
- [ ] Confirmation dialog before enabling NSFW mode
- [ ] Upload progress bars
+ - [ ] Save user session tokens in DB
diff --git a/Util/Extensions.cs b/Util/Extensions.cs
new file mode 100644
index 0000000..bbf9a12
--- /dev/null
+++ b/Util/Extensions.cs
@@ -0,0 +1,33 @@
+namespace HyperBooru.Util;
+
+public static class Extensions {
+ public static readonly string[] MagnitudeOrders = new[] {
+ "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"
+ };
+
+ public static DateTime? TryParseDateTimeUtc(this string s) {
+ bool success = DateTime.TryParse(s, out var dateTime);
+ return success ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) : null;
+ }
+
+ public static string ToBytesSI(this long x) {
+ var exp = (int) Math.Log10(x);
+ var suffix = MagnitudeOrders.ElementAtOrDefault(exp / 3 - 1);
+ if(suffix is null)
+ return x.ToString();
+ double n = x / Math.Pow(10, exp / 3 * 3);
+ return $"{Math.Round(n, 2 - (exp % 3))} {suffix}B";
+ }
+
+ public static string ToStringHumanReadable(this TimeSpan t) {
+ if(t.TotalMilliseconds < 1000)
+ return string.Format("{0:0}ms", t.TotalMilliseconds);
+ if(t.TotalSeconds < 60)
+ return string.Format("{0:0.00}s", t.TotalSeconds);
+ if(t.TotalMinutes < 60)
+ return string.Format("{0:0}m{0:0}s", t.TotalMinutes, t.Seconds);
+ if(t.TotalHours < 24)
+ return string.Format("{0:0}h{0:0}m", t.TotalHours, t.Minutes);
+ return string.Format("{0:0.00}d", t.TotalDays);
+ }
+}
diff --git a/Util.cs b/Util/LimitedConcurrencyTaskScheduler.cs
index 6af6c81..8d5adf2 100644
--- a/Util.cs
+++ b/Util/LimitedConcurrencyTaskScheduler.cs
@@ -1,37 +1,5 @@
namespace HyperBooru.Util;
-public static class Extensions {
- public static readonly string[] MagnitudeOrders = new[] {
- "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"
- };
-
- public static DateTime? TryParseDateTimeUtc(this string s) {
- bool success = DateTime.TryParse(s, out var dateTime);
- return success ? DateTime.SpecifyKind(dateTime, DateTimeKind.Utc) : null;
- }
-
- public static string ToBytesSI(this long x) {
- var exp = (int) Math.Log10(x);
- var suffix = MagnitudeOrders.ElementAtOrDefault(exp / 3 - 1);
- if(suffix is null)
- return x.ToString();
- double n = x / Math.Pow(10, exp / 3 * 3);
- return $"{Math.Round(n, 2 - (exp % 3))} {suffix}B";
- }
-
- public static string ToStringHumanReadable(this TimeSpan t) {
- if(t.TotalMilliseconds < 1000)
- return string.Format("{0:0}ms", t.TotalMilliseconds);
- if(t.TotalSeconds < 60)
- return string.Format("{0:0.00}s", t.TotalSeconds);
- if(t.TotalMinutes < 60)
- return string.Format("{0:0}m{0:0}s", t.TotalMinutes, t.Seconds);
- if(t.TotalHours < 24)
- return string.Format("{0:0}h{0:0}m", t.TotalHours, t.Minutes);
- return string.Format("{0:0.00}d", t.TotalDays);
- }
-}
-
public class LimitedConcurrencyTaskScheduler : TaskScheduler {
public sealed override int MaximumConcurrencyLevel =>
maxConcurrency;
diff --git a/Util/MemoryCache.cs b/Util/MemoryCache.cs
new file mode 100644
index 0000000..ba314cf
--- /dev/null
+++ b/Util/MemoryCache.cs
@@ -0,0 +1,98 @@
+using System.Collections;
+
+namespace HyperBooru.Util;
+
+public class MemoryCache<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>> where TKey : struct {
+ /// <summary>Maximum number of items this cache may hold (unlimited if null)</summary>
+ public int? MaxItems { get; init; }
+ /// <summary>Maximum amount of time an item may stay in cache before it is removed (unlimited if null)</summary>
+ public TimeSpan? MaxAge { get; init; }
+ /// <summary>Function that will be called to populate the cache in the event of a cache-miss</summary>
+ public Func<TKey, TValue?>? DataSource { get; init; }
+
+ private Dictionary<TKey, (DateTime createTime, TValue value)> cache = new();
+
+ public TValue this[TKey key] {
+ get => GetValue(key);
+ set {
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ }
+ }
+
+ public TValue GetValue(TKey key) {
+ bool success = cache.TryGetValue(key, out var result);
+ if(success) {
+ if(MaxAge is null)
+ return result.value;
+ if(result.createTime > DateTime.Now - MaxAge)
+ return result.value;
+ }
+
+ if(DataSource is null)
+ throw new KeyNotFoundException();
+
+ TValue? value = DataSource(key);
+
+ if(value is null)
+ throw new KeyNotFoundException();
+
+ Prune();
+ cache[key] = (DateTime.Now, value);
+ return value;
+ }
+
+ public bool TryGetValue(TKey key, out TValue? value) {
+ try {
+ value = GetValue(key);
+ return true;
+ } catch(KeyNotFoundException) {
+ value = default;
+ return false;
+ }
+ }
+
+ public bool Remove(TKey key) => cache.Remove(key);
+
+ public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ foreach(var kv in cache) {
+ // Don't return expired cache items
+ if(expiry is not null)
+ if(kv.Value.createTime < expiry)
+ continue;
+
+ yield return new KeyValuePair<TKey, TValue>(kv.Key, kv.Value.value);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ private void Prune() {
+ DateTime? expiry = MaxAge is null ? null : DateTime.Now - MaxAge;
+
+ // If an expiry time for cache items was
+ // specified, remove expired cache items.
+ if(expiry is not null) {
+ foreach(var kv in cache) {
+ if(kv.Value.createTime < expiry)
+ cache.Remove(kv.Key);
+ }
+ }
+
+ // If this cache was created with a maximum size,
+ // remove elements until that size is reached.
+ if(MaxItems is null || cache.Count() < MaxItems)
+ return;
+
+ var toRemove = cache
+ .OrderBy(kv => kv.Value.createTime)
+ .Take(cache.Count() - (int) MaxItems + 1)
+ .Select(kv => kv.Key)
+ .ToArray();
+
+ foreach(var key in toRemove)
+ cache.Remove(key);
+ }
+}