using HyperBooru.Util; using Microsoft.EntityFrameworkCore; using System.Data; namespace HyperBooru.Services; public class SecurityService { private IDbContextFactory dbFactory; private MemoryCache principalCache; private MemoryCache aclCache; public SecurityService(IDbContextFactory dbFactory) { this.dbFactory = dbFactory; // 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); } }; 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 Filter( IEnumerable objects, HBPrincipal principal, ulong permissions) { foreach(var obj in objects) { var perms = GetPermissions(obj.Acl, principal); if((perms & permissions) == permissions) yield return obj; } } public IEnumerable Filter( IEnumerable objects, HBPrincipal principal, T permissions) where T : Enum => Filter(objects, principal, permissions); /// /// Resolve the specified ACL and return a bitmask representing /// all the permissions the specified principal has. /// /// /// ACL to resolve (returns a bitmask consisting of all 1's if this field is null) /// private ulong GetPermissions(Acl? acl, HBPrincipal principal) { if(acl is null) return ulong.MaxValue; bool hasAllowRules = acl.Rules .Any(r => r.Action == AclRuleAction.Allow); ulong permissions = hasAllowRules ? 0 : ulong.MaxValue; var principals = GetGroupMemberShip(principal) .Cast() .Concat(new[] { principal }) .ToArray(); acl.Rules.IntersectBy(principals, r => r.Principal); foreach(var rule in acl.Rules) { if(!principals.Contains(rule.Principal)) continue; if(rule.Action == AclRuleAction.Allow) permissions |= rule.Permissions; else permissions &= ~rule.Permissions; } return permissions; } /// /// Recursively get all groups of which the specified principal /// is a member, including implicit memberships. /// private List GetGroupMemberShip(HBPrincipal principal) { var groups = principal.MemberOf.ToList(); while(true) { 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; foreach(var id in toAdd) groups.Add((Group) principalCache[id]); } return groups; } }