summaryrefslogtreecommitdiff
path: root/Services/SecurityService.cs
blob: 2d23a58bad9468145b4acb13def77b95d1c68f86 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
using HyperBooru.Util;
using Microsoft.EntityFrameworkCore;
using System.Data;

namespace HyperBooru.Services;

public interface ISecurityService {
    public IPrincipal[]? SearchPrincipals(string name);
    public IPrincipal?   GetPrincipal(string name);
    public IUser?        GetUser(string name);
    public IGroup?       GetGroup(string name);
    public bool          ValidatePassword(IUser user, string password);

    public SecurityIdentifier? TranslateName(string name);
    public string              TranslateName(SecurityIdentifier sid);

    public IEnumerable<HBObject> Filter(
        IEnumerable<HBObject> objects,
        IPrincipal principal,
        ulong permissions);

    public IEnumerable<HBObject> Filter<T>(
        IEnumerable<HBObject> objects,
        IPrincipal principal,
        T permissions) where T : Enum;
}

public class SecurityService : ISecurityService {
    private IDbContextFactory<HBContext> dbFactory;

    private MemoryCache<SidStruct, IGroup[]>   membershipCache;
    private MemoryCache<int, Acl>              aclCache;

    IPrincipalProvider principalProvider;

    public SecurityService(
        IDbContextFactory<HBContext> dbFactory,
        IPrincipalProvider principalProvider) {

        this.dbFactory         = dbFactory;
        this.principalProvider = principalProvider;

        // TODO: preload the principal cache
        membershipCache = new() {
            MaxItems = 1000,
            MaxAge = TimeSpan.FromMinutes(10),
            DataSource = (SidStruct sid) =>
                principalProvider.GetGroups(new SecurityIdentifier(sid), true)
        };

        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.AclId == id);
            }
        };
    }

    public IPrincipal[]? SearchPrincipals(string name) =>
        principalProvider.SearchPrincipals(name);

    public IPrincipal? GetPrincipal(string name) =>
        principalProvider.GetPrincipal(name);

    public IUser? GetUser(string name) =>
        principalProvider.GetUser(name);

    public IGroup? GetGroup(string name) =>
        principalProvider.GetGroup(name);

    public bool ValidatePassword(IUser user, string password) =>
        principalProvider.ValidatePassword(user, password);

    public string TranslateName(SecurityIdentifier sid) {
        var wellKnownSid = WellKnownSid.TranslateName(sid);
        if(wellKnownSid is not null)
            return wellKnownSid;

        return sid.SddlForm;
    }

    public SecurityIdentifier? TranslateName(string name) {
        var wellKnownSid = WellKnownSid.TranslateName(name);
        if(wellKnownSid is not null)
            return wellKnownSid;

        throw new NotImplementedException();
    }

    public IEnumerable<HBObject> Filter(
        IEnumerable<HBObject> objects,
        IPrincipal principal,
        ulong permissions) {

        foreach(var obj in objects) {
            var perms = GetPermissions(obj.Acl, principal);
            if((perms & permissions) == permissions)
                yield return obj;
        }
    }

    public IEnumerable<HBObject> Filter<T>(
        IEnumerable<HBObject> objects,
        IPrincipal principal,
        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, IPrincipal principal) {
        if(acl is null)
            return ulong.MaxValue;

        ulong permissions = 0;

        var principals = membershipCache[principal.Sid.SidStruct]
            .Select(g => g.Sid)
            .Concat(new[] { principal.Sid })
            .ToArray();

        var allowRules = acl.Rules.Where(r => r.Action == AclRuleAction.Allow);
        var denyRules  = acl.Rules.Where(r => r.Action == AclRuleAction.Deny);

        foreach(var rule in allowRules) {
            if(!principals.Contains(rule.Principal))
                continue;
            permissions |= rule.Permissions;
        }

        foreach(var rule in denyRules) {
            if(!principals.Contains(rule.Principal))
                continue;
            permissions &= ~rule.Permissions;
        }

        return permissions;
    }
}