diff --git a/caveat.go b/caveat.go index 37667cb..fa8eac7 100644 --- a/caveat.go +++ b/caveat.go @@ -39,6 +39,7 @@ const ( AttestationAuthFlyioUserID AttestationAuthGitHubUserID AttestationAuthGoogleUserID + CavAction // allocate internal blocks of size 255 here block255Min CaveatType = 1 << 16 diff --git a/resset/action.go b/resset/action.go index ee0eb98..f9896cf 100644 --- a/resset/action.go +++ b/resset/action.go @@ -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 @@ -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 + } +} diff --git a/resset/action_test.go b/resset/action_test.go new file mode 100644 index 0000000..950db88 --- /dev/null +++ b/resset/action_test.go @@ -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, + ) +} diff --git a/resset/if_present_test.go b/resset/if_present_test.go index 443d9c7..7cb17b6 100644 --- a/resset/if_present_test.go +++ b/resset/if_present_test.go @@ -1,11 +1,8 @@ package resset import ( - "encoding/json" "errors" - "fmt" "testing" - "time" "github.com/alecthomas/assert/v2" "github.com/superfly/macaroon" @@ -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 -} diff --git a/resset/resset_test.go b/resset/resset_test.go new file mode 100644 index 0000000..343a70e --- /dev/null +++ b/resset/resset_test.go @@ -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 +}