mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
macaroons: add super macaroon helpers
Introduce helper functions in the macaroons package to manage super macaroon files and permissions. Add SuperMacaroonExists, MacaroonMatchesPermissions, and BakeAndWriteSuperMacaroon.
This commit is contained in:
parent
e093948a35
commit
3975e83006
2 changed files with 250 additions and 0 deletions
|
|
@ -6,8 +6,12 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"gopkg.in/macaroon-bakery.v2/bakery"
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||||
"gopkg.in/macaroon.v2"
|
"gopkg.in/macaroon.v2"
|
||||||
)
|
)
|
||||||
|
|
@ -118,3 +122,127 @@ func BakeSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient,
|
||||||
|
|
||||||
return hex.EncodeToString(macBytes), err
|
return hex.EncodeToString(macBytes), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperMacaroonExists determines whether a macaroon file exists at the given
|
||||||
|
// path.
|
||||||
|
func SuperMacaroonExists(path string) bool {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MacaroonMatchesPermissions checks if the macaroon at the given path contains
|
||||||
|
// exactly the expected permissions (no more and no less).
|
||||||
|
func MacaroonMatchesPermissions(path string,
|
||||||
|
expectedPerms []bakery.Op) (bool, error) {
|
||||||
|
|
||||||
|
macBytes, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := &macaroon.Macaroon{}
|
||||||
|
if err := mac.UnmarshalBinary(macBytes); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawID := mac.Id()
|
||||||
|
if len(rawID) == 0 || rawID[0] != byte(bakery.LatestVersion) {
|
||||||
|
return false, errors.New("invalid macaroon version")
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedID := &lnrpc.MacaroonId{}
|
||||||
|
if err := proto.Unmarshal(rawID[1:], decodedID); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map expected permissions for easy lookup: entity -> action -> true.
|
||||||
|
expectedMap := make(map[string]map[string]bool)
|
||||||
|
for _, op := range expectedPerms {
|
||||||
|
if expectedMap[op.Entity] == nil {
|
||||||
|
expectedMap[op.Entity] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMap[op.Entity][op.Action] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map actual permissions from decoded macaroon ID:
|
||||||
|
// entity -> action -> true.
|
||||||
|
actualMap := make(map[string]map[string]bool)
|
||||||
|
for _, op := range decodedID.Ops {
|
||||||
|
if op == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if actualMap[op.Entity] == nil {
|
||||||
|
actualMap[op.Entity] = make(map[string]bool)
|
||||||
|
}
|
||||||
|
for _, action := range op.Actions {
|
||||||
|
actualMap[op.Entity][action] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the mapped sets for exact equality.
|
||||||
|
if len(expectedMap) != len(actualMap) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for entity, actions := range expectedMap {
|
||||||
|
actualActions, ok := actualMap[entity]
|
||||||
|
|
||||||
|
if !ok || len(actions) != len(actualActions) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
for action := range actions {
|
||||||
|
if !actualActions[action] {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BakeAndWriteSuperMacaroon bakes a super macaroon and writes it to disk.
|
||||||
|
func BakeAndWriteSuperMacaroon(ctx context.Context, lnd lnrpc.LightningClient,
|
||||||
|
path string, perms []bakery.Op) error {
|
||||||
|
|
||||||
|
var suffixBytes [4]byte
|
||||||
|
rootKeyID := NewSuperMacaroonRootKeyID(suffixBytes)
|
||||||
|
|
||||||
|
superMacHex, err := BakeSuperMacaroon(
|
||||||
|
ctx, lnd, rootKeyID, perms, nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to bake super macaroon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
superMacBytes, err := hex.DecodeString(superMacHex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode baked "+
|
||||||
|
"super macaroon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, superMacBytes, 0600); err != nil {
|
||||||
|
return fmt.Errorf("unable to write super macaroon to %v: %w",
|
||||||
|
path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMacaroonSuffix checks that the super macaroon path is not empty and ends
|
||||||
|
// with the expected suffix.
|
||||||
|
func HasMacaroonSuffix(path string) error {
|
||||||
|
if path == "" {
|
||||||
|
return fmt.Errorf("super-macaroon-path cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(path, ".macaroon") {
|
||||||
|
return fmt.Errorf("super-macaroon-path must end with the " +
|
||||||
|
".macaroon suffix")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package macaroons
|
package macaroons
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -43,3 +47,121 @@ func TestIsSuperMacaroon(t *testing.T) {
|
||||||
|
|
||||||
require.True(t, IsSuperMacaroon(testMacHex))
|
require.True(t, IsSuperMacaroon(testMacHex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSuperMacaroonHelpers tests that SuperMacaroonExists and
|
||||||
|
// MacaroonMatchesPermissions behave correctly.
|
||||||
|
func TestSuperMacaroonHelpers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
path := filepath.Join(tempDir, "test.macaroon")
|
||||||
|
|
||||||
|
// Verify that it doesn't exist yet.
|
||||||
|
require.False(t, SuperMacaroonExists(path))
|
||||||
|
|
||||||
|
// Write the test macaroon.
|
||||||
|
macBytes, err := hex.DecodeString(testMacHex)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(path, macBytes, 0600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now it should exist.
|
||||||
|
require.True(t, SuperMacaroonExists(path))
|
||||||
|
|
||||||
|
// The macaroon matches this expected list of permissions.
|
||||||
|
expectedPerms := []bakery.Op{
|
||||||
|
{Entity: "account", Action: "read"},
|
||||||
|
{Entity: "auction", Action: "read"},
|
||||||
|
{Entity: "audit", Action: "read"},
|
||||||
|
{Entity: "auth", Action: "read"},
|
||||||
|
{Entity: "info", Action: "read"},
|
||||||
|
{Entity: "insights", Action: "read"},
|
||||||
|
{Entity: "invoices", Action: "read"},
|
||||||
|
{Entity: "loop", Action: "in"},
|
||||||
|
{Entity: "loop", Action: "out"},
|
||||||
|
{Entity: "macaroon", Action: "read"},
|
||||||
|
{Entity: "message", Action: "read"},
|
||||||
|
{Entity: "offchain", Action: "read"},
|
||||||
|
{Entity: "onchain", Action: "read"},
|
||||||
|
{Entity: "order", Action: "read"},
|
||||||
|
{Entity: "peers", Action: "read"},
|
||||||
|
{Entity: "rates", Action: "read"},
|
||||||
|
{Entity: "recommendation", Action: "read"},
|
||||||
|
{Entity: "report", Action: "read"},
|
||||||
|
{Entity: "suggestions", Action: "read"},
|
||||||
|
{Entity: "swap", Action: "read"},
|
||||||
|
{Entity: "terms", Action: "read"},
|
||||||
|
}
|
||||||
|
|
||||||
|
matches, err := MacaroonMatchesPermissions(path, expectedPerms)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, matches)
|
||||||
|
|
||||||
|
// A subset of permissions should NOT match exactly.
|
||||||
|
matches, err = MacaroonMatchesPermissions(path, expectedPerms[:5])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, matches)
|
||||||
|
|
||||||
|
// Extra/different permissions should NOT match exactly.
|
||||||
|
differentPerms := append(
|
||||||
|
expectedPerms,
|
||||||
|
bakery.Op{Entity: "invalid", Action: "write"},
|
||||||
|
)
|
||||||
|
matches, err = MacaroonMatchesPermissions(path, differentPerms)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasMacaroonSuffix tests that HasMacaroonSuffix correctly
|
||||||
|
// checks the suffix of the super macaroon path.
|
||||||
|
func TestHasMacaroonSuffix(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
wantErr bool
|
||||||
|
errStr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty path",
|
||||||
|
path: "",
|
||||||
|
wantErr: true,
|
||||||
|
errStr: "super-macaroon-path cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid path",
|
||||||
|
path: "/tmp/test.macaroon",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid path - missing suffix",
|
||||||
|
path: "/tmp/test.mac",
|
||||||
|
wantErr: true,
|
||||||
|
errStr: "super-macaroon-path must end " +
|
||||||
|
"with the .macaroon suffix",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid path - wrong suffix",
|
||||||
|
path: "/tmp/test.macaroon.tmp",
|
||||||
|
wantErr: true,
|
||||||
|
errStr: "super-macaroon-path must end " +
|
||||||
|
"with the .macaroon suffix",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := HasMacaroonSuffix(tt.path)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.errStr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue