Skip to content

Commit a752204

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 7123e41 commit a752204

File tree

5 files changed

+133
-4
lines changed

5 files changed

+133
-4
lines changed

session/interface.go

Lines changed: 16 additions & 0 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,6 +118,9 @@ 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.
@@ -162,6 +167,7 @@ func buildSession(id ID, localPrivKey *btcec.PrivateKey, label string, typ Type,
162167
PrivacyFlags: opts.privacyFlags,
163168
GroupID: groupID,
164169
MacaroonRecipe: opts.macaroonRecipe,
170+
AccountID: opts.accountID,
165171
}
166172

167173
if len(opts.featureConfig) != 0 {
@@ -195,6 +201,9 @@ type sessionOptions struct {
195201
// macaroonRecipe holds the permissions and caveats that should be used
196202
// to bake the macaroon to be used with this session.
197203
macaroonRecipe *MacaroonRecipe
204+
205+
// accountID is an optional account that the session has been linked to.
206+
accountID fn.Option[accounts.AccountID]
198207
}
199208

200209
// defaultSessionOptions returns a new sessionOptions struct with default
@@ -257,6 +266,13 @@ func WithMacaroonRecipe(caveats []macaroon.Caveat, perms []bakery.Op) Option {
257266
}
258267
}
259268

269+
// WithAccount can be used to link the session to an account.
270+
func WithAccount(id accounts.AccountID) Option {
271+
return func(o *sessionOptions) {
272+
o.accountID = fn.Some(id)
273+
}
274+
}
275+
260276
// IDToGroupIndex defines an interface for the session ID to group ID index.
261277
type IDToGroupIndex interface {
262278
// GetGroupID will return the group ID for the given session ID.

session/kvdb_store.go

Lines changed: 14 additions & 0 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
)
1820

@@ -194,6 +196,8 @@ func getSessionKey(session *Session) []byte {
194196
func (db *BoltStore) NewSession(label string, typ Type, expiry time.Time,
195197
serverAddr string, opts ...Option) (*Session, error) {
196198

199+
ctx := context.TODO()
200+
197201
var session *Session
198202
err := db.Update(func(tx *bbolt.Tx) error {
199203
sessionBucket, err := getBucket(tx, sessionBucketKey)
@@ -216,6 +220,16 @@ func (db *BoltStore) NewSession(label string, typ Type, expiry time.Time,
216220

217221
sessionKey := getSessionKey(session)
218222

223+
// If an account is being linked, we first need to check that
224+
// it exists.
225+
session.AccountID.WhenSome(func(account accounts.AccountID) {
226+
session.AccountID = fn.Some(account)
227+
_, err = db.accounts.Account(ctx, account)
228+
})
229+
if err != nil {
230+
return err
231+
}
232+
219233
if len(sessionBucket.Get(sessionKey)) != 0 {
220234
return fmt.Errorf("session with local public key(%x) "+
221235
"already exists",

session/store_test.go

Lines changed: 80 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,12 +413,36 @@ func reserveSession(db Store, label string,
360413
mod(opts)
361414
}
362415

363-
return db.NewSession(label, opts.sessType,
364-
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
365-
"foo.bar.baz:1234",
416+
options := []Option{
366417
WithDevServer(),
367418
WithPrivacy(PrivacyFlags{ClearPubkeys}),
368419
WithLinkedGroupID(opts.groupID),
420+
}
421+
422+
var caveats []macaroon.Caveat
423+
opts.account.WhenSome(func(id accounts.AccountID) {
424+
// For now, we manually add the account caveat for bbolt
425+
// compatibility.
426+
accountCaveat := checkers.Condition(
427+
macaroons.CondLndCustom,
428+
fmt.Sprintf("%s %x", accounts.CondAccount, id[:]),
429+
)
430+
431+
caveats = append(caveats, macaroon.Caveat{
432+
Id: []byte(accountCaveat),
433+
})
434+
435+
options = append(
436+
options,
437+
WithAccount(id),
438+
WithMacaroonRecipe(caveats, nil),
439+
)
440+
})
441+
442+
return db.NewSession(
443+
label, opts.sessType,
444+
time.Date(99999, 1, 1, 0, 0, 0, 0, time.UTC),
445+
"foo.bar.baz:1234", options...,
369446
)
370447
}
371448

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_rpcserver.go

Lines changed: 10 additions & 1 deletion
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
@@ -316,6 +321,10 @@ func (s *sessionRpcServer) AddSession(ctx context.Context,
316321
sessOpts = append(sessOpts, session.WithDevServer())
317322
}
318323

324+
accountID.WhenSome(func(id accounts.AccountID) {
325+
sessOpts = append(sessOpts, session.WithAccount(id))
326+
})
327+
319328
sess, err := s.cfg.db.NewSession(
320329
req.Label, typ, expiry, req.MailboxServerAddr, sessOpts...,
321330
)

0 commit comments

Comments
 (0)