Skip to content

Commit

Permalink
implement Caveat for resset.Action
Browse files Browse the repository at this point in the history
this allows restricting access without specifying a resource
  • Loading branch information
btoews committed Feb 1, 2024
1 parent ae6905c commit ae080f5
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 112 deletions.
1 change: 1 addition & 0 deletions caveat.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const (
AttestationAuthFlyioUserID
AttestationAuthGitHubUserID
AttestationAuthGoogleUserID
CavAction

// allocate internal blocks of size 255 here
block255Min CaveatType = 1 << 16
Expand Down
25 changes: 24 additions & 1 deletion resset/action.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package resset

import "encoding/json"
import (
"encoding/json"
"fmt"

"github.com/superfly/macaroon"
)

// Action is an RWX-style bitmap of actions that can be taken on a resource
// (eg org, app, machine). An Action can describe the permission limitations
Expand Down Expand Up @@ -114,3 +119,21 @@ func (a *Action) UnmarshalJSON(b []byte) error {
func (a Action) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(new(Action)) }
func (c *Action) CaveatType() macaroon.CaveatType { return macaroon.CavAction }
func (c *Action) Name() string { return "Action" }

// Implements macaroon.Caveat
func (c *Action) Prohibits(a macaroon.Access) error {
rsa, ok := a.(Access)
switch {
case !ok:
return macaroon.ErrInvalidAccess
case !rsa.GetAction().IsSubsetOf(*c):
return fmt.Errorf("%w access %s (%s not allowed)", ErrUnauthorizedForAction, rsa.GetAction(), rsa.GetAction().Remove(*c))
default:
return nil
}
}
19 changes: 19 additions & 0 deletions resset/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package resset

import (
"testing"

"github.com/alecthomas/assert/v2"
"github.com/superfly/macaroon"
)

func TestActionCaveat(t *testing.T) {
cs := macaroon.NewCaveatSet(cavParent(ActionAll, 123), ptr(ActionRead))
assert.NoError(t,
cs.Validate(&testAccess{Action: ActionRead, ParentResource: ptr(uint64(123))}),
)
assert.IsError(t,
cs.Validate(&testAccess{Action: ActionWrite, ParentResource: ptr(uint64(123))}),
ErrUnauthorizedForAction,
)
}
111 changes: 0 additions & 111 deletions resset/if_present_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package resset

import (
"encoding/json"
"errors"
"fmt"
"testing"
"time"

"github.com/alecthomas/assert/v2"
"github.com/superfly/macaroon"
Expand Down Expand Up @@ -55,111 +52,3 @@ func TestIfPresent(t *testing.T) {
no(ErrUnauthorizedForAction, &testAccess{ParentResource: ptr(uint64(123)), Action: ActionWrite}) // action allowed earlier, disallowed by else
no(ErrUnauthorizedForAction, &testAccess{ParentResource: ptr(uint64(123)), Action: ActionControl}) // action only allowed by if
}

func TestCaveatSerialization(t *testing.T) {
cs := macaroon.NewCaveatSet(
&IfPresent{Ifs: macaroon.NewCaveatSet(&macaroon.ValidityWindow{NotBefore: 123, NotAfter: 234}), Else: ActionDelete},
)

b, err := json.Marshal(cs)
assert.NoError(t, err)

cs2 := macaroon.NewCaveatSet()
err = json.Unmarshal(b, cs2)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)

b, err = encode(cs)
assert.NoError(t, err)
cs2, err = macaroon.DecodeCaveats(b)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)
}

const (
cavTestParentResource = iota + macaroon.CavMinUserDefined + 10
cavTestChildResource
)

type testCaveatParentResource struct {
ID uint64
Permission Action
}

func cavParent(permission Action, id uint64) macaroon.Caveat {
return &testCaveatParentResource{id, permission}
}

func init() { macaroon.RegisterCaveatType(&testCaveatParentResource{}) }
func (c *testCaveatParentResource) CaveatType() macaroon.CaveatType { return cavTestParentResource }
func (c *testCaveatParentResource) Name() string { return "ParentResource" }

func (c *testCaveatParentResource) Prohibits(f macaroon.Access) error {
tf, isTestAccess := f.(*testAccess)

switch {
case !isTestAccess:
return macaroon.ErrInvalidAccess
case tf.ParentResource == nil:
return ErrResourceUnspecified
case *tf.ParentResource != c.ID:
return fmt.Errorf("%w resource", ErrUnauthorizedForResource)
case !tf.Action.IsSubsetOf(c.Permission):
return fmt.Errorf("%w action", ErrUnauthorizedForAction)
default:
return nil
}
}

type testCaveatChildResource struct {
ID uint64
Permission Action
}

func cavChild(permission Action, id uint64) macaroon.Caveat {
return &testCaveatChildResource{id, permission}
}

func init() { macaroon.RegisterCaveatType(&testCaveatChildResource{}) }
func (c *testCaveatChildResource) CaveatType() macaroon.CaveatType { return cavTestChildResource }
func (c *testCaveatChildResource) Name() string { return "ChildResource" }

func (c *testCaveatChildResource) Prohibits(f macaroon.Access) error {
tf, isTestAccess := f.(*testAccess)

switch {
case !isTestAccess:
return macaroon.ErrInvalidAccess
case tf.ChildResource == nil:
return ErrResourceUnspecified
case *tf.ChildResource != c.ID:
return fmt.Errorf("%w resource", ErrUnauthorizedForResource)
case !tf.Action.IsSubsetOf(c.Permission):
return fmt.Errorf("%w action", ErrUnauthorizedForAction)
default:
return nil
}
}

type testAccess struct {
Action Action
ParentResource *uint64
ChildResource *uint64
}

var _ Access = (*testAccess)(nil)

func (f *testAccess) GetAction() Action {
return f.Action
}

func (f *testAccess) Now() time.Time {
return time.Now()
}

func (f *testAccess) Validate() error {
if f.ChildResource != nil && f.ParentResource == nil {
return ErrResourceUnspecified
}
return nil
}
120 changes: 120 additions & 0 deletions resset/resset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package resset

import (
"encoding/json"
"fmt"
"testing"
"time"

"github.com/alecthomas/assert/v2"
"github.com/superfly/macaroon"
)

func TestCaveatSerialization(t *testing.T) {
cs := macaroon.NewCaveatSet(
&IfPresent{Ifs: macaroon.NewCaveatSet(&macaroon.ValidityWindow{NotBefore: 123, NotAfter: 234}), Else: ActionDelete},
ptr(ActionRead),
)

b, err := json.Marshal(cs)
assert.NoError(t, err)

cs2 := macaroon.NewCaveatSet()
err = json.Unmarshal(b, cs2)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)

b, err = encode(cs)
assert.NoError(t, err)
cs2, err = macaroon.DecodeCaveats(b)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)
}

const (
cavTestParentResource = iota + macaroon.CavMinUserDefined + 10
cavTestChildResource
)

type testCaveatParentResource struct {
ID uint64
Permission Action
}

func cavParent(permission Action, id uint64) macaroon.Caveat {
return &testCaveatParentResource{id, permission}
}

func init() { macaroon.RegisterCaveatType(&testCaveatParentResource{}) }
func (c *testCaveatParentResource) CaveatType() macaroon.CaveatType { return cavTestParentResource }
func (c *testCaveatParentResource) Name() string { return "ParentResource" }

func (c *testCaveatParentResource) Prohibits(f macaroon.Access) error {
tf, isTestAccess := f.(*testAccess)

switch {
case !isTestAccess:
return macaroon.ErrInvalidAccess
case tf.ParentResource == nil:
return ErrResourceUnspecified
case *tf.ParentResource != c.ID:
return fmt.Errorf("%w resource", ErrUnauthorizedForResource)
case !tf.Action.IsSubsetOf(c.Permission):
return fmt.Errorf("%w action", ErrUnauthorizedForAction)
default:
return nil
}
}

type testCaveatChildResource struct {
ID uint64
Permission Action
}

func cavChild(permission Action, id uint64) macaroon.Caveat {
return &testCaveatChildResource{id, permission}
}

func init() { macaroon.RegisterCaveatType(&testCaveatChildResource{}) }
func (c *testCaveatChildResource) CaveatType() macaroon.CaveatType { return cavTestChildResource }
func (c *testCaveatChildResource) Name() string { return "ChildResource" }

func (c *testCaveatChildResource) Prohibits(f macaroon.Access) error {
tf, isTestAccess := f.(*testAccess)

switch {
case !isTestAccess:
return macaroon.ErrInvalidAccess
case tf.ChildResource == nil:
return ErrResourceUnspecified
case *tf.ChildResource != c.ID:
return fmt.Errorf("%w resource", ErrUnauthorizedForResource)
case !tf.Action.IsSubsetOf(c.Permission):
return fmt.Errorf("%w action", ErrUnauthorizedForAction)
default:
return nil
}
}

type testAccess struct {
Action Action
ParentResource *uint64
ChildResource *uint64
}

var _ Access = (*testAccess)(nil)

func (f *testAccess) GetAction() Action {
return f.Action
}

func (f *testAccess) Now() time.Time {
return time.Now()
}

func (f *testAccess) Validate() error {
if f.ChildResource != nil && f.ParentResource == nil {
return ErrResourceUnspecified
}
return nil
}

0 comments on commit ae080f5

Please sign in to comment.