-
Notifications
You must be signed in to change notification settings - Fork 1
/
ACL.hs
166 lines (136 loc) · 4.84 KB
/
ACL.hs
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
{-# LANGUAGE OverloadedStrings #-}
module ACL where
import Data.Maybe (fromMaybe)
import Data.Monoid (First(First, getFirst))
import Data.Semigroup ((<>))
import Data.Text (Text, unpack)
-- | An authentication token (simple placeholder for this POC).
--
data AuthenticationToken = AuthenticationToken
{ tokenUsername :: Username
, tokenGroups :: [Group]
, tokenIPAddress :: String -- FIXME better type
}
type Username = Text
type Group = Text
-- | An AccessEvaluator
data AccessEvaluator = AccessEvaluator
{ evaluateAccess :: AuthenticationToken -> Bool
, printAccessEvaluator :: Text -- ^ string representation
}
instance Show AccessEvaluator where
show = unpack . printAccessEvaluator
-- | An ACL expression is left-associative with no operator
-- precedence. In other words:
--
-- @
-- a || b && c || d == (((a || b) && c) || d)
-- @
--
-- This can give different results from the "usual" precedence
-- of AND and OR. The old Java implementation has this semantics.
--
-- Note that the construction is right-associative to make parsing
-- easy, but 'evaluateExpression' evaluates the structure
-- left-associatively.
--
data ACLExpression
= End AccessEvaluator
| And AccessEvaluator ACLExpression
| Or AccessEvaluator ACLExpression
deriving (Show)
-- | Evaluate expression left-associatively.
-- (The implementation is a bit convoluted for this reason)
--
evaluateExpression :: AuthenticationToken -> ACLExpression -> Bool
evaluateExpression tok = go id{-use the first fragment as-is-}
where
go f (End l) = f (evaluateAccess l tok)
go f (Or l r) = go (f (evaluateAccess l tok) ||) r
go f (And l r) = go (f (evaluateAccess l tok) &&) r
type Permission = Text
data ACLRuleType = Allow | Deny
deriving (Eq, Show)
data ACLRule = ACLRule
{ aclRuleType :: ACLRuleType
, aclRulePermissions :: [Permission]
, aclRuleExpression :: ACLExpression
}
deriving (Show)
-- | Evaluate a rule. An @Allow@ rule will evaluate to
-- @Just Allowed@ if its expression evaluate to @True@s, else
-- @Nothing@. Similarly, a @Deny@ rule will evaluate to
-- @Just Denied@ or @Nothing@.
--
evaluateRule :: AuthenticationToken -> ACLRule -> Maybe ACLResult
evaluateRule tok (ACLRule ruleType _ expr) =
if evaluateExpression tok expr then Just (result ruleType) else Nothing
where
result Deny = Denied
result Allow = Allowed
-- | Specifies whether "allow" or "deny" rules will be processed first
data ACLRuleOrder = AllowDeny | DenyAllow
-- | Result of evaluating an ACL
data ACLResult = Allowed | Denied
data ACL = ACL
{ aclName :: Text
, aclPermissions :: [Permission]
-- ^ the union of the permissions of each acl entry (hypothetically)
, aclRules :: [ACLRule]
, aclDescription :: Text
}
deriving (Show)
-- | Evaluate an ACL for the given 'Permission'. The 'ACLRuleOrder'
-- controls whether 'Deny' rules will be processed before 'Allow'
-- rules or vice-versa. The first matching rule covering the given
-- 'Permission' wins. If no rule matches for the given permission,
-- access is 'Denied'.
--
evaluateACL
:: ACLRuleOrder
-> AuthenticationToken
-> Permission
-> ACL
-> ACLResult
evaluateACL order tok perm (ACL _ _ rules _ ) =
fromMaybe Denied result -- deny if no rules matched
where
-- rules for the given permissions
permRules = filter (elem perm . aclRulePermissions) rules
-- order rules by allow/deny according to ACLRuleOrder
orderedRules = case order of
DenyAllow -> denyRules <> allowRules
AllowDeny -> allowRules <> denyRules
denyRules = filter ((== Deny) . aclRuleType) permRules
allowRules = filter ((== Allow) . aclRuleType) permRules
-- the first matching rule wins
result = getFirst (foldMap (First . evaluateRule tok) orderedRules)
data EqCondition = Equal | NotEqual
deriving (Eq)
eq :: (Eq a) => EqCondition -> (a -> a -> Bool)
eq Equal = (==)
eq NotEqual = (/=)
printEq :: EqCondition -> Text
printEq Equal = "="
printEq NotEqual = "!="
-- | Constract a user access evaluator
--
userAccessEvaluator :: EqCondition -> Text -> AccessEvaluator
userAccessEvaluator eqCond s = AccessEvaluator f repr
where
f | eqCond == Equal && (s `elem` ["anybody", "everybody"]) = const True
| otherwise = eq eqCond s . tokenUsername
repr = "user" <> printEq eqCond <> "\"" <> s <> "\""
groupAccessEvaluator :: EqCondition -> Text -> AccessEvaluator
groupAccessEvaluator eqCond s = AccessEvaluator f repr
where
f = any (eq eqCond s) . tokenGroups
repr = "group" <> printEq eqCond <> "\"" <> s <> "\""
-- | extract IP address from auth token and perform regex match
ipaddressAccessEvaluator :: EqCondition -> Text -> AccessEvaluator
ipaddressAccessEvaluator eqCond pat = AccessEvaluator f repr
where
op = case eqCond of Equal -> id ; NotEqual -> not
regexMatch = undefined pat -- TODO
f tok = op (regexMatch (tokenIPAddress tok))
repr = "ipaddress" <> printEq eqCond <> "\"" <> pat <> "\""