Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions caveat.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const (
_ // deprecated
CavFlyioVolumes
CavFlyioApps
CavFlyioAppPrefixes
CavValidityWindow
CavFlyioFeatureSet
CavFlyioMutations
Expand Down
13 changes: 13 additions & 0 deletions flyio/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions flyio/caveats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
Expand Down
11 changes: 11 additions & 0 deletions flyio/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
83 changes: 83 additions & 0 deletions flyio/caveats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")},
Expand Down Expand Up @@ -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)
}
Loading