Skip to content

Commit a7c5e97

Browse files
committed
multi: implement rule-enforcer
1 parent e806542 commit a7c5e97

File tree

2 files changed

+358
-13
lines changed

2 files changed

+358
-13
lines changed

firewall/rule_enforcer.go

Lines changed: 330 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/lightninglabs/lightning-terminal/firewalldb"
8+
"github.com/lightninglabs/lightning-terminal/perms"
79
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
10+
"github.com/lightninglabs/lightning-terminal/rules"
11+
"github.com/lightninglabs/lightning-terminal/session"
12+
"github.com/lightninglabs/lndclient"
813
"github.com/lightningnetwork/lnd/lnrpc"
14+
"google.golang.org/grpc/codes"
15+
"google.golang.org/grpc/status"
16+
"google.golang.org/protobuf/proto"
917
)
1018

1119
const (
@@ -20,6 +28,47 @@ var _ mid.RequestInterceptor = (*RuleEnforcer)(nil)
2028
// RuleEnforcer is a RequestInterceptor that makes sure all firewall related
2129
// custom caveats in a macaroon are properly enforced.
2230
type RuleEnforcer struct {
31+
ruleDB firewalldb.RulesDB
32+
actionsDB firewalldb.ActionReadDBGetter
33+
markActionErrored func(reqID uint64, reason string) error
34+
newPrivMap firewalldb.NewPrivacyMapDB
35+
36+
permsMgr *perms.Manager
37+
getFeaturePerms featurePerms
38+
39+
nodeID [33]byte
40+
41+
routerClient lndclient.RouterClient
42+
lndClient lndclient.LightningClient
43+
44+
ruleMgrs rules.ManagerSet
45+
}
46+
47+
// featurePerms defines the signature of a function that can be used to fetch
48+
// feature permissions.
49+
type featurePerms func(ctx context.Context) (map[string]map[string]bool, error)
50+
51+
// NewRuleEnforcer constructs a new RuleEnforcer instance.
52+
func NewRuleEnforcer(ruleDB firewalldb.RulesDB,
53+
actionsDB firewalldb.ActionReadDBGetter, getFeaturePerms featurePerms,
54+
permsMgr *perms.Manager, nodeID [33]byte,
55+
routerClient lndclient.RouterClient,
56+
lndClient lndclient.LightningClient, ruleMgrs rules.ManagerSet,
57+
markActionErrored func(reqID uint64, reason string) error,
58+
privMap firewalldb.NewPrivacyMapDB) *RuleEnforcer {
59+
60+
return &RuleEnforcer{
61+
ruleDB: ruleDB,
62+
actionsDB: actionsDB,
63+
permsMgr: permsMgr,
64+
getFeaturePerms: getFeaturePerms,
65+
nodeID: nodeID,
66+
routerClient: routerClient,
67+
lndClient: lndClient,
68+
ruleMgrs: ruleMgrs,
69+
markActionErrored: markActionErrored,
70+
newPrivMap: privMap,
71+
}
2372
}
2473

2574
// Name returns the name of the interceptor.
@@ -41,7 +90,7 @@ func (r *RuleEnforcer) CustomCaveatName() string {
4190

4291
// Intercept processes an RPC middleware interception request and returns the
4392
// interception result which either accepts or rejects the intercepted message.
44-
func (r *RuleEnforcer) Intercept(_ context.Context,
93+
func (r *RuleEnforcer) Intercept(ctx context.Context,
4594
req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) {
4695

4796
ri, err := NewInfoFromRequest(req)
@@ -50,15 +99,287 @@ func (r *RuleEnforcer) Intercept(_ context.Context,
5099
"interception request: %v", err)
51100
}
52101

53-
log.Infof("Enforcing rule %v", ri)
102+
if ri.Rules == nil {
103+
return mid.RPCOk(req)
104+
}
105+
106+
log.Tracef("RuleEnforcer: Intercepting %v", ri)
107+
108+
if ri.MetaInfo == nil {
109+
return mid.RPCErrString(req, "missing MetaInfo")
110+
}
111+
112+
// Ensure that the specified feature name is one listed in the macaroon.
113+
featureName := ri.MetaInfo.Feature
114+
_, ok := ri.Rules.FeatureRules[featureName]
115+
if len(ri.Rules.FeatureRules) != 0 && !ok {
116+
return mid.RPCErrString(req, "feature %s does not correspond "+
117+
"to a feature specified in the macaroon caveat",
118+
featureName)
119+
}
120+
121+
// Ensure that the feature specified in the MetaInfo is one that we
122+
// know about from our last interaction with the Autopilot server.
123+
featurePerms, err := r.getFeaturePerms(ctx)
124+
if err != nil {
125+
return mid.RPCErrString(req, "unable to get feature "+
126+
"permissions")
127+
}
128+
129+
perms, ok := featurePerms[featureName]
130+
if !ok {
131+
return mid.RPCErrString(req, "feature %s is not a known "+
132+
"feature", featureName)
133+
}
134+
135+
// Then check that this URI is allowed given the list of perms the
136+
// Autopilot told us this feature could use.
137+
if !perms[ri.URI] {
138+
return mid.RPCErrString(req, "Method %s is not allowed for "+
139+
"feature %s", ri.URI, featureName)
140+
}
141+
142+
switch ri.MWRequestType {
143+
case MWRequestTypeStreamAuth:
144+
return mid.RPCOk(req)
145+
146+
// Parse incoming requests and act on them.
147+
case MWRequestTypeRequest:
148+
replacement, err := r.handleRequest(ctx, ri)
149+
if err != nil {
150+
dbErr := r.markActionErrored(ri.RequestID, err.Error())
151+
if dbErr != nil {
152+
log.Error("could not mark action for "+
153+
"request ID %d as Errored: %v",
154+
ri.RequestID, dbErr)
155+
}
156+
157+
return mid.RPCErr(req, err)
158+
}
159+
160+
// No error occurred but the request should be replaced with
161+
// the given custom request. Wrap it in the correct RPC
162+
// request of the interceptor now.
163+
if replacement != nil {
164+
return mid.RPCReplacement(req, replacement)
165+
}
166+
167+
// No error and no replacement, just return an empty request of
168+
// the correct type.
169+
return mid.RPCOk(req)
170+
171+
// Parse and possibly manipulate outgoing responses.
172+
case MWRequestTypeResponse:
173+
if ri.IsError {
174+
replacementErr, err := r.handleErrorResponse(ctx, ri)
175+
if err != nil {
176+
return mid.RPCErr(req, err)
177+
}
178+
179+
// No error occurred but the response error should be
180+
// replaced with the given custom error. Wrap it in the
181+
// correct RPC response of the interceptor now.
182+
if replacementErr != nil {
183+
return mid.RPCErrReplacement(
184+
req, replacementErr,
185+
)
186+
}
187+
188+
// No error and no replacement, just return an empty
189+
// response of the correct type.
190+
return mid.RPCOk(req)
191+
}
192+
193+
replacement, err := r.handleResponse(ctx, ri)
194+
if err != nil {
195+
return mid.RPCErr(req, err)
196+
}
197+
198+
// No error occurred but the response should be replaced with
199+
// the given custom response. Wrap it in the correct RPC
200+
// response of the interceptor now.
201+
if replacement != nil {
202+
return mid.RPCReplacement(req, replacement)
203+
}
204+
205+
// No error and no replacement, just return an empty response of
206+
// the correct type.
207+
return mid.RPCOk(req)
208+
209+
default:
210+
return mid.RPCErrString(req, "invalid intercept type: %v", r)
211+
}
212+
}
213+
214+
// handleRequest gathers the rules that will need to enforced for the given
215+
// feature and runs the request against each of those.
216+
func (r *RuleEnforcer) handleRequest(ctx context.Context,
217+
ri *RequestInfo) (proto.Message, error) {
218+
219+
sessionID, err := session.IDFromMacaroon(ri.Macaroon)
220+
if err != nil {
221+
return nil, fmt.Errorf("could not extract ID from macaroon")
222+
}
223+
224+
rules, err := r.collectEnforcers(ri, sessionID)
225+
if err != nil {
226+
return nil, fmt.Errorf("error parsing rules: %v", err)
227+
}
228+
229+
msg, err := mid.ParseProtobuf(
230+
ri.GRPCMessageType, ri.Serialized,
231+
)
232+
if err != nil {
233+
return nil, fmt.Errorf("error parsing proto: %v", err)
234+
}
235+
236+
for _, rule := range rules {
237+
newRequest, err := rule.HandleRequest(ctx, ri.URI, msg)
238+
if err != nil {
239+
st := status.Errorf(
240+
codes.ResourceExhausted, "rule violation: %v",
241+
err,
242+
)
243+
return nil, st
244+
}
245+
246+
if newRequest != nil {
247+
msg = newRequest
248+
}
249+
}
250+
251+
return nil, nil
252+
}
253+
254+
// handleResponse gathers the rules that will need to be enforced for the given
255+
// feature and runs the response against each of those.
256+
func (r *RuleEnforcer) handleResponse(ctx context.Context,
257+
ri *RequestInfo) (proto.Message, error) {
258+
259+
sessionID, err := session.IDFromMacaroon(ri.Macaroon)
260+
if err != nil {
261+
return nil, fmt.Errorf("could not extract ID from macaroon")
262+
}
263+
264+
enforcers, err := r.collectEnforcers(ri, sessionID)
265+
if err != nil {
266+
return nil, fmt.Errorf("error parsing rules: %v", err)
267+
}
268+
269+
msg, err := mid.ParseProtobuf(ri.GRPCMessageType, ri.Serialized)
270+
if err != nil {
271+
return nil, fmt.Errorf("error parsing proto: %v", err)
272+
}
273+
274+
for _, enforcer := range enforcers {
275+
newResponse, err := enforcer.HandleResponse(ctx, ri.URI, msg)
276+
if err != nil {
277+
return nil, err
278+
}
279+
280+
if newResponse != nil {
281+
msg = newResponse
282+
}
283+
}
284+
285+
return msg, nil
286+
}
287+
288+
// handleErrorResponse gathers the rules that will need to be enforced for the
289+
// given feature and runs the response error against each of those.
290+
func (r *RuleEnforcer) handleErrorResponse(ctx context.Context,
291+
ri *RequestInfo) (error, error) {
292+
293+
sessionID, err := session.IDFromMacaroon(ri.Macaroon)
294+
if err != nil {
295+
return nil, fmt.Errorf("could not extract ID from macaroon")
296+
}
297+
298+
enforcers, err := r.collectEnforcers(ri, sessionID)
299+
if err != nil {
300+
return nil, fmt.Errorf("error parsing rules: %v", err)
301+
}
302+
303+
parsedErr := mid.ParseResponseErr(ri.Serialized)
304+
305+
for _, enforcer := range enforcers {
306+
newErr, err := enforcer.HandleErrorResponse(
307+
ctx, ri.URI, parsedErr,
308+
)
309+
if err != nil {
310+
return nil, err
311+
}
312+
313+
if newErr != nil {
314+
parsedErr = newErr
315+
}
316+
}
317+
318+
return parsedErr, nil
319+
}
320+
321+
// collectRule initialises and returns all the Rules that need to be enforced
322+
// for the given request.
323+
func (r *RuleEnforcer) collectEnforcers(ri *RequestInfo, sessionID session.ID) (
324+
[]rules.Enforcer, error) {
325+
326+
ruleEnforcers := make(
327+
[]rules.Enforcer, 0,
328+
len(ri.Rules.FeatureRules)+len(ri.Rules.SessionRules),
329+
)
330+
331+
for rule, value := range ri.Rules.FeatureRules[ri.MetaInfo.Feature] {
332+
r, err := r.initRule(
333+
ri.RequestID, rule, []byte(value), ri.MetaInfo.Feature,
334+
sessionID, false, ri.WithPrivacy,
335+
)
336+
if err != nil {
337+
return nil, err
338+
}
339+
340+
ruleEnforcers = append(ruleEnforcers, r)
341+
}
342+
343+
return ruleEnforcers, nil
344+
}
345+
346+
// initRule initialises a rule.Rule with any required config values.
347+
func (r *RuleEnforcer) initRule(reqID uint64, name string, value []byte,
348+
featureName string, sessionID session.ID, sessionRule,
349+
privacy bool) (rules.Enforcer, error) {
350+
351+
ruleValues, err := r.ruleMgrs.InitRuleValues(name, value)
352+
if err != nil {
353+
return nil, err
354+
}
355+
356+
if privacy {
357+
privMap := r.newPrivMap(sessionID)
358+
ruleValues, err = ruleValues.PseudoToReal(privMap)
359+
if err != nil {
360+
return nil, fmt.Errorf("could not prepare rule "+
361+
"value: %v", err)
362+
}
363+
}
364+
365+
allActionsDB := r.actionsDB.GetActionsReadDB(sessionID, featureName)
366+
actionsDB := allActionsDB.FeatureActionsDB()
367+
rulesDB := r.ruleDB.GetKVStores(name, sessionID, featureName)
368+
369+
if sessionRule {
370+
actionsDB = allActionsDB.SessionActionsDB()
371+
rulesDB = r.ruleDB.GetKVStores(name, sessionID, "")
372+
}
54373

55-
// Enforce actual rules.
56-
numRules := len(ri.Rules.SessionRules) + len(ri.Rules.FeatureRules)
57-
if numRules > 0 {
58-
// TODO(guggero): Implement rules and their enforcement.
59-
log.Debugf("There are %d rules to enforce", numRules)
374+
cfg := &rules.ConfigImpl{
375+
Stores: rulesDB,
376+
ActionsDB: actionsDB,
377+
MethodPerms: r.permsMgr.URIPermissions,
378+
NodeID: r.nodeID,
379+
RouterClient: r.routerClient,
380+
LndClient: r.lndClient,
381+
ReqID: int64(reqID),
60382
}
61383

62-
// Send empty response, accepting the request.
63-
return mid.RPCOk(req)
384+
return r.ruleMgrs.InitEnforcer(cfg, name, ruleValues)
64385
}

0 commit comments

Comments
 (0)