lnrpc+rpcserver: add QoL flag for permission check

To make it even more obvious that by default the permissions to check
aren't taken from the full method provided, we add a new flag that does
that on request.
This commit is contained in:
Oliver Gugger committed 2025-07-21 13:07:43 +02:00
1 parent 66340500e9
commit 6b1c85265f
5 files changed
+680 -543

No files matched your search

+72
View File
@@ -12,6 +12,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -316,6 +317,77 @@ func testMacaroonAuthentication(ht *lntest.HarnessTest) {
require.Error(t, err)
require.Contains(t, err.Error(), "permission denied")
},
}, {
// Check that with the CheckMacaroonPermissions RPC, we can
// check that a macaroon follows the permissions of a given
// method.
name: "default permissions from full method",
run: func(ctxt context.Context, t *testing.T) {
// We test that the macaroon of the test client has
// all the permissions for calling the BakeMacaroon RPC.
mac, err := testNode.ReadMacaroon(
testNode.Cfg.AdminMacPath, wait.DefaultTimeout,
)
require.NoError(t, err)
macBytes, err := mac.MarshalBinary()
require.NoError(t, err)
rpcURI := "/lnrpc.Lightning/BakeMacaroon"
checkReq := &lnrpc.CheckMacPermRequest{
Macaroon: macBytes,
FullMethod: rpcURI,
CheckDefaultPermsFromFullMethod: true,
}
// Test that CheckMacaroonPermissions accurately
// characterizes macaroon as valid, since the admin
// macaroon should have all the permissions.
checkResp, err := testClient.CheckMacaroonPermissions(
ctxt, checkReq,
)
require.NoError(t, err)
require.Equal(t, checkResp.Valid, true)
// Check different error cases.
dummy := []*lnrpc.MacaroonPermission{{
Entity: "foo",
}}
_, err = testClient.CheckMacaroonPermissions(
ctxt, &lnrpc.CheckMacPermRequest{
Permissions: dummy,
CheckDefaultPermsFromFullMethod: true,
},
)
require.ErrorContains(
t, err, "cannot check default permissions "+
"from full method and from provided "+
"permission list at the same time",
)
_, err = testClient.CheckMacaroonPermissions(
ctxt, &lnrpc.CheckMacPermRequest{
FullMethod: "",
CheckDefaultPermsFromFullMethod: true,
},
)
require.ErrorContains(
t, err, "cannot check default permissions "+
"from full method without providing "+
"the full method name",
)
_, err = testClient.CheckMacaroonPermissions(
ctxt, &lnrpc.CheckMacPermRequest{
FullMethod: "baz",
CheckDefaultPermsFromFullMethod: true,
},
)
require.ErrorContains(
t, err, "no permissions found for full method "+
"baz",
)
},
}}
for _, tc := range testCases {
+562 -541
View File
File diff suppressed because it is too large. Load diff
+11 -1
View File
@@ -5119,7 +5119,8 @@ message CheckMacPermRequest {
// If the list of permissions given is empty, then the macaroon is
// considered valid only based on issuance authority and caveat validity.
// An empty list of permissions is therefore equivalent to saying "skip
// checking permissions".
// checking permissions" (unless check_default_perms_from_full_method is
// specified).
repeated MacaroonPermission permissions = 2;
// The RPC method to check the macaroon against. This is only used if there
@@ -5129,6 +5130,15 @@ message CheckMacPermRequest {
// first, extract the permissions for the method, and then pass them in the
// `permissions` field above.
string fullMethod = 3;
// If this field is set to true, then the permissions list above MUST be
// empty. The default permissions for the provided fullMethod will be used
// to check the macaroon. This is equivalent to looking up the permissions
// for a method in the `ListPermissions` RPC and then calling this RPC with
// the permission list returned from that call. Without this flag, the list
// of permissions must be non-empty for the check to actually perform a
// permission check.
bool check_default_perms_from_full_method = 4;
}
message CheckMacPermResponse {
+5 -1
View File
@@ -4676,11 +4676,15 @@
"type": "object",
"$ref": "#/definitions/lnrpcMacaroonPermission"
},
"description": "The list of permissions the macaroon should be checked against. Only if\nthe macaroon contains all of these permissions, it is considered valid.\nIf the list of permissions given is empty, then the macaroon is\nconsidered valid only based on issuance authority and caveat validity.\nAn empty list of permissions is therefore equivalent to saying \"skip\nchecking permissions\"."
"description": "The list of permissions the macaroon should be checked against. Only if\nthe macaroon contains all of these permissions, it is considered valid.\nIf the list of permissions given is empty, then the macaroon is\nconsidered valid only based on issuance authority and caveat validity.\nAn empty list of permissions is therefore equivalent to saying \"skip\nchecking permissions\" (unless check_default_perms_from_full_method is\nspecified)."
},
"fullMethod": {
"type": "string",
"description": "The RPC method to check the macaroon against. This is only used if there\nare custom `uri:\u003crpcpackage\u003e.\u003cServiceName\u003e/\u003cMethodName\u003e` permissions in\nthe permission list above. To check a macaroon against the list of\npermissions of a certain RPC method, query the `ListPermissions` RPC\nfirst, extract the permissions for the method, and then pass them in the\n`permissions` field above."
},
"check_default_perms_from_full_method": {
"type": "boolean",
"description": "If this field is set to true, then the permissions list above MUST be\nempty. The default permissions for the provided fullMethod will be used\nto check the macaroon. This is equivalent to looking up the permissions\nfor a method in the `ListPermissions` RPC and then calling this RPC with\nthe permission list returned from that call. Without this flag, the list\nof permissions must be non-empty for the check to actually perform a\npermission check."
}
}
},
+30
View File
@@ -8794,6 +8794,20 @@ func (r *rpcServer) ListPermissions(_ context.Context,
func (r *rpcServer) CheckMacaroonPermissions(ctx context.Context,
req *lnrpc.CheckMacPermRequest) (*lnrpc.CheckMacPermResponse, error) {
// Sanity-check the input parameters to eliminate impossible
// combinations.
switch {
case len(req.Permissions) > 0 && req.CheckDefaultPermsFromFullMethod:
return nil, fmt.Errorf("cannot check default permissions " +
"from full method and from provided permission list " +
"at the same time")
case len(req.FullMethod) == 0 && req.CheckDefaultPermsFromFullMethod:
return nil, fmt.Errorf("cannot check default permissions " +
"from full method without providing the full method " +
"name")
}
// Turn grpc macaroon permission into bakery.Op for the server to
// process.
permissions := make([]bakery.Op, len(req.Permissions))
@@ -8804,6 +8818,22 @@ func (r *rpcServer) CheckMacaroonPermissions(ctx context.Context,
}
}
// If the user wants to check the default permissions for the
// full method, then we'll use the interceptor chain to obtain the
// default permissions for the full method. This overwrites the
// user-provided permissions parsed above, but those are required to be
// empty anyway if the flag is turned on.
if req.CheckDefaultPermsFromFullMethod {
allPerms := r.interceptorChain.Permissions()
methodPerms, ok := allPerms[req.FullMethod]
if !ok {
return nil, fmt.Errorf("no permissions found for "+
"full method %s", req.FullMethod)
}
permissions = methodPerms
}
err := r.macService.CheckMacAuth(
ctx, req.Macaroon, permissions, req.FullMethod,
)