From f04e4cbf62154b568348f969ce7e0ab433ea03fb Mon Sep 17 00:00:00 2001 From: Tim <165851289+timflyio@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:06:17 -1000 Subject: [PATCH] tim resset extended action (#37) * resset: allow custom Action types in ResourceSet --------- Co-authored-by: btoews --- flyio/access.go | 2 +- flyio/caveat_set_test.go | 64 +++++++++++++-------------- flyio/caveats.go | 20 ++++----- flyio/caveats_test.go | 2 +- internal/test-vectors/test_vectors.go | 24 +++++----- resset/action.go | 14 +----- resset/example_test.go | 4 +- resset/if_present.go | 4 +- resset/resource_set.go | 50 ++++++++++++++------- resset/resource_set_test.go | 12 ++--- resset/resset_test.go | 4 +- 11 files changed, 104 insertions(+), 96 deletions(-) diff --git a/flyio/access.go b/flyio/access.go index ca70201..6698758 100644 --- a/flyio/access.go +++ b/flyio/access.go @@ -168,7 +168,7 @@ func (a *Access) GetPermittedRoles() []Role { return []Role{RoleMember} } - if memberAllowed, ok := MemberFeatures[*a.Feature]; ok && a.Action.IsSubsetOf(memberAllowed) { + if memberAllowed, ok := MemberFeatures[*a.Feature]; ok && resset.IsSubsetOf(a.Action, memberAllowed) { return []Role{RoleMember} } diff --git a/flyio/caveat_set_test.go b/flyio/caveat_set_test.go index 43c2845..92a4faa 100644 --- a/flyio/caveat_set_test.go +++ b/flyio/caveat_set_test.go @@ -13,7 +13,7 @@ import ( func TestScopeOrganizationID(t *testing.T) { // error if not org constrained _, err := OrganizationScope(macaroon.NewCaveatSet( - &Apps{resset.ResourceSet[uint64]{123: resset.ActionAll}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{123: resset.ActionAll}}, )) assert.True(t, errors.Is(err, macaroon.ErrUnauthorized)) @@ -52,7 +52,7 @@ func TestScopeOrganizationID(t *testing.T) { // ok - no permission allowed by IfPresent _, err = OrganizationScope(macaroon.NewCaveatSet( &Organization{ID: 123, Mask: resset.ActionAll}, - &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64]{123: resset.ActionAll}})}, + &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64, resset.Action]{123: resset.ActionAll}})}, )) assert.NoError(t, err) @@ -60,7 +60,7 @@ func TestScopeOrganizationID(t *testing.T) { // ok - some child resource is required id, err = OrganizationScope(macaroon.NewCaveatSet( &Organization{ID: 123, Mask: resset.ActionAll}, - &Apps{resset.ResourceSet[uint64]{234: resset.ActionAll}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{234: resset.ActionAll}}, )) assert.NoError(t, err) @@ -77,8 +77,8 @@ func TestAppIDs(t *testing.T) { // try each case with a id=* caveat, which should be a noop for scoping. bases := [][]macaroon.Caveat{ {}, - {&Apps{resset.ResourceSet[uint64]{0: resset.ActionAll}}}, - {&Apps{resset.ResourceSet[uint64]{0: resset.ActionNone}}}, + {&Apps{resset.ResourceSet[uint64, resset.Action]{0: resset.ActionAll}}}, + {&Apps{resset.ResourceSet[uint64, resset.Action]{0: resset.ActionNone}}}, } for _, base := range bases { @@ -98,24 +98,24 @@ func TestAppIDs(t *testing.T) { // {} for disjoint Apps ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}}, - &Apps{resset.ResourceSet[uint64]{2: resset.ActionRead}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{2: resset.ActionRead}}, )...)) assert.Equal(t, empty, ids) // {} for disjoint Apps/IfPresent ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}}, - &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64]{2: resset.ActionRead}})}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}}, + &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64, resset.Action]{2: resset.ActionRead}})}, )...)) assert.Equal(t, empty, ids) // {} for disjoint IfPresents ids = AppScope(macaroon.NewCaveatSet(append(base, - &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}})}, - &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64]{2: resset.ActionRead}})}, + &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}})}, + &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64, resset.Action]{2: resset.ActionRead}})}, )...)) assert.Equal(t, empty, ids) @@ -127,44 +127,44 @@ func TestAppIDs(t *testing.T) { // nil if app unconstrained and has unrelated caveats ids = AppScope(macaroon.NewCaveatSet(append(base, - &resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string]{"wg": resset.ActionAll}})}, + &resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string, resset.Action]{"wg": resset.ActionAll}})}, )...)) assert.Equal(t, unconstrained, ids) // {123} if app constrained ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}}, )...)) assert.Equal(t, constrained, ids) // {123} if no permissions allowed on app ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionNone}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionNone}}, )...)) assert.Equal(t, constrained, ids) // {123} if disjoint permissions allowed on app ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}}, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionWrite}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionWrite}}, )...)) assert.Equal(t, constrained, ids) // {123} if app constrained by IfPresent ids = AppScope(macaroon.NewCaveatSet(append(base, - &resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64]{1: resset.ActionRead}})}, + &resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionRead}})}, )...)) assert.Equal(t, constrained, ids) // {123} if app constrained and other IfPresent ids = AppScope(macaroon.NewCaveatSet(append(base, - &Apps{resset.ResourceSet[uint64]{1: resset.ActionAll}}, - &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string]{"wg": resset.ActionAll}})}, + &Apps{resset.ResourceSet[uint64, resset.Action]{1: resset.ActionAll}}, + &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string, resset.Action]{"wg": resset.ActionAll}})}, )...)) assert.Equal(t, constrained, ids) @@ -186,40 +186,40 @@ func TestClusters(t *testing.T) { assert.Equal(t, empty, ids) // {} for disjoint Clusters - ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}}, &Clusters{resset.ResourceSet[string]{"2": resset.ActionRead}})) + ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}}, &Clusters{resset.ResourceSet[string, resset.Action]{"2": resset.ActionRead}})) assert.Equal(t, empty, ids) // {} for disjoint Clusters/IfPresent - ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}}, &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"2": resset.ActionRead}})})) + ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}}, &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"2": resset.ActionRead}})})) assert.Equal(t, empty, ids) // {} for disjoint IfPresents ids = ClusterScope(macaroon.NewCaveatSet( - &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}})}, - &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"2": resset.ActionRead}})}, + &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}})}, + &resset.IfPresent{Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"2": resset.ActionRead}})}, )) assert.Equal(t, empty, ids) // {123} if cluster constrained - ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}})) + ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}})) assert.Equal(t, constrained, ids) // {123} if no permissions allowed on cluster - ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionNone}})) + ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionNone}})) assert.Equal(t, constrained, ids) // {123} if disjoint permissions allowed on cluster - ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}}, &Clusters{resset.ResourceSet[string]{"1": resset.ActionWrite}})) + ids = ClusterScope(macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}}, &Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionWrite}})) assert.Equal(t, constrained, ids) // {123} if cluster constrained by IfPresent - ids = ClusterScope(macaroon.NewCaveatSet(&resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string]{"1": resset.ActionRead}})})) + ids = ClusterScope(macaroon.NewCaveatSet(&resset.IfPresent{Else: resset.ActionRead, Ifs: macaroon.NewCaveatSet(&Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionRead}})})) assert.Equal(t, constrained, ids) // {123} if cluster constrained and other IfPresent ids = ClusterScope(macaroon.NewCaveatSet( - &Clusters{resset.ResourceSet[string]{"1": resset.ActionAll}}, - &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string]{"wg": resset.ActionAll}})}, + &Clusters{resset.ResourceSet[string, resset.Action]{"1": resset.ActionAll}}, + &resset.IfPresent{Else: resset.ActionNone, Ifs: macaroon.NewCaveatSet(&FeatureSet{resset.ResourceSet[string, resset.Action]{"wg": resset.ActionAll}})}, )) assert.Equal(t, constrained, ids) } @@ -227,7 +227,7 @@ func TestClusters(t *testing.T) { func TestAppsAllowing(t *testing.T) { // OrganizationScope error _, _, err := AppsAllowing(macaroon.NewCaveatSet( - &Apps{resset.ResourceSet[uint64]{123: resset.ActionAll}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{123: resset.ActionAll}}, ), resset.ActionNone) assert.True(t, errors.Is(err, macaroon.ErrUnauthorized)) @@ -250,7 +250,7 @@ func TestAppsAllowing(t *testing.T) { // action prohibited on all apps _, _, err = AppsAllowing(macaroon.NewCaveatSet( &Organization{ID: 987, Mask: resset.ActionAll}, - &Apps{resset.ResourceSet[uint64]{123: resset.ActionRead}}, + &Apps{resset.ResourceSet[uint64, resset.Action]{123: resset.ActionRead}}, ), resset.ActionWrite) assert.True(t, errors.Is(err, resset.ErrUnauthorizedForAction)) @@ -267,7 +267,7 @@ func TestAppsAllowing(t *testing.T) { // action allowed on some apps orgID, appIDs, err = AppsAllowing(macaroon.NewCaveatSet( &Organization{ID: 987, Mask: resset.ActionAll}, - &Apps{Apps: resset.ResourceSet[uint64]{123: resset.ActionAll, 234: resset.ActionWrite, 345: resset.ActionRead}}, + &Apps{Apps: resset.ResourceSet[uint64, resset.Action]{123: resset.ActionAll, 234: resset.ActionWrite, 345: resset.ActionRead}}, ), resset.ActionWrite) assert.NoError(t, err) diff --git a/flyio/caveats.go b/flyio/caveats.go index b2158b0..63e4dd9 100644 --- a/flyio/caveats.go +++ b/flyio/caveats.go @@ -75,8 +75,8 @@ func (c *Organization) Prohibits(a macaroon.Access) error { return fmt.Errorf("%w org", resset.ErrResourceUnspecified) case c.ID != resset.ZeroID[uint64]() && c.ID != *f.GetOrgID(): return fmt.Errorf("%w org %d, only %d", resset.ErrUnauthorizedForResource, *f.GetOrgID(), c.ID) - case !f.GetAction().IsSubsetOf(c.Mask): - return fmt.Errorf("%w access %s (%s not allowed)", resset.ErrUnauthorizedForAction, f.GetAction(), f.GetAction().Remove(c.Mask)) + case !resset.IsSubsetOf(f.GetAction(), c.Mask): + return fmt.Errorf("%w access %s (%s not allowed)", resset.ErrUnauthorizedForAction, f.GetAction(), resset.Remove(f.GetAction(), c.Mask)) default: return nil } @@ -86,7 +86,7 @@ func (c *Organization) Prohibits(a macaroon.Access) error { // only with the listed apps, regardless of what the token says. Additional Apps can be added, // but they can only narrow, not expand, which apps (or access levels) can be reached from the token. type Apps struct { - Apps resset.ResourceSet[uint64] `json:"apps"` + Apps resset.ResourceSet[uint64, resset.Action] `json:"apps"` } func init() { @@ -106,7 +106,7 @@ func (c *Apps) Prohibits(a macaroon.Access) error { } type Volumes struct { - Volumes resset.ResourceSet[string] `json:"volumes"` + Volumes resset.ResourceSet[string, resset.Action] `json:"volumes"` } func init() { macaroon.RegisterCaveatType(&Volumes{}) } @@ -122,7 +122,7 @@ func (c *Volumes) Prohibits(a macaroon.Access) error { } type Machines struct { - Machines resset.ResourceSet[string] `json:"machines"` + Machines resset.ResourceSet[string, resset.Action] `json:"machines"` } func init() { macaroon.RegisterCaveatType(&Machines{}) } @@ -138,7 +138,7 @@ func (c *Machines) Prohibits(a macaroon.Access) error { } type MachineFeatureSet struct { - Features resset.ResourceSet[string] `json:"features"` + Features resset.ResourceSet[string, resset.Action] `json:"features"` } func init() { macaroon.RegisterCaveatType(&MachineFeatureSet{}) } @@ -159,7 +159,7 @@ func (c *MachineFeatureSet) Prohibits(a macaroon.Access) error { // individually with a Networks caveat. The feature name is free-form and more // should be addded as it makes sense. type FeatureSet struct { - Features resset.ResourceSet[string] `json:"features"` + Features resset.ResourceSet[string, resset.Action] `json:"features"` } func init() { macaroon.RegisterCaveatType(&FeatureSet{}) } @@ -225,7 +225,7 @@ func (c *IsUser) Prohibits(a macaroon.Access) error { // Clusters is a set of Cluster caveats, with their RWX access levels. Clusters // belong to the "litefs-cloud" org-feature. type Clusters struct { - Clusters resset.ResourceSet[string] `json:"clusters"` + Clusters resset.ResourceSet[string, resset.Action] `json:"clusters"` } func init() { macaroon.RegisterCaveatType(&Clusters{}) } @@ -389,7 +389,7 @@ func (c *Commands) Prohibits(a macaroon.Access) error { } type AppFeatureSet struct { - Features resset.ResourceSet[string] `json:"features"` + Features resset.ResourceSet[string, resset.Action] `json:"features"` } func init() { macaroon.RegisterCaveatType(&AppFeatureSet{}) } @@ -410,7 +410,7 @@ func (c *AppFeatureSet) Prohibits(a macaroon.Access) error { // provider (e.g. `https://storage.fly/my_bucket`), or a object within a bucket // (e.g. `https://storage.fly/my_bucket/my_file`). type StorageObjects struct { - Prefixes resset.ResourceSet[resset.Prefix] `json:"storage_objects"` + Prefixes resset.ResourceSet[resset.Prefix, resset.Action] `json:"storage_objects"` } func init() { diff --git a/flyio/caveats_test.go b/flyio/caveats_test.go index cb8f38d..04f8f07 100644 --- a/flyio/caveats_test.go +++ b/flyio/caveats_test.go @@ -12,7 +12,7 @@ import ( func TestCaveatSerialization(t *testing.T) { cs := macaroon.NewCaveatSet( &Organization{ID: 123, Mask: resset.ActionRead}, - &Apps{Apps: resset.ResourceSet[uint64]{123: resset.ActionRead}}, + &Apps{Apps: resset.ResourceSet[uint64, resset.Action]{123: resset.ActionRead}}, &FeatureSet{Features: resset.New(resset.ActionRead, "123")}, &Volumes{Volumes: resset.New(resset.ActionRead, "123")}, &Machines{Machines: resset.New(resset.ActionRead, "123")}, diff --git a/internal/test-vectors/test_vectors.go b/internal/test-vectors/test_vectors.go index c855b03..6f1cb59 100644 --- a/internal/test-vectors/test_vectors.go +++ b/internal/test-vectors/test_vectors.go @@ -110,18 +110,18 @@ var caveats = macaroon.NewCaveatSet( ptr(uint64Caveat(123)), &sliceCaveat{1, 2, 3}, &mapCaveat{"c": "c", "a": "a", "b": "b"}, - &intResourceSetCaveat{Body: resset.ResourceSet[uint64]{3: resset.ActionAll, 1: resset.ActionAll, 2: resset.ActionAll}}, - &stringResourceSetCaveat{Body: resset.ResourceSet[string]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}}, - &prefixResourceSetCaveat{Body: resset.ResourceSet[resset.Prefix]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}}, + &intResourceSetCaveat{Body: resset.ResourceSet[uint64, resset.Action]{3: resset.ActionAll, 1: resset.ActionAll, 2: resset.ActionAll}}, + &stringResourceSetCaveat{Body: resset.ResourceSet[string, resset.Action]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}}, + &prefixResourceSetCaveat{Body: resset.ResourceSet[resset.Prefix, resset.Action]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}}, &structCaveat{ StringField: "foo", IntField: -123, UintField: 123, SliceField: []byte{1, 2, 3}, MapField: map[string]string{"c": "c", "a": "a", "b": "b"}, - IntResourceSetField: resset.ResourceSet[uint64]{3: resset.ActionAll, 1: resset.ActionAll, 2: resset.ActionAll}, - StringResourceSetField: resset.ResourceSet[string]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}, - PrefixResourceSetField: resset.ResourceSet[resset.Prefix]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}, + IntResourceSetField: resset.ResourceSet[uint64, resset.Action]{3: resset.ActionAll, 1: resset.ActionAll, 2: resset.ActionAll}, + StringResourceSetField: resset.ResourceSet[string, resset.Action]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}, + PrefixResourceSetField: resset.ResourceSet[resset.Prefix, resset.Action]{"c": resset.ActionAll, "a": resset.ActionAll, "b": resset.ActionAll}, }, auth.RequireUser(123), auth.RequireOrganization(123), @@ -205,7 +205,7 @@ func (c mapCaveat) EncodeMsgpack(enc *msgpack.Encoder) error { } type intResourceSetCaveat struct { - Body resset.ResourceSet[uint64] + Body resset.ResourceSet[uint64, resset.Action] } func init() { macaroon.RegisterCaveatType(new(intResourceSetCaveat)) } @@ -214,7 +214,7 @@ func (c *intResourceSetCaveat) Name() string { return "IntR func (c *intResourceSetCaveat) Prohibits(f macaroon.Access) error { return nil } type stringResourceSetCaveat struct { - Body resset.ResourceSet[string] + Body resset.ResourceSet[string, resset.Action] } func init() { macaroon.RegisterCaveatType(new(stringResourceSetCaveat)) } @@ -223,7 +223,7 @@ func (c *stringResourceSetCaveat) Name() string { return "S func (c *stringResourceSetCaveat) Prohibits(f macaroon.Access) error { return nil } type prefixResourceSetCaveat struct { - Body resset.ResourceSet[resset.Prefix] + Body resset.ResourceSet[resset.Prefix, resset.Action] } func init() { macaroon.RegisterCaveatType(new(prefixResourceSetCaveat)) } @@ -237,9 +237,9 @@ type structCaveat struct { UintField uint64 SliceField []byte MapField map[string]string - IntResourceSetField resset.ResourceSet[uint64] - StringResourceSetField resset.ResourceSet[string] - PrefixResourceSetField resset.ResourceSet[resset.Prefix] + IntResourceSetField resset.ResourceSet[uint64, resset.Action] + StringResourceSetField resset.ResourceSet[string, resset.Action] + PrefixResourceSetField resset.ResourceSet[resset.Prefix, resset.Action] } func init() { macaroon.RegisterCaveatType(new(structCaveat)) } diff --git a/resset/action.go b/resset/action.go index f9896cf..3602bdc 100644 --- a/resset/action.go +++ b/resset/action.go @@ -14,16 +14,6 @@ import ( // implementations. type Action uint16 -// IsSubsetOf returns wether all bits in p are set in other. -func (a Action) IsSubsetOf(other Action) bool { - return a&other == a -} - -// Remove returns the bits in p but not other -func (a Action) Remove(other Action) Action { - return (a & other) ^ a -} - const ( // ActionRead indicates reading attributes of the specified objects. ActionRead Action = 1 << iota @@ -131,8 +121,8 @@ func (c *Action) Prohibits(a macaroon.Access) error { 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)) + case !IsSubsetOf(rsa.GetAction(), *c): + return fmt.Errorf("%w access %s (%s not allowed)", ErrUnauthorizedForAction, rsa.GetAction(), Remove(rsa.GetAction(), *c)) default: return nil } diff --git a/resset/example_test.go b/resset/example_test.go index 4ebe555..d3170d2 100644 --- a/resset/example_test.go +++ b/resset/example_test.go @@ -15,7 +15,7 @@ const ( // implements macaroon.Caveat. Constrains access to widgets type Widgets struct { - Widgets ResourceSet[string] `json:"widgets"` + Widgets ResourceSet[string, Action] `json:"widgets"` } // register our Widgets caveat with the macaroons library so it's able to @@ -86,7 +86,7 @@ func Example() { // constrain the macaroon to accessing widget "foo" with any action or // reading widget "bar". - err = userMacaroon.Add(&Widgets{ResourceSet[string]{ + err = userMacaroon.Add(&Widgets{ResourceSet[string, Action]{ "foo": ActionAll, "bar": ActionRead, }}) diff --git a/resset/if_present.go b/resset/if_present.go index 90e8a61..a0b05f6 100644 --- a/resset/if_present.go +++ b/resset/if_present.go @@ -46,8 +46,8 @@ func (c *IfPresent) Prohibits(a macaroon.Access) error { } } - if !ifBranch && !ra.GetAction().IsSubsetOf(c.Else) { - return fmt.Errorf("%w access %s (%s not allowed)", ErrUnauthorizedForAction, ra.GetAction(), ra.GetAction().Remove(c.Else)) + if !ifBranch && !IsSubsetOf(ra.GetAction(), c.Else) { + return fmt.Errorf("%w access %s (%s not allowed)", ErrUnauthorizedForAction, ra.GetAction(), Remove(ra.GetAction(), c.Else)) } return err diff --git a/resset/resource_set.go b/resset/resource_set.go index 57127c6..30fd315 100644 --- a/resset/resource_set.go +++ b/resset/resource_set.go @@ -6,12 +6,28 @@ import ( "github.com/superfly/macaroon" msgpack "github.com/vmihailenco/msgpack/v5" + "golang.org/x/exp/constraints" "golang.org/x/exp/maps" "golang.org/x/exp/slices" ) type ID interface { - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~string + constraints.Integer | ~string +} + +type BitMask interface { + constraints.Unsigned + String() string +} + +// IsSubsetOf returns wether all bits in a are set in b. +func IsSubsetOf[M BitMask](a, b M) bool { + return a&b == a +} + +// Remove returns the bits in a but not b +func Remove[M BitMask](a, b M) M { + return (a & b) ^ a } // ZeroID gets the zero value (0, or "") for a resource. This is used to refer @@ -26,26 +42,26 @@ func ZeroID[I ID]() (ret I) { // marshalling. As a result, they should be wrapped in a struct rather than // simply aliasing the type. For example, don't do this: // -// type myCaveat resset.ResourceSet[uint64] +// type myCaveat resset.ResourceSet[uint64, Action] // // Instead, do this: // // type myCaveat struct { -// Resources resset.ResourceSet[uint64] +// Resources resset.ResourceSet[uint64, Action] // } -type ResourceSet[I ID] map[I]Action +type ResourceSet[I ID, M BitMask] map[I]M -func New[I ID](p Action, ids ...I) ResourceSet[I] { - ret := make(ResourceSet[I], len(ids)) +func New[I ID, M BitMask](m M, ids ...I) ResourceSet[I, M] { + ret := make(ResourceSet[I, M], len(ids)) for _, id := range ids { - ret[id] = p + ret[id] = m } return ret } -func (rs ResourceSet[I]) Prohibits(id *I, action Action, resourceType string) error { +func (rs ResourceSet[I, M]) Prohibits(id *I, action M, resourceType string) error { if err := rs.validate(); err != nil { return err } @@ -55,8 +71,10 @@ func (rs ResourceSet[I]) Prohibits(id *I, action Action, resourceType string) er var ( foundPerm = false - perm = ActionAll zeroID I + zeroM M + maxM = zeroM - 1 + perm = maxM allowedIDs []I ) @@ -79,18 +97,18 @@ func (rs ResourceSet[I]) Prohibits(id *I, action Action, resourceType string) er return fmt.Errorf("%w %s %v (only %v)", ErrUnauthorizedForResource, resourceType, *id, allowedIDs) } - if !action.IsSubsetOf(perm) { - return fmt.Errorf("%w access %s on %s (%s not allowed)", ErrUnauthorizedForAction, action, resourceType, action.Remove(perm)) + if !IsSubsetOf(action, perm) { + return fmt.Errorf("%w access %s on %s (%s not allowed)", ErrUnauthorizedForAction, action, resourceType, Remove(action, perm)) } return nil } -var _ msgpack.CustomEncoder = ResourceSet[uint64]{} -var _ msgpack.CustomEncoder = ResourceSet[int32]{} -var _ msgpack.CustomEncoder = ResourceSet[string]{} +var _ msgpack.CustomEncoder = ResourceSet[uint64, Action]{} +var _ msgpack.CustomEncoder = ResourceSet[int32, Action]{} +var _ msgpack.CustomEncoder = ResourceSet[string, Action]{} -func (rs ResourceSet[I]) EncodeMsgpack(enc *msgpack.Encoder) error { +func (rs ResourceSet[I, M]) EncodeMsgpack(enc *msgpack.Encoder) error { if err := enc.EncodeMapLen(len(rs)); err != nil { return err } @@ -112,7 +130,7 @@ func (rs ResourceSet[I]) EncodeMsgpack(enc *msgpack.Encoder) error { return nil } -func (rs ResourceSet[ID]) validate() error { +func (rs ResourceSet[ID, M]) validate() error { var zeroID ID if _, hasZero := rs[zeroID]; hasZero && len(rs) != 1 { return fmt.Errorf("%w: cannot specify zero ID along with other IDs", macaroon.ErrBadCaveat) diff --git a/resset/resource_set_test.go b/resset/resource_set_test.go index 78988fd..801f33d 100644 --- a/resset/resource_set_test.go +++ b/resset/resource_set_test.go @@ -13,7 +13,7 @@ import ( func TestResourceSet(t *testing.T) { zero := ZeroID[string]() - rs := &ResourceSet[string]{ + rs := &ResourceSet[string, Action]{ "foo": ActionRead | ActionWrite, "bar": ActionWrite, } @@ -28,7 +28,7 @@ func TestResourceSet(t *testing.T) { func TestZeroID(t *testing.T) { zero := ZeroID[string]() - rs := &ResourceSet[string]{zero: ActionRead} + rs := &ResourceSet[string, Action]{zero: ActionRead} assert.NoError(t, rs.Prohibits(ptr("foo"), ActionRead, "test resource")) assert.NoError(t, rs.Prohibits(ptr(zero), ActionRead, "test resource")) @@ -37,7 +37,7 @@ func TestZeroID(t *testing.T) { assert.True(t, errors.Is(rs.Prohibits(ptr("foo"), ActionWrite, "test resource"), ErrUnauthorizedForAction)) assert.True(t, errors.Is(rs.Prohibits(ptr(zero), ActionWrite, "test resource"), ErrUnauthorizedForAction)) - rs = &ResourceSet[string]{ + rs = &ResourceSet[string, Action]{ zero: ActionRead | ActionWrite, "bar": ActionWrite, } @@ -55,7 +55,7 @@ func TestResourceSetJSON(t *testing.T) { assert.NoError(t, err) assert.Equal(t, rsj2, rsj) - rs2 := ResourceSet[uint64]{} + rs2 := ResourceSet[uint64, Action]{} assert.NoError(t, json.Unmarshal(rsj, &rs2)) assert.Equal(t, rs, rs2) } @@ -66,7 +66,7 @@ func TestResourceSetMessagePack(t *testing.T) { rsm, err := encode(rs) assert.NoError(t, err) - rs2 := ResourceSet[uint64]{} + rs2 := ResourceSet[uint64, Action]{} assert.NoError(t, msgpack.Unmarshal(rsm, &rs2)) assert.Equal(t, rs, rs2) @@ -88,7 +88,7 @@ func TestResourceSetMessagePack(t *testing.T) { rsm3, err := encode(map[uint64]Action{1: ActionRead, 2: ActionRead, 3: ActionRead}) assert.NoError(t, err) - rs3 := ResourceSet[uint64]{} + rs3 := ResourceSet[uint64, Action]{} assert.NoError(t, msgpack.Unmarshal(rsm3, &rs3)) assert.Equal(t, rs, rs3) } diff --git a/resset/resset_test.go b/resset/resset_test.go index 343a70e..229e8e8 100644 --- a/resset/resset_test.go +++ b/resset/resset_test.go @@ -59,7 +59,7 @@ func (c *testCaveatParentResource) Prohibits(f macaroon.Access) error { return ErrResourceUnspecified case *tf.ParentResource != c.ID: return fmt.Errorf("%w resource", ErrUnauthorizedForResource) - case !tf.Action.IsSubsetOf(c.Permission): + case !IsSubsetOf(tf.Action, c.Permission): return fmt.Errorf("%w action", ErrUnauthorizedForAction) default: return nil @@ -89,7 +89,7 @@ func (c *testCaveatChildResource) Prohibits(f macaroon.Access) error { return ErrResourceUnspecified case *tf.ChildResource != c.ID: return fmt.Errorf("%w resource", ErrUnauthorizedForResource) - case !tf.Action.IsSubsetOf(c.Permission): + case !IsSubsetOf(tf.Action, c.Permission): return fmt.Errorf("%w action", ErrUnauthorizedForAction) default: return nil