• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 12312390362

13 Dec 2024 08:44AM UTC coverage: 57.458% (+8.5%) from 48.92%
12312390362

Pull #9343

github

ellemouton
fn: rework the ContextGuard and add tests

In this commit, the ContextGuard struct is re-worked such that the
context that its new main WithCtx method provides is cancelled in sync
with a parent context being cancelled or with it's quit channel being
cancelled. Tests are added to assert the behaviour. In order for the
close of the quit channel to be consistent with the cancelling of the
derived context, the quit channel _must_ be contained internal to the
ContextGuard so that callers are only able to close the channel via the
exposed Quit method which will then take care to first cancel any
derived context that depend on the quit channel before returning.
Pull Request #9343: fn: expand the ContextGuard and add tests

101853 of 177264 relevant lines covered (57.46%)

24972.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.81
/macaroons/constraints.go
1
package macaroons
2

3
import (
4
        "bytes"
5
        "context"
6
        "fmt"
7
        "net"
8
        "strings"
9
        "time"
10

11
        "google.golang.org/grpc/peer"
12
        "gopkg.in/macaroon-bakery.v2/bakery/checkers"
13
        macaroon "gopkg.in/macaroon.v2"
14
)
15

16
const (
17
        // CondLndCustom is the first party caveat condition name that is used
18
        // for all custom caveats in lnd. Every custom caveat entry will be
19
        // encoded as the string
20
        // "lnd-custom <custom-caveat-name> <custom-caveat-condition>"
21
        // in the serialized macaroon. We choose a single space as the delimiter
22
        // between the because that is also used by the macaroon bakery library.
23
        CondLndCustom = "lnd-custom"
24
)
25

26
// CustomCaveatAcceptor is an interface that contains a single method for
27
// checking whether a macaroon with the given custom caveat name should be
28
// accepted or not.
29
type CustomCaveatAcceptor interface {
30
        // CustomCaveatSupported returns nil if a macaroon with the given custom
31
        // caveat name can be validated by any component in lnd (for example an
32
        // RPC middleware). If no component is registered to handle the given
33
        // custom caveat then an error must be returned. This method only checks
34
        // the availability of a validating component, not the validity of the
35
        // macaroon itself.
36
        CustomCaveatSupported(customCaveatName string) error
37
}
38

39
// Constraint type adds a layer of indirection over macaroon caveats.
40
type Constraint func(*macaroon.Macaroon) error
41

42
// Checker type adds a layer of indirection over macaroon checkers. A Checker
43
// returns the name of the checker and the checker function; these are used to
44
// register the function with the bakery service's compound checker.
45
type Checker func() (string, checkers.Func)
46

47
// AddConstraints returns new derived macaroon by applying every passed
48
// constraint and tightening its restrictions.
49
func AddConstraints(mac *macaroon.Macaroon,
50
        cs ...Constraint) (*macaroon.Macaroon, error) {
1✔
51

1✔
52
        // The macaroon library's Clone() method has a subtle bug that doesn't
1✔
53
        // correctly clone all caveats. We need to use our own, safe clone
1✔
54
        // function instead.
1✔
55
        newMac, err := SafeCopyMacaroon(mac)
1✔
56
        if err != nil {
1✔
57
                return nil, err
×
58
        }
×
59

60
        for _, constraint := range cs {
2✔
61
                if err := constraint(newMac); err != nil {
1✔
62
                        return nil, err
×
63
                }
×
64
        }
65
        return newMac, nil
1✔
66
}
67

68
// Each *Constraint function is a functional option, which takes a pointer
69
// to the macaroon and adds another restriction to it. For each *Constraint,
70
// the corresponding *Checker is provided if not provided by default.
71

72
// TimeoutConstraint restricts the lifetime of the macaroon
73
// to the amount of seconds given.
74
func TimeoutConstraint(seconds int64) func(*macaroon.Macaroon) error {
3✔
75
        return func(mac *macaroon.Macaroon) error {
6✔
76
                macaroonTimeout := time.Duration(seconds)
3✔
77
                requestTimeout := time.Now().Add(time.Second * macaroonTimeout)
3✔
78
                caveat := checkers.TimeBeforeCaveat(requestTimeout)
3✔
79
                return mac.AddFirstPartyCaveat([]byte(caveat.Condition))
3✔
80
        }
3✔
81
}
82

83
// IPLockConstraint locks macaroon to a specific IP address.
84
// If address is an empty string, this constraint does nothing to
85
// accommodate default value's desired behavior.
86
func IPLockConstraint(ipAddr string) func(*macaroon.Macaroon) error {
2✔
87
        return func(mac *macaroon.Macaroon) error {
4✔
88
                if ipAddr != "" {
4✔
89
                        macaroonIPAddr := net.ParseIP(ipAddr)
2✔
90
                        if macaroonIPAddr == nil {
3✔
91
                                return fmt.Errorf("incorrect macaroon IP-" +
1✔
92
                                        "lock address")
1✔
93
                        }
1✔
94
                        caveat := checkers.Condition("ipaddr",
1✔
95
                                macaroonIPAddr.String())
1✔
96
                        return mac.AddFirstPartyCaveat([]byte(caveat))
1✔
97
                }
98
                return nil
×
99
        }
100
}
101

102
// IPLockChecker accepts client IP from the validation context and compares it
103
// with IP locked in the macaroon. It is of the `Checker` type.
104
func IPLockChecker() (string, checkers.Func) {
5✔
105
        return "ipaddr", func(ctx context.Context, cond, arg string) error {
5✔
106
                // Get peer info and extract IP address from it for macaroon
×
107
                // check.
×
108
                pr, ok := peer.FromContext(ctx)
×
109
                if !ok {
×
110
                        return fmt.Errorf("unable to get peer info from context")
×
111
                }
×
112
                peerAddr, _, err := net.SplitHostPort(pr.Addr.String())
×
113
                if err != nil {
×
114
                        return fmt.Errorf("unable to parse peer address")
×
115
                }
×
116

117
                if !net.ParseIP(arg).Equal(net.ParseIP(peerAddr)) {
×
118
                        msg := "macaroon locked to different IP address"
×
119
                        return fmt.Errorf(msg)
×
120
                }
×
121
                return nil
×
122
        }
123
}
124

125
// CustomConstraint returns a function that adds a custom caveat condition to
126
// a macaroon.
127
func CustomConstraint(name, condition string) func(*macaroon.Macaroon) error {
2✔
128
        return func(mac *macaroon.Macaroon) error {
4✔
129
                // We rely on a name being set for the interception, so don't
2✔
130
                // allow creating a caveat without a name in the first place.
2✔
131
                if name == "" {
2✔
132
                        return fmt.Errorf("name cannot be empty")
×
133
                }
×
134

135
                // The inner (custom) condition is optional.
136
                outerCondition := fmt.Sprintf("%s %s", name, condition)
2✔
137
                if condition == "" {
3✔
138
                        outerCondition = name
1✔
139
                }
1✔
140

141
                caveat := checkers.Condition(CondLndCustom, outerCondition)
2✔
142
                return mac.AddFirstPartyCaveat([]byte(caveat))
2✔
143
        }
144
}
145

146
// CustomChecker returns a Checker function that is used by the macaroon bakery
147
// library to check whether a custom caveat is supported by lnd in general or
148
// not. Support in this context means: An additional gRPC interceptor was set up
149
// that validates the content (=condition) of the custom caveat. If such an
150
// interceptor is in place then the acceptor should return a nil error. If no
151
// interceptor exists for the custom caveat in the macaroon of a request context
152
// then a non-nil error should be returned and the macaroon is rejected as a
153
// whole.
154
func CustomChecker(acceptor CustomCaveatAcceptor) Checker {
×
155
        // We return the general name of all lnd custom macaroons and a function
×
156
        // that splits the outer condition to extract the name of the custom
×
157
        // condition and the condition itself. In the bakery library that's used
×
158
        // here, a caveat always has the following form:
×
159
        //
×
160
        // <condition-name> <condition-value>
×
161
        //
×
162
        // Because a checker function needs to be bound to the condition name we
×
163
        // have to choose a static name for the first part ("lnd-custom", see
×
164
        // CondLndCustom. Otherwise we'd need to register a new Checker function
×
165
        // for each custom caveat that's registered. To allow for a generic
×
166
        // custom caveat handling, we just add another layer and expand the
×
167
        // initial <condition-value> into
×
168
        //
×
169
        // "<custom-condition-name> <custom-condition-value>"
×
170
        //
×
171
        // The full caveat string entry of a macaroon that uses this generic
×
172
        // mechanism would therefore look like this:
×
173
        //
×
174
        // "lnd-custom <custom-condition-name> <custom-condition-value>"
×
175
        checker := func(_ context.Context, _, outerCondition string) error {
×
176
                if outerCondition != strings.TrimSpace(outerCondition) {
×
177
                        return fmt.Errorf("unexpected white space found in " +
×
178
                                "caveat condition")
×
179
                }
×
180
                if outerCondition == "" {
×
181
                        return fmt.Errorf("expected custom caveat, got empty " +
×
182
                                "string")
×
183
                }
×
184

185
                // The condition part of the original caveat is now name and
186
                // condition of the custom caveat (we add a layer of conditions
187
                // to allow one custom checker to work for all custom lnd
188
                // conditions that implement arbitrary business logic).
189
                parts := strings.Split(outerCondition, " ")
×
190
                customCaveatName := parts[0]
×
191

×
192
                return acceptor.CustomCaveatSupported(customCaveatName)
×
193
        }
194

195
        return func() (string, checkers.Func) {
×
196
                return CondLndCustom, checker
×
197
        }
×
198
}
199

200
// HasCustomCaveat tests if the given macaroon has a custom caveat with the
201
// given custom caveat name.
202
func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool {
7✔
203
        if mac == nil {
8✔
204
                return false
1✔
205
        }
1✔
206

207
        caveatPrefix := []byte(fmt.Sprintf(
6✔
208
                "%s %s", CondLndCustom, customCaveatName,
6✔
209
        ))
6✔
210
        for _, caveat := range mac.Caveats() {
12✔
211
                if bytes.HasPrefix(caveat.Id, caveatPrefix) {
8✔
212
                        return true
2✔
213
                }
2✔
214
        }
215

216
        return false
4✔
217
}
218

219
// GetCustomCaveatCondition returns the custom caveat condition for the given
220
// custom caveat name from the given macaroon.
221
func GetCustomCaveatCondition(mac *macaroon.Macaroon,
222
        customCaveatName string) string {
2✔
223

2✔
224
        if mac == nil {
2✔
225
                return ""
×
226
        }
×
227

228
        caveatPrefix := []byte(fmt.Sprintf(
2✔
229
                "%s %s ", CondLndCustom, customCaveatName,
2✔
230
        ))
2✔
231
        for _, caveat := range mac.Caveats() {
4✔
232
                // The caveat id has a format of
2✔
233
                // "lnd-custom [custom-caveat-name] [custom-caveat-condition]"
2✔
234
                // and we only want the condition part. If we match the prefix
2✔
235
                // part we return the condition that comes after the prefix.
2✔
236
                if bytes.HasPrefix(caveat.Id, caveatPrefix) {
3✔
237
                        caveatSplit := strings.SplitN(
1✔
238
                                string(caveat.Id),
1✔
239
                                string(caveatPrefix),
1✔
240
                                2,
1✔
241
                        )
1✔
242
                        if len(caveatSplit) == 2 {
2✔
243
                                return caveatSplit[1]
1✔
244
                        }
1✔
245
                }
246
        }
247

248
        // We didn't find a condition for the given custom caveat name.
249
        return ""
1✔
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc