diff --git a/docs/release-notes/release-notes-0.14.2.md b/docs/release-notes/release-notes-0.14.2.md index 7363af62e..81c0458b2 100644 --- a/docs/release-notes/release-notes-0.14.2.md +++ b/docs/release-notes/release-notes-0.14.2.md @@ -92,6 +92,10 @@ Postgres](https://github.com/lightningnetwork/lnd/pull/6111) exposed](https://github.com/lightningnetwork/lnd/pull/6146) inside WaitingCloseResp from calling `PendingChannels`. +* [CustomCaveatCondition is now properly + set](https://github.com/lightningnetwork/lnd/pull/6185) on + `RPCMiddlewareRequest` messages. + ## Routing @@ -108,6 +112,7 @@ Postgres](https://github.com/lightningnetwork/lnd/pull/6111) * Andras Banki-Horvath * Andreas Schjønhaug * Bjarne Magnussen +* Daniel McNally * Elle Mouton * Harsha Goli * Joost Jager diff --git a/lntest/itest/lnd_rpc_middleware_interceptor_test.go b/lntest/itest/lnd_rpc_middleware_interceptor_test.go index 48be71fca..ebc6676bf 100644 --- a/lntest/itest/lnd_rpc_middleware_interceptor_test.go +++ b/lntest/itest/lnd_rpc_middleware_interceptor_test.go @@ -195,7 +195,7 @@ func middlewareInterceptionTest(t *testing.T, node *lntest.HarnessNode, // block the execution of the main task otherwise. req := &lnrpc.ListChannelsRequest{ActiveOnly: true} go registration.interceptUnary( - "/lnrpc.Lightning/ListChannels", req, nil, + "/lnrpc.Lightning/ListChannels", req, nil, readOnly, ) // Do the actual call now and wait for the interceptor to do its thing. @@ -208,7 +208,7 @@ func middlewareInterceptionTest(t *testing.T, node *lntest.HarnessNode, // Let's test the same for a streaming endpoint. req2 := &lnrpc.PeerEventSubscription{} go registration.interceptStream( - "/lnrpc.Lightning/SubscribePeerEvents", req2, nil, + "/lnrpc.Lightning/SubscribePeerEvents", req2, nil, readOnly, ) // Do the actual call now and wait for the interceptor to do its thing. @@ -327,6 +327,7 @@ func middlewareManipulationTest(t *testing.T, node *lntest.HarnessNode, req := &lnrpc.ListChannelsRequest{ActiveOnly: true} go registration.interceptUnary( "/lnrpc.Lightning/ListChannels", req, replacementResponse, + readOnly, ) // Do the actual call now and wait for the interceptor to do its thing. @@ -349,7 +350,7 @@ func middlewareManipulationTest(t *testing.T, node *lntest.HarnessNode, req2 := &lnrpc.PeerEventSubscription{} go registration.interceptStream( "/lnrpc.Lightning/SubscribePeerEvents", req2, - replacementResponse2, + replacementResponse2, readOnly, ) // Do the actual call now and wait for the interceptor to do its thing. @@ -522,11 +523,21 @@ func registerMiddleware(t *testing.T, node *lntest.HarnessNode, // NOTE: Must be called in a goroutine as this will block until the response is // read from the response channel. func (h *middlewareHarness) interceptUnary(methodURI string, - expectedRequest proto.Message, responseReplacement proto.Message) { + expectedRequest proto.Message, responseReplacement proto.Message, + readOnly bool) { // Read intercept message and make sure it's for an RPC request. reqIntercept, err := h.stream.Recv() require.NoError(h.t, err) + + // Make sure the custom condition is populated correctly (if we're using + // a macaroon with a custom condition). + if !readOnly { + require.Equal( + h.t, "itest-value", reqIntercept.CustomCaveatCondition, + ) + } + req := reqIntercept.GetRequest() require.NotNil(h.t, req) @@ -564,11 +575,21 @@ func (h *middlewareHarness) interceptUnary(methodURI string, // NOTE: Must be called in a goroutine as this will block until the first // response is read from the response channel. func (h *middlewareHarness) interceptStream(methodURI string, - expectedRequest proto.Message, responseReplacement proto.Message) { + expectedRequest proto.Message, responseReplacement proto.Message, + readOnly bool) { // Read intercept message and make sure it's for an RPC stream auth. authIntercept, err := h.stream.Recv() require.NoError(h.t, err) + + // Make sure the custom condition is populated correctly (if we're using + // a macaroon with a custom condition). + if !readOnly { + require.Equal( + h.t, "itest-value", authIntercept.CustomCaveatCondition, + ) + } + auth := authIntercept.GetStreamAuth() require.NotNil(h.t, auth) diff --git a/macaroons/constraints.go b/macaroons/constraints.go index 2cb36cacc..bd8741006 100644 --- a/macaroons/constraints.go +++ b/macaroons/constraints.go @@ -216,3 +216,37 @@ func HasCustomCaveat(mac *macaroon.Macaroon, customCaveatName string) bool { return false } + +// GetCustomCaveatCondition returns the custom caveat condition for the given +// custom caveat name from the given macaroon. +func GetCustomCaveatCondition(mac *macaroon.Macaroon, + customCaveatName string) string { + + if mac == nil { + return "" + } + + caveatPrefix := []byte(fmt.Sprintf( + "%s %s ", CondLndCustom, customCaveatName, + )) + for _, caveat := range mac.Caveats() { + + // The caveat id has a format of + // "lnd-custom [custom-caveat-name] [custom-caveat-condition]" + // and we only want the condition part. If we match the prefix + // part we return the condition that comes after the prefix. + if bytes.HasPrefix(caveat.Id, caveatPrefix) { + caveatSplit := strings.SplitN( + string(caveat.Id), + string(caveatPrefix), + 2, + ) + if len(caveatSplit) == 2 { + return caveatSplit[1] + } + } + } + + // We didn't find a condition for the given custom caveat name. + return "" +} diff --git a/macaroons/constraints_test.go b/macaroons/constraints_test.go index 659b5d724..6e1e243d1 100644 --- a/macaroons/constraints_test.go +++ b/macaroons/constraints_test.go @@ -132,6 +132,11 @@ func TestCustomConstraint(t *testing.T) { require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) require.False(t, macaroons.HasCustomCaveat(nil, "foo")) + customCaveatCondition := macaroons.GetCustomCaveatCondition( + testMacaroon, "unit-test", + ) + require.Equal(t, customCaveatCondition, "test-value") + // Custom caveats don't necessarily need a value, just the name is fine // too to create a tagged macaroon. constraintFunc = macaroons.CustomConstraint("unit-test", "") @@ -144,4 +149,9 @@ func TestCustomConstraint(t *testing.T) { require.True(t, macaroons.HasCustomCaveat(testMacaroon, "unit-test")) require.False(t, macaroons.HasCustomCaveat(testMacaroon, "test-value")) require.False(t, macaroons.HasCustomCaveat(testMacaroon, "something")) + + customCaveatCondition = macaroons.GetCustomCaveatCondition( + testMacaroon, "unit-test", + ) + require.Equal(t, customCaveatCondition, "") } diff --git a/rpcperms/interceptor.go b/rpcperms/interceptor.go index e20b182ae..add82e206 100644 --- a/rpcperms/interceptor.go +++ b/rpcperms/interceptor.go @@ -910,7 +910,7 @@ func (r *InterceptorChain) middlewareRegistered() bool { // acceptRequest sends an intercept request to all middlewares that have // registered for it. This means either a middleware has requested read-only -// access or the request actually has a macaroon which a caveat the middleware +// access or the request actually has a macaroon with a caveat the middleware // registered for. func (r *InterceptorChain) acceptRequest(requestID uint64, msg *InterceptionRequest) error { @@ -929,6 +929,10 @@ func (r *InterceptorChain) acceptRequest(requestID uint64, continue } + msg.CustomCaveatCondition = macaroons.GetCustomCaveatCondition( + msg.Macaroon, middleware.customCaveatName, + ) + resp, err := middleware.intercept(requestID, msg) // Error during interception itself. @@ -975,6 +979,10 @@ func (r *InterceptorChain) interceptResponse(ctx context.Context, continue } + msg.CustomCaveatCondition = macaroons.GetCustomCaveatCondition( + msg.Macaroon, middleware.customCaveatName, + ) + resp, err := middleware.intercept(requestID, msg) // Error during interception itself.