mirror of https://github.com/lightninglabs/loop
lsat: introduce LSAT related utilities
We introduce a new package: `lsat`, which aims to provide utilities that will serve useful in the context of LSAT creation and verification for LSAT-enabled services.pull/109/head
parent
edc3037077
commit
1eb8ed3da5
@ -0,0 +1,142 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/macaroon.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PreimageKey is the key used for a payment preimage caveat.
|
||||||
|
PreimageKey = "preimage"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidCaveat is an error returned when we attempt to decode a
|
||||||
|
// caveat with an invalid format.
|
||||||
|
ErrInvalidCaveat = errors.New("caveat must be of the form " +
|
||||||
|
"\"condition=value\"")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Caveat is a predicate that can be applied to an LSAT in order to restrict its
|
||||||
|
// use in some form. Caveats are evaluated during LSAT verification after the
|
||||||
|
// LSAT's signature is verified. The predicate of each caveat must hold true in
|
||||||
|
// order to successfully validate an LSAT.
|
||||||
|
type Caveat struct {
|
||||||
|
// Condition serves as a way to identify a caveat and how to satisfy it.
|
||||||
|
Condition string
|
||||||
|
|
||||||
|
// Value is what will be used to satisfy a caveat. This can be as
|
||||||
|
// flexible as needed, as long as it can be encoded into a string.
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCaveat construct a new caveat with the given condition and value.
|
||||||
|
func NewCaveat(condition string, value string) Caveat {
|
||||||
|
return Caveat{Condition: condition, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a user-friendly view of a caveat.
|
||||||
|
func (c Caveat) String() string {
|
||||||
|
return EncodeCaveat(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeCaveat encodes a caveat into its string representation.
|
||||||
|
func EncodeCaveat(c Caveat) string {
|
||||||
|
return fmt.Sprintf("%v=%v", c.Condition, c.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeCaveat decodes a caveat from its string representation.
|
||||||
|
func DecodeCaveat(s string) (Caveat, error) {
|
||||||
|
parts := strings.SplitN(s, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return Caveat{}, ErrInvalidCaveat
|
||||||
|
}
|
||||||
|
return Caveat{Condition: parts[0], Value: parts[1]}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFirstPartyCaveats adds a set of caveats as first-party caveats to a
|
||||||
|
// macaroon.
|
||||||
|
func AddFirstPartyCaveats(m *macaroon.Macaroon, caveats ...Caveat) error {
|
||||||
|
for _, c := range caveats {
|
||||||
|
rawCaveat := []byte(EncodeCaveat(c))
|
||||||
|
if err := m.AddFirstPartyCaveat(rawCaveat); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasCaveat checks whether the given macaroon has a caveat with the given
|
||||||
|
// condition, and if so, returns its value. If multiple caveats with the same
|
||||||
|
// condition exist, then the value of the last one is returned.
|
||||||
|
func HasCaveat(m *macaroon.Macaroon, cond string) (string, bool) {
|
||||||
|
var value *string
|
||||||
|
for _, rawCaveat := range m.Caveats() {
|
||||||
|
caveat, err := DecodeCaveat(string(rawCaveat.Id))
|
||||||
|
if err != nil {
|
||||||
|
// Ignore any unknown caveats as we can't decode them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if caveat.Condition == cond {
|
||||||
|
value = &caveat.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return *value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyCaveats determines whether every relevant caveat of an LSAT holds true.
|
||||||
|
// A caveat is considered relevant if a satisfier is provided for it, which is
|
||||||
|
// what we'll use as their evaluation.
|
||||||
|
//
|
||||||
|
// NOTE: The caveats provided should be in the same order as in the LSAT to
|
||||||
|
// ensure the correctness of each satisfier's SatisfyPrevious.
|
||||||
|
func VerifyCaveats(caveats []Caveat, satisfiers ...Satisfier) error {
|
||||||
|
// Construct a set of our satisfiers to determine which caveats we know
|
||||||
|
// how to satisfy.
|
||||||
|
caveatSatisfiers := make(map[string]Satisfier, len(satisfiers))
|
||||||
|
for _, satisfier := range satisfiers {
|
||||||
|
caveatSatisfiers[satisfier.Condition] = satisfier
|
||||||
|
}
|
||||||
|
relevantCaveats := make(map[string][]Caveat)
|
||||||
|
for _, caveat := range caveats {
|
||||||
|
if _, ok := caveatSatisfiers[caveat.Condition]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relevantCaveats[caveat.Condition] = append(
|
||||||
|
relevantCaveats[caveat.Condition], caveat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for condition, caveats := range relevantCaveats {
|
||||||
|
satisfier := caveatSatisfiers[condition]
|
||||||
|
|
||||||
|
// Since it's possible for a chain of caveat to exist for the
|
||||||
|
// same condition as a way to demote privileges, we'll ensure
|
||||||
|
// each one satisfies its previous.
|
||||||
|
for i, j := 0, 1; j < len(caveats); i, j = i+1, j+1 {
|
||||||
|
prevCaveat := caveats[i]
|
||||||
|
curCaveat := caveats[j]
|
||||||
|
err := satisfier.SatisfyPrevious(prevCaveat, curCaveat)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we verify the previous ones, if any, we can proceed to
|
||||||
|
// verify the final one, which is the decision maker.
|
||||||
|
err := satisfier.SatisfyFinal(caveats[len(caveats)-1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,202 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gopkg.in/macaroon.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testMacaroon, _ = macaroon.New(nil, nil, "", macaroon.LatestVersion)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCaveatSerialization ensures that we can properly encode/decode valid
|
||||||
|
// caveats and cannot do so for invalid ones.
|
||||||
|
func TestCaveatSerialization(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
caveatStr string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid caveat",
|
||||||
|
caveatStr: "expiration=1337",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid caveat with separator in value",
|
||||||
|
caveatStr: "expiration=1337=",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid caveat",
|
||||||
|
caveatStr: "expiration:1337",
|
||||||
|
err: ErrInvalidCaveat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
success := t.Run(test.name, func(t *testing.T) {
|
||||||
|
caveat, err := DecodeCaveat(test.caveatStr)
|
||||||
|
if !errors.Is(err, test.err) {
|
||||||
|
t.Fatalf("expected err \"%v\", got \"%v\"",
|
||||||
|
test.err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
caveatStr := EncodeCaveat(caveat)
|
||||||
|
if caveatStr != test.caveatStr {
|
||||||
|
t.Fatalf("expected encoded caveat \"%v\", "+
|
||||||
|
"got \"%v\"", test.caveatStr, caveatStr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHasCaveat ensures we can determine whether a macaroon contains a caveat
|
||||||
|
// with a specific condition.
|
||||||
|
func TestHasCaveat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
cond = "cond"
|
||||||
|
value = "value"
|
||||||
|
)
|
||||||
|
m := testMacaroon.Clone()
|
||||||
|
|
||||||
|
// The macaroon doesn't have any caveats, so we shouldn't find any.
|
||||||
|
if _, ok := HasCaveat(m, cond); ok {
|
||||||
|
t.Fatal("found unexpected caveat with unknown condition")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add two caveats, one in a valid LSAT format and another invalid.
|
||||||
|
// We'll test that we're still able to determine the macaroon contains
|
||||||
|
// the valid caveat even though there is one that is invalid.
|
||||||
|
invalidCaveat := []byte("invalid")
|
||||||
|
if err := m.AddFirstPartyCaveat(invalidCaveat); err != nil {
|
||||||
|
t.Fatalf("unable to add macaroon caveat: %v", err)
|
||||||
|
}
|
||||||
|
validCaveat1 := Caveat{Condition: cond, Value: value}
|
||||||
|
if err := AddFirstPartyCaveats(m, validCaveat1); err != nil {
|
||||||
|
t.Fatalf("unable to add macaroon caveat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caveatValue, ok := HasCaveat(m, cond)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected macaroon to contain caveat")
|
||||||
|
}
|
||||||
|
if caveatValue != validCaveat1.Value {
|
||||||
|
t.Fatalf("expected caveat value \"%v\", got \"%v\"",
|
||||||
|
validCaveat1.Value, caveatValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we add another caveat with the same condition, the value of the
|
||||||
|
// most recently added caveat should be returned instead.
|
||||||
|
validCaveat2 := validCaveat1
|
||||||
|
validCaveat2.Value += value
|
||||||
|
if err := AddFirstPartyCaveats(m, validCaveat2); err != nil {
|
||||||
|
t.Fatalf("unable to add macaroon caveat: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caveatValue, ok = HasCaveat(m, cond)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected macaroon to contain caveat")
|
||||||
|
}
|
||||||
|
if caveatValue != validCaveat2.Value {
|
||||||
|
t.Fatalf("expected caveat value \"%v\", got \"%v\"",
|
||||||
|
validCaveat2.Value, caveatValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVerifyCaveats ensures caveat verification only holds true for known
|
||||||
|
// caveats.
|
||||||
|
func TestVerifyCaveats(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
caveat1 := Caveat{Condition: "1", Value: "test"}
|
||||||
|
caveat2 := Caveat{Condition: "2", Value: "test"}
|
||||||
|
satisfier := Satisfier{
|
||||||
|
Condition: caveat1.Condition,
|
||||||
|
SatisfyPrevious: func(c Caveat, prev Caveat) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SatisfyFinal: func(c Caveat) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
invalidSatisfyPrevious := func(c Caveat, prev Caveat) error {
|
||||||
|
return errors.New("no")
|
||||||
|
}
|
||||||
|
invalidSatisfyFinal := func(c Caveat) error {
|
||||||
|
return errors.New("no")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
caveats []Caveat
|
||||||
|
satisfiers []Satisfier
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple verification",
|
||||||
|
caveats: []Caveat{caveat1},
|
||||||
|
satisfiers: []Satisfier{satisfier},
|
||||||
|
shouldFail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown caveat",
|
||||||
|
caveats: []Caveat{caveat1, caveat2},
|
||||||
|
satisfiers: []Satisfier{satisfier},
|
||||||
|
shouldFail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one invalid",
|
||||||
|
caveats: []Caveat{caveat1, caveat2},
|
||||||
|
satisfiers: []Satisfier{
|
||||||
|
satisfier,
|
||||||
|
{
|
||||||
|
Condition: caveat2.Condition,
|
||||||
|
SatisfyFinal: invalidSatisfyFinal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prev invalid",
|
||||||
|
caveats: []Caveat{caveat1, caveat1},
|
||||||
|
satisfiers: []Satisfier{
|
||||||
|
{
|
||||||
|
Condition: caveat1.Condition,
|
||||||
|
SatisfyPrevious: invalidSatisfyPrevious,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shouldFail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
success := t.Run(test.name, func(t *testing.T) {
|
||||||
|
err := VerifyCaveats(test.caveats, test.satisfiers...)
|
||||||
|
if test.shouldFail && err == nil {
|
||||||
|
t.Fatal("expected caveat verification to fail")
|
||||||
|
}
|
||||||
|
if !test.shouldFail && err != nil {
|
||||||
|
t.Fatal("unexpected caveat verification failure")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// LatestVersion is the latest version used for minting new LSATs.
|
||||||
|
LatestVersion = 0
|
||||||
|
|
||||||
|
// SecretSize is the size in bytes of a LSAT's secret, also known as
|
||||||
|
// the root key of the macaroon.
|
||||||
|
SecretSize = 32
|
||||||
|
|
||||||
|
// TokenIDSize is the size in bytes of an LSAT's ID encoded in its
|
||||||
|
// macaroon identifier.
|
||||||
|
TokenIDSize = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// byteOrder is the byte order used to encode/decode a macaroon's raw
|
||||||
|
// identifier.
|
||||||
|
byteOrder = binary.BigEndian
|
||||||
|
|
||||||
|
// ErrUnknownVersion is an error returned when attempting to decode an
|
||||||
|
// LSAT identifier with an unknown version.
|
||||||
|
ErrUnknownVersion = errors.New("unknown LSAT version")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Identifier contains the static identifying details of an LSAT. This is
|
||||||
|
// intended to be used as the identifier of the macaroon within an LSAT.
|
||||||
|
type Identifier struct {
|
||||||
|
// Version is the version of an LSAT. Having a version allows us to
|
||||||
|
// introduce new fields to the identifier in a backwards-compatible
|
||||||
|
// manner.
|
||||||
|
Version uint16
|
||||||
|
|
||||||
|
// PaymentHash is the payment hash linked to an LSAT. Verification of
|
||||||
|
// an LSAT depends on a valid payment, which is enforced by ensuring a
|
||||||
|
// preimage is provided that hashes to our payment hash.
|
||||||
|
PaymentHash lntypes.Hash
|
||||||
|
|
||||||
|
// TokenID is the unique identifier of an LSAT.
|
||||||
|
TokenID [TokenIDSize]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeIdentifier encodes an LSAT's identifier according to its version.
|
||||||
|
func EncodeIdentifier(w io.Writer, id *Identifier) error {
|
||||||
|
if err := binary.Write(w, byteOrder, id.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch id.Version {
|
||||||
|
// A version 0 identifier consists of its linked payment hash, followed
|
||||||
|
// by the token ID.
|
||||||
|
case 0:
|
||||||
|
if _, err := w.Write(id.PaymentHash[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := w.Write(id.TokenID[:])
|
||||||
|
return err
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: %v", ErrUnknownVersion, id.Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeIdentifier decodes an LSAT's identifier according to its version.
|
||||||
|
func DecodeIdentifier(r io.Reader) (*Identifier, error) {
|
||||||
|
var version uint16
|
||||||
|
if err := binary.Read(r, byteOrder, &version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch version {
|
||||||
|
// A version 0 identifier consists of its linked payment hash, followed
|
||||||
|
// by the token ID.
|
||||||
|
case 0:
|
||||||
|
var paymentHash lntypes.Hash
|
||||||
|
if _, err := r.Read(paymentHash[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var tokenID [TokenIDSize]byte
|
||||||
|
if _, err := r.Read(tokenID[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Identifier{
|
||||||
|
Version: version,
|
||||||
|
PaymentHash: paymentHash,
|
||||||
|
TokenID: tokenID,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrUnknownVersion, version)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testPaymentHash lntypes.Hash
|
||||||
|
testTokenID [TokenIDSize]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestIdentifierSerialization ensures proper serialization of known identifier
|
||||||
|
// versions and failures for unknown versions.
|
||||||
|
func TestIdentifierSerialization(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
id Identifier
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid identifier",
|
||||||
|
id: Identifier{
|
||||||
|
Version: LatestVersion,
|
||||||
|
PaymentHash: testPaymentHash,
|
||||||
|
TokenID: testTokenID,
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown version",
|
||||||
|
id: Identifier{
|
||||||
|
Version: LatestVersion + 1,
|
||||||
|
PaymentHash: testPaymentHash,
|
||||||
|
TokenID: testTokenID,
|
||||||
|
},
|
||||||
|
err: ErrUnknownVersion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
success := t.Run(test.name, func(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := EncodeIdentifier(&buf, &test.id)
|
||||||
|
if !errors.Is(err, test.err) {
|
||||||
|
t.Fatalf("expected err \"%v\", got \"%v\"",
|
||||||
|
test.err, err)
|
||||||
|
}
|
||||||
|
if test.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := DecodeIdentifier(&buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to decode identifier: %v", err)
|
||||||
|
}
|
||||||
|
if *id != test.id {
|
||||||
|
t.Fatalf("expected id %v, got %v", test.id, *id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Satisfier provides a generic interface to satisfy a caveat based on its
|
||||||
|
// condition.
|
||||||
|
type Satisfier struct {
|
||||||
|
// Condition is the condition of the caveat we'll attempt to satisfy.
|
||||||
|
Condition string
|
||||||
|
|
||||||
|
// SatisfyPrevious ensures a caveat is in accordance with a previous one
|
||||||
|
// with the same condition. This is needed since caveats of the same
|
||||||
|
// condition can be used multiple times as long as they enforce more
|
||||||
|
// permissions than the previous.
|
||||||
|
//
|
||||||
|
// For example, we have a caveat that only allows us to use an LSAT for
|
||||||
|
// 7 more days. We can add another caveat that only allows for 3 more
|
||||||
|
// days of use and lend it to another party.
|
||||||
|
SatisfyPrevious func(previous Caveat, current Caveat) error
|
||||||
|
|
||||||
|
// SatisfyFinal satisfies the final caveat of an LSAT. If multiple
|
||||||
|
// caveats with the same condition exist, this will only be executed
|
||||||
|
// once all previous caveats are also satisfied.
|
||||||
|
SatisfyFinal func(Caveat) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicesSatisfier implements a satisfier to determine whether the target
|
||||||
|
// service is authorized for a given LSAT.
|
||||||
|
//
|
||||||
|
// TODO(wilmer): Add tier verification?
|
||||||
|
func NewServicesSatisfier(targetService string) Satisfier {
|
||||||
|
return Satisfier{
|
||||||
|
Condition: CondServices,
|
||||||
|
SatisfyPrevious: func(prev, cur Caveat) error {
|
||||||
|
// Construct a set of the services we were previously
|
||||||
|
// allowed to access.
|
||||||
|
prevServices, err := decodeServicesCaveatValue(prev.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prevAllowed := make(map[string]struct{}, len(prevServices))
|
||||||
|
for _, service := range prevServices {
|
||||||
|
prevAllowed[service.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The caveat should not include any new services that
|
||||||
|
// weren't previously allowed.
|
||||||
|
currentServices, err := decodeServicesCaveatValue(cur.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, service := range currentServices {
|
||||||
|
if _, ok := prevAllowed[service.Name]; !ok {
|
||||||
|
return fmt.Errorf("service %v not "+
|
||||||
|
"previously allowed", service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SatisfyFinal: func(c Caveat) error {
|
||||||
|
services, err := decodeServicesCaveatValue(c.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, service := range services {
|
||||||
|
if service.Name == targetService {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("target service %v not authorized",
|
||||||
|
targetService)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCapabilitiesSatisfier implements a satisfier to determine whether the
|
||||||
|
// target capability for a service is authorized for a given LSAT.
|
||||||
|
func NewCapabilitiesSatisfier(service string, targetCapability string) Satisfier {
|
||||||
|
return Satisfier{
|
||||||
|
Condition: service + CondCapabilitiesSuffix,
|
||||||
|
SatisfyPrevious: func(prev, cur Caveat) error {
|
||||||
|
// Construct a set of the service's capabilities we were
|
||||||
|
// previously allowed to access.
|
||||||
|
prevCapabilities := strings.Split(prev.Value, ",")
|
||||||
|
allowed := make(map[string]struct{}, len(prevCapabilities))
|
||||||
|
for _, capability := range prevCapabilities {
|
||||||
|
allowed[capability] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The caveat should not include any new service
|
||||||
|
// capabilities that weren't previously allowed.
|
||||||
|
currentCapabilities := strings.Split(cur.Value, ",")
|
||||||
|
for _, capability := range currentCapabilities {
|
||||||
|
if _, ok := allowed[capability]; !ok {
|
||||||
|
return fmt.Errorf("capability %v not "+
|
||||||
|
"previously allowed", capability)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
SatisfyFinal: func(c Caveat) error {
|
||||||
|
capabilities := strings.Split(c.Value, ",")
|
||||||
|
for _, capability := range capabilities {
|
||||||
|
if capability == targetCapability {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("target capability %v not authorized",
|
||||||
|
targetCapability)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CondServices is the condition used for a services caveat.
|
||||||
|
CondServices = "services"
|
||||||
|
|
||||||
|
// CondCapabilitiesSuffix is the condition suffix used for a service's
|
||||||
|
// capabilities caveat. For example, the condition of a capabilities
|
||||||
|
// caveat for a service named `loop` would be `loop_capabilities`.
|
||||||
|
CondCapabilitiesSuffix = "_capabilities"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoServices is an error returned when we attempt to decode the
|
||||||
|
// services included in a caveat.
|
||||||
|
ErrNoServices = errors.New("no services found")
|
||||||
|
|
||||||
|
// ErrInvalidService is an error returned when we attempt to decode a
|
||||||
|
// service with an invalid format.
|
||||||
|
ErrInvalidService = errors.New("service must be of the form " +
|
||||||
|
"\"name:tier\"")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceTier represents the different possible tiers of an LSAT-enabled
|
||||||
|
// service.
|
||||||
|
type ServiceTier uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BaseTier is the base tier of an LSAT-enabled service. This tier
|
||||||
|
// should be used for any new LSATs that are not part of a service tier
|
||||||
|
// upgrade.
|
||||||
|
BaseTier ServiceTier = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service contains the details of an LSAT-enabled service.
|
||||||
|
type Service struct {
|
||||||
|
// Name is the name of the LSAT-enabled service.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Tier is the tier of the LSAT-enabled service.
|
||||||
|
Tier ServiceTier
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicesCaveat creates a new services caveat with the provided caveats.
|
||||||
|
func NewServicesCaveat(services ...Service) (Caveat, error) {
|
||||||
|
value, err := encodeServicesCaveatValue(services...)
|
||||||
|
if err != nil {
|
||||||
|
return Caveat{}, err
|
||||||
|
}
|
||||||
|
return Caveat{
|
||||||
|
Condition: CondServices,
|
||||||
|
Value: value,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeServicesCaveatValue encodes a list of services into the expected format
|
||||||
|
// of a services caveat's value.
|
||||||
|
func encodeServicesCaveatValue(services ...Service) (string, error) {
|
||||||
|
if len(services) == 0 {
|
||||||
|
return "", ErrNoServices
|
||||||
|
}
|
||||||
|
|
||||||
|
var s strings.Builder
|
||||||
|
for i, service := range services {
|
||||||
|
if service.Name == "" {
|
||||||
|
return "", errors.New("missing service name")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmtStr := "%v:%v"
|
||||||
|
if i < len(services)-1 {
|
||||||
|
fmtStr += ","
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&s, fmtStr, service.Name, uint8(service.Tier))
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeServicesCaveatValue decodes a list of services from the expected format
|
||||||
|
// of a services caveat's value.
|
||||||
|
func decodeServicesCaveatValue(s string) ([]Service, error) {
|
||||||
|
if s == "" {
|
||||||
|
return nil, ErrNoServices
|
||||||
|
}
|
||||||
|
|
||||||
|
rawServices := strings.Split(s, ",")
|
||||||
|
services := make([]Service, 0, len(rawServices))
|
||||||
|
for _, rawService := range rawServices {
|
||||||
|
serviceInfo := strings.Split(rawService, ":")
|
||||||
|
if len(serviceInfo) != 2 {
|
||||||
|
return nil, ErrInvalidService
|
||||||
|
}
|
||||||
|
|
||||||
|
name, tierStr := serviceInfo[0], serviceInfo[1]
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidService,
|
||||||
|
"empty name")
|
||||||
|
}
|
||||||
|
tier, err := strconv.Atoi(tierStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidService, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
services = append(services, Service{
|
||||||
|
Name: name,
|
||||||
|
Tier: ServiceTier(tier),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCapabilitiesCaveat creates a new capabilities caveat for the given
|
||||||
|
// service.
|
||||||
|
func NewCapabilitiesCaveat(serviceName string, capabilities string) Caveat {
|
||||||
|
return Caveat{
|
||||||
|
Condition: serviceName + CondCapabilitiesSuffix,
|
||||||
|
Value: capabilities,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package lsat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestServicesCaveatSerialization ensures that we can properly encode/decode
|
||||||
|
// valid services from a caveat and cannot do so for invalid ones.
|
||||||
|
func TestServicesCaveatSerialization(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single service",
|
||||||
|
value: "a:0",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple services",
|
||||||
|
value: "a:0,b:1,c:0",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no services",
|
||||||
|
value: "",
|
||||||
|
err: ErrNoServices,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service missing name",
|
||||||
|
value: ":0",
|
||||||
|
err: ErrInvalidService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service missing tier",
|
||||||
|
value: "a",
|
||||||
|
err: ErrInvalidService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service empty tier",
|
||||||
|
value: "a:",
|
||||||
|
err: ErrInvalidService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service non-numeric tier",
|
||||||
|
value: "a:b",
|
||||||
|
err: ErrInvalidService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty services",
|
||||||
|
value: ",,",
|
||||||
|
err: ErrInvalidService,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
success := t.Run(test.name, func(t *testing.T) {
|
||||||
|
services, err := decodeServicesCaveatValue(test.value)
|
||||||
|
if !errors.Is(err, test.err) {
|
||||||
|
t.Fatalf("expected err \"%v\", got \"%v\"",
|
||||||
|
test.err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
value, _ := encodeServicesCaveatValue(services...)
|
||||||
|
if value != test.value {
|
||||||
|
t.Fatalf("expected encoded services \"%v\", "+
|
||||||
|
"got \"%v\"", test.value, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !success {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue