diff --git a/caveat.go b/caveat.go index b33c409..eb04385 100644 --- a/caveat.go +++ b/caveat.go @@ -17,6 +17,7 @@ const ( _ // deprecated CavFlyioVolumes CavFlyioApps + CavFlyioAppPrefixes CavValidityWindow CavFlyioFeatureSet CavFlyioMutations diff --git a/flyio/access.go b/flyio/access.go index 78d13b5..6e3b427 100644 --- a/flyio/access.go +++ b/flyio/access.go @@ -13,6 +13,7 @@ type Access struct { Action resset.Action `json:"action,omitempty"` OrgID *uint64 `json:"orgid,omitempty"` AppID *uint64 `json:"appid,omitempty"` + AppName *string `json:"appname,omitempty"` AppFeature *string `json:"app_feature,omitempty"` Feature *string `json:"feature,omitempty"` Volume *string `json:"volume,omitempty"` @@ -201,6 +202,18 @@ var _ AppIDGetter = (*Access)(nil) // GetAppID implements AppIDGetter. func (a *Access) GetAppID() *uint64 { return a.AppID } +// AppNameGetter is an interface allowing other packages to implement Accesses +// that work with Caveats defined in this package. +type AppNameGetter interface { + resset.Access + GetAppName() *string +} + +// GetAppName implements AppNameGetter. +func (a *Access) GetAppName() *string { return a.AppName } + +var _ AppNameGetter = (*Access)(nil) + // AppFeatureGetter is an interface allowing other packages to implement // Accesses that work with Caveats defined in this package. type AppFeatureGetter interface { diff --git a/flyio/caveats.go b/flyio/caveats.go index 6befa60..8834eae 100644 --- a/flyio/caveats.go +++ b/flyio/caveats.go @@ -14,6 +14,7 @@ const ( CavOrganization = macaroon.CavFlyioOrganization CavVolumes = macaroon.CavFlyioVolumes CavApps = macaroon.CavFlyioApps + CavAppPrefixes = macaroon.CavFlyioAppPrefixes CavFeatureSet = macaroon.CavFlyioFeatureSet CavMutations = macaroon.CavFlyioMutations CavMachines = macaroon.CavFlyioMachines @@ -106,6 +107,30 @@ func (c *Apps) Prohibits(a macaroon.Access) error { return c.Apps.Prohibits(f.GetAppID(), f.GetAction(), "app") } +// AppPrefixes is a set of prefix caveats, with their RWX access levels. A token with this set can be used +// only with apps that have a name matching the prefix. +type AppPrefixes struct { + Prefixes resset.ResourceSet[resset.Prefix, resset.Action] `json:"app_prefixes"` +} + +func init() { macaroon.RegisterCaveatType(&AppPrefixes{}) } +func (c *AppPrefixes) CaveatType() macaroon.CaveatType { return CavAppPrefixes } +func (c *AppPrefixes) Name() string { return "AppPrefixes" } + +func (c *AppPrefixes) Prohibits(a macaroon.Access) error { + f, isFlyioAccess := a.(AppNameGetter) + if !isFlyioAccess { + return fmt.Errorf("%w: access isnt AppNameGetter", macaroon.ErrInvalidAccess) + } + + var prefix *resset.Prefix + if name := f.GetAppName(); name != nil { + p := resset.Prefix(*name) + prefix = &p + } + return c.Prefixes.Prohibits(prefix, f.GetAction(), "app") +} + type Volumes struct { Volumes resset.ResourceSet[string, resset.Action] `json:"volumes"` } diff --git a/flyio/caveats.md b/flyio/caveats.md index f58de5d..f6ab69d 100644 --- a/flyio/caveats.md +++ b/flyio/caveats.md @@ -195,6 +195,17 @@ being requested is in the caveat, and if the access being requested is a subset allowed for that resource. They are not relevant (return `ErrResourceUnspecified`) if the access request does not specify the corresponding resource. +``` + { + "type": "AppPrefixes", + "body": { + "app_prefixes": { + "prefix-": "rw" + } + } + }, +``` + ``` { "type": "Volumes", diff --git a/flyio/caveats_test.go b/flyio/caveats_test.go index 04f8f07..339a94c 100644 --- a/flyio/caveats_test.go +++ b/flyio/caveats_test.go @@ -13,6 +13,7 @@ func TestCaveatSerialization(t *testing.T) { cs := macaroon.NewCaveatSet( &Organization{ID: 123, Mask: resset.ActionRead}, &Apps{Apps: resset.ResourceSet[uint64, resset.Action]{123: resset.ActionRead}}, + &AppPrefixes{Prefixes: resset.ResourceSet[resset.Prefix, resset.Action]{"foo-": resset.ActionRead}}, &FeatureSet{Features: resset.New(resset.ActionRead, "123")}, &Volumes{Volumes: resset.New(resset.ActionRead, "123")}, &Machines{Machines: resset.New(resset.ActionRead, "123")}, @@ -271,3 +272,85 @@ func TestCommands(t *testing.T) { Action: resset.ActionWrite, }, resset.ErrUnauthorizedForAction) } + +func TestAppPrefixes(t *testing.T) { + sptr := func(s string) *string { return &s } + yes := func(cs *macaroon.CaveatSet, access *Access) { + t.Helper() + assert.NoError(t, cs.Validate(access)) + } + + no := func(cs *macaroon.CaveatSet, access *Access, target error) { + t.Helper() + err := cs.Validate(access) + assert.Error(t, err) + assert.IsError(t, err, target) + } + + cs := macaroon.NewCaveatSet(&AppPrefixes{ + Prefixes: resset.ResourceSet[resset.Prefix, resset.Action]{ + "foo-": resset.ActionRead | resset.ActionWrite, + "bar-": resset.ActionRead, + }, + }) + + // foo-* has rw. + yes(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("foo-123"), + Action: resset.ActionRead, + }) + + yes(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("foo-123"), + Action: resset.ActionWrite, + }) + + no(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("foo-123"), + Action: resset.ActionControl, + }, resset.ErrUnauthorizedForAction) + + // bar-* has r. + yes(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("bar-123"), + Action: resset.ActionRead, + }) + + no(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("bar-123"), + Action: resset.ActionWrite, + }, resset.ErrUnauthorizedForAction) + + // foo* doesn't have access + no(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr("foo"), + Action: resset.ActionRead, + }, resset.ErrUnauthorizedForResource) + + // "" doesn't have access + no(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + AppName: sptr(""), + Action: resset.ActionRead, + }, resset.ErrUnauthorizedForResource) + + // unnamed app doesn't have access + no(cs, &Access{ + OrgID: uptr(1), + AppID: uptr(1), + Action: resset.ActionRead, + }, resset.ErrResourceUnspecified) +}