Skip to content

Commit 0ed84a3

Browse files
committed
session: assert account exists before linking a session to it
In this commit, we more tightly & explicitly link a session to an account. At a persitance layer, we have always only linked a session to an account by encoding the AccountID within the macaroon caveat that we store with the session. We still keep this persistence the same but now we first ensure that the account exists and we also add an AccountID field to the Session struct.
1 parent 9bcee65 commit 0ed84a3

File tree

6 files changed

+125
-11
lines changed

6 files changed

+125
-11
lines changed

session/interface.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66

77
"github.com/btcsuite/btcd/btcec/v2"
88
"github.com/lightninglabs/lightning-node-connect/mailbox"
9+
"github.com/lightninglabs/lightning-terminal/accounts"
910
"github.com/lightninglabs/lightning-terminal/macaroons"
11+
"github.com/lightningnetwork/lnd/fn"
1012
"gopkg.in/macaroon-bakery.v2/bakery"
1113
"gopkg.in/macaroon.v2"
1214
)
@@ -116,14 +118,18 @@ type Session struct {
116118
// group of sessions. If this is the very first session in the group
117119
// then this will be the same as ID.
118120
GroupID ID
121+
122+
// AccountID is an optional account that the session has been linked to.
123+
AccountID fn.Option[accounts.AccountID]
119124
}
120125

121126
// buildSession creates a new session with the given user-defined parameters.
122127
func buildSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type,
123128
created, expiry time.Time, serverAddr string, devServer bool,
124129
perms []bakery.Op, caveats []macaroon.Caveat,
125130
featureConfig FeaturesConfig, privacy bool, linkedGroupID *ID,
126-
flags PrivacyFlags) (*Session, error) {
131+
flags PrivacyFlags, account fn.Option[accounts.AccountID]) (*Session,
132+
error) {
127133

128134
_, pairingSecret, err := mailbox.NewPassphraseEntropy()
129135
if err != nil {
@@ -158,6 +164,7 @@ func buildSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type,
158164
WithPrivacyMapper: privacy,
159165
PrivacyFlags: flags,
160166
GroupID: groupID,
167+
AccountID: account,
161168
}
162169

163170
if perms != nil || caveats != nil {
@@ -193,7 +200,8 @@ type Store interface {
193200
NewSession(label string, typ Type, expiry time.Time, serverAddr string,
194201
devServer bool, perms []bakery.Op, caveats []macaroon.Caveat,
195202
featureConfig FeaturesConfig, privacy bool, linkedGroupID *ID,
196-
flags PrivacyFlags) (*Session, error)
203+
flags PrivacyFlags,
204+
account fn.Option[accounts.AccountID]) (*Session, error)
197205

198206
// GetSession fetches the session with the given key.
199207
GetSession(key *btcec.PublicKey) (*Session, error)

session/kvdb_store.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package session
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/binary"
67
"errors"
78
"fmt"
@@ -13,6 +14,7 @@ import (
1314
"github.com/btcsuite/btcd/btcec/v2"
1415
"github.com/lightninglabs/lightning-terminal/accounts"
1516
"github.com/lightningnetwork/lnd/clock"
17+
"github.com/lightningnetwork/lnd/fn"
1618
"go.etcd.io/bbolt"
1719
"gopkg.in/macaroon-bakery.v2/bakery"
1820
"gopkg.in/macaroon.v2"
@@ -196,7 +198,10 @@ func getSessionKey(session *Session) []byte {
196198
func (db *BoltStore) NewSession(label string, typ Type, expiry time.Time,
197199
serverAddr string, devServer bool, perms []bakery.Op,
198200
caveats []macaroon.Caveat, featureConfig FeaturesConfig, privacy bool,
199-
linkedGroupID *ID, flags PrivacyFlags) (*Session, error) {
201+
linkedGroupID *ID, flags PrivacyFlags,
202+
account fn.Option[accounts.AccountID]) (*Session, error) {
203+
204+
ctx := context.TODO()
200205

201206
var session *Session
202207
err := db.Update(func(tx *bbolt.Tx) error {
@@ -213,14 +218,24 @@ func (db *BoltStore) NewSession(label string, typ Type, expiry time.Time,
213218
session, err = buildSession(
214219
id, localPrivKey, label, typ, db.clock.Now(), expiry,
215220
serverAddr, devServer, perms, caveats, featureConfig,
216-
privacy, linkedGroupID, flags,
221+
privacy, linkedGroupID, flags, account,
217222
)
218223
if err != nil {
219224
return err
220225
}
221226

222227
sessionKey := getSessionKey(session)
223228

229+
// If an account is being linked, we first need to check that
230+
// it exists.
231+
account.WhenSome(func(account accounts.AccountID) {
232+
session.AccountID = fn.Some(account)
233+
_, err = db.accounts.Account(ctx, account)
234+
})
235+
if err != nil {
236+
return err
237+
}
238+
224239
if len(sessionBucket.Get(sessionKey)) != 0 {
225240
return fmt.Errorf("session with local public key(%x) "+
226241
"already exists",

session/store_test.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package session
22

33
import (
4+
"context"
5+
"fmt"
46
"testing"
57
"time"
68

79
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/lightninglabs/lightning-terminal/accounts"
811
"github.com/lightningnetwork/lnd/clock"
12+
"github.com/lightningnetwork/lnd/fn"
13+
"github.com/lightningnetwork/lnd/macaroons"
914
"github.com/stretchr/testify/require"
15+
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
16+
"gopkg.in/macaroon.v2"
1017
)
1118

1219
var testTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
@@ -324,9 +331,49 @@ func TestStateShift(t *testing.T) {
324331
require.ErrorContains(t, err, "illegal session state transition")
325332
}
326333

334+
// TestLinkedAccount tests that linking a session to an account works as
335+
// expected.
336+
func TestLinkedAccount(t *testing.T) {
337+
t.Parallel()
338+
ctx := context.Background()
339+
clock := clock.NewTestClock(testTime)
340+
341+
accts := accounts.NewTestDB(t, clock)
342+
db := NewTestDBWithAccounts(t, clock, accts)
343+
344+
// Reserve a session. Link it to an account that does not yet exist.
345+
// This should fail.
346+
acctID := accounts.AccountID{1, 2, 3, 4}
347+
_, err := reserveSession(db, "session 1", withAccount(acctID))
348+
require.ErrorIs(t, err, accounts.ErrAccNotFound)
349+
350+
// Now, add a new account
351+
acct, err := accts.NewAccount(ctx, 1234, clock.Now().Add(time.Hour), "")
352+
require.NoError(t, err)
353+
354+
// Reserve a session. Link it to the account that was just created.
355+
// This should succeed.
356+
357+
s1, err := reserveSession(db, "session 1", withAccount(acct.ID))
358+
require.NoError(t, err)
359+
require.True(t, s1.AccountID.IsSome())
360+
s1.AccountID.WhenSome(func(id accounts.AccountID) {
361+
require.Equal(t, acct.ID, id)
362+
})
363+
364+
// Make sure that a fetched session includes the account ID.
365+
s1, err = db.GetSessionByID(s1.ID)
366+
require.NoError(t, err)
367+
require.True(t, s1.AccountID.IsSome())
368+
s1.AccountID.WhenSome(func(id accounts.AccountID) {
369+
require.Equal(t, acct.ID, id)
370+
})
371+
}
372+
327373
type testSessionOpts struct {
328374
groupID *ID
329375
sessType Type
376+
account fn.Option[accounts.AccountID]
330377
}
331378

332379
func defaultTestSessOpts() *testSessionOpts {
@@ -352,6 +399,12 @@ func withType(t Type) testSessionModifier {
352399
}
353400
}
354401

402+
func withAccount(alias accounts.AccountID) testSessionModifier {
403+
return func(s *testSessionOpts) {
404+
s.account = fn.Some(alias)
405+
}
406+
}
407+
355408
func reserveSession(db Store, label string,
356409
mods ...testSessionModifier) (*Session, error) {
357410

@@ -360,10 +413,25 @@ func reserveSession(db Store, label string,
360413
mod(opts)
361414
}
362415

363-
return db.NewSession(label, opts.sessType,
416+
var caveats []macaroon.Caveat
417+
opts.account.WhenSome(func(id accounts.AccountID) {
418+
// For now, we manually add the account caveat for bbolt
419+
// compatibility.
420+
accountCaveat := checkers.Condition(
421+
macaroons.CondLndCustom,
422+
fmt.Sprintf("%s %x", accounts.CondAccount, id[:]),
423+
)
424+
425+
caveats = append(caveats, macaroon.Caveat{
426+
Id: []byte(accountCaveat),
427+
})
428+
})
429+
430+
return db.NewSession(
431+
label, opts.sessType,
364432
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
365-
"foo.bar.baz:1234", true, nil, nil, nil, true, opts.groupID,
366-
[]PrivacyFlag{ClearPubkeys},
433+
"foo.bar.baz:1234", true, nil, caveats, nil, true, opts.groupID,
434+
[]PrivacyFlag{ClearPubkeys}, opts.account,
367435
)
368436
}
369437

session/tlv.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"time"
88

99
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/lightninglabs/lightning-terminal/accounts"
1011
"github.com/lightningnetwork/lnd/tlv"
1112
"gopkg.in/macaroon-bakery.v2/bakery"
1213
"gopkg.in/macaroon.v2"
@@ -278,6 +279,18 @@ func DeserializeSession(r io.Reader) (*Session, error) {
278279
session.GroupID = session.ID
279280
}
280281

282+
// For any sessions stored in the BBolt store, a coupled account (if
283+
// any) is linked implicitly via the macaroon recipe caveat. So we
284+
// need to extract it from there.
285+
if session.MacaroonRecipe != nil {
286+
session.AccountID, err = accounts.IDFromCaveats(
287+
session.MacaroonRecipe.Caveats,
288+
)
289+
if err != nil {
290+
return nil, err
291+
}
292+
}
293+
281294
return session, nil
282295
}
283296

session/tlv_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"time"
77

88
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/lightninglabs/lightning-terminal/accounts"
10+
"github.com/lightningnetwork/lnd/fn"
911
"github.com/lightningnetwork/lnd/tlv"
1012
"github.com/stretchr/testify/require"
1113
"gopkg.in/macaroon-bakery.v2/bakery"
@@ -137,6 +139,7 @@ func TestSerializeDeserializeSession(t *testing.T) {
137139
test.caveats, test.featureConfig, true,
138140
test.linkedGroupID,
139141
[]PrivacyFlag{ClearPubkeys},
142+
fn.None[accounts.AccountID](),
140143
)
141144
require.NoError(t, err)
142145

@@ -189,7 +192,7 @@ func TestGroupIDForOlderSessions(t *testing.T) {
189192
time.Now(),
190193
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
191194
"foo.bar.baz:1234", true, nil, nil, nil, false, nil,
192-
PrivacyFlags{},
195+
PrivacyFlags{}, fn.None[accounts.AccountID](),
193196
)
194197
require.NoError(t, err)
195198

@@ -225,7 +228,7 @@ func TestGroupID(t *testing.T) {
225228
time.Now(),
226229
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
227230
"foo.bar.baz:1234", true, nil, nil, nil, false, nil,
228-
PrivacyFlags{},
231+
PrivacyFlags{}, fn.None[accounts.AccountID](),
229232
)
230233
require.NoError(t, err)
231234

@@ -241,6 +244,7 @@ func TestGroupID(t *testing.T) {
241244
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
242245
"foo.bar.baz:1234", true, nil, nil, nil, false,
243246
&session1.GroupID, PrivacyFlags{},
247+
fn.None[accounts.AccountID](),
244248
)
245249
require.NoError(t, err)
246250

session_rpcserver.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/lightninglabs/lightning-terminal/perms"
2323
"github.com/lightninglabs/lightning-terminal/rules"
2424
"github.com/lightninglabs/lightning-terminal/session"
25+
"github.com/lightningnetwork/lnd/fn"
2526
"github.com/lightningnetwork/lnd/macaroons"
2627
"google.golang.org/grpc"
2728
"gopkg.in/macaroon-bakery.v2/bakery"
@@ -212,7 +213,10 @@ func (s *sessionRpcServer) AddSession(ctx context.Context,
212213
permissions[entity][action] = struct{}{}
213214
}
214215

215-
var caveats []macaroon.Caveat
216+
var (
217+
caveats []macaroon.Caveat
218+
accountID fn.Option[accounts.AccountID]
219+
)
216220
switch typ {
217221
// For the default session types we use empty caveats and permissions,
218222
// the macaroons are baked correctly when creating the session.
@@ -232,6 +236,7 @@ func (s *sessionRpcServer) AddSession(ctx context.Context,
232236
caveats = append(caveats, macaroon.Caveat{
233237
Id: []byte(cav),
234238
})
239+
accountID = fn.Some(*id)
235240

236241
// For the custom macaroon type, we use the custom permissions specified
237242
// in the request. For the time being, the caveats list will be empty
@@ -311,7 +316,7 @@ func (s *sessionRpcServer) AddSession(ctx context.Context,
311316
sess, err := s.cfg.db.NewSession(
312317
req.Label, typ, expiry, req.MailboxServerAddr,
313318
req.DevServer, uniquePermissions, caveats, nil, false, nil,
314-
session.PrivacyFlags{},
319+
session.PrivacyFlags{}, accountID,
315320
)
316321
if err != nil {
317322
return nil, fmt.Errorf("error creating new session: %v", err)
@@ -1124,6 +1129,7 @@ func (s *sessionRpcServer) AddAutopilotSession(ctx context.Context,
11241129
req.Label, session.TypeAutopilot, expiry,
11251130
req.MailboxServerAddr, req.DevServer, perms, caveats,
11261131
clientConfig, privacy, linkedGroupID, privacyFlags,
1132+
fn.None[accounts.AccountID](),
11271133
)
11281134
if err != nil {
11291135
return nil, fmt.Errorf("error creating new session: %v", err)

0 commit comments

Comments
 (0)