Skip to content
Closed
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 @@ -45,6 +45,7 @@ const (
CavFlyioStorageObjects
CavAllowedRoles
CavFlyioFlySrc
CavFlyioMachineFeatureID

// allocate internal blocks of size 255 here
block255Min CaveatType = 1 << 16
Expand Down
38 changes: 38 additions & 0 deletions flyio/caveats.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
CavStorageObjects = macaroon.CavFlyioStorageObjects
CavAllowedRoles = macaroon.CavAllowedRoles
CavFlySrc = macaroon.CavFlyioFlySrc
CavMachineFeatureID = macaroon.CavFlyioMachineFeatureID
)

type FromMachine struct {
Expand Down Expand Up @@ -478,3 +479,40 @@ func (c *FlySrc) Prohibits(a macaroon.Access) error {

return nil
}

// MachineFeatureID restricts some machine features to be accessed from a specific machine ID.
// It rejects a request only if the access is for the machine feature, and if the access machine ID does not match.
type MachineFeatureID struct {
MachineID string `json:"machine_id"`
Features []string `json:"features"`
}

func init() { macaroon.RegisterCaveatType(&MachineFeatureID{}) }
func (c *MachineFeatureID) CaveatType() macaroon.CaveatType { return CavMachineFeatureID }
func (c *MachineFeatureID) Name() string { return "MachineFeatureID" }

func (c *MachineFeatureID) Prohibits(a macaroon.Access) error {
f, isFlyioAccess := a.(MachineFeatureGetter)
switch {
case !isFlyioAccess || f.GetMachineFeature() == nil:
// If the access doesnt have a machine feature, it is allowed.
return nil
case !slices.Contains(c.Features, *f.GetMachineFeature()):
// If it has a machine feature that is not controlled by the caveat, it is allowed.
return nil
}

// If this feature is controlled by the caveat, then the access must have
// a machine ID which must match the caveat's machine ID.
mg, isFlyioAccess := a.(MachineGetter)
switch {
case !isFlyioAccess:
return fmt.Errorf("%w: access isnt MachineFeatureGetter", macaroon.ErrInvalidAccess)
case mg.GetMachine() == nil:
return fmt.Errorf("%w machine", resset.ErrResourceUnspecified)
case c.MachineID != *mg.GetMachine():
return fmt.Errorf("%w machine %s, only %s", resset.ErrUnauthorizedForResource, *mg.GetMachine(), c.MachineID)
default:
return nil
}
}
18 changes: 18 additions & 0 deletions flyio/caveats.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@ access request does not specify the corresponding resource.
},
```

### MachineFeatureID

The MachineFeatureID Caveat restricts which machine IDs can be accessed with specific machine features.
It has a machine ID and a list of machine features. If the access references any of the specified machine features,
then the machine ID of the access must match the caveat's machine ID. Otherwise access is allowed.

```
{
"type": "MachineFeatureID",
"body": {
"machine_id": "e7843624c496e8",
"features": [
"self-serve"
]
}
}
```

### IfPresent Caveat

The IfPresent Caveat is a little bit different than other Caveats. It has an "if-then" part
Expand Down
60 changes: 60 additions & 0 deletions flyio/caveats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func TestCaveatSerialization(t *testing.T) {
&IsMember{},
ptr(AllowedRoles(RoleAdmin)),
&Commands{Command{[]string{"123"}, true}},
&MachineFeatureID{"e7843624c496e8", []string{"self-serve"}},
)

b, err := json.Marshal(cs)
Expand Down Expand Up @@ -271,3 +272,62 @@ func TestCommands(t *testing.T) {
Action: resset.ActionWrite,
}, resset.ErrUnauthorizedForAction)
}

func TestFeaturesID(t *testing.T) {
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(&MachineFeatureID{
MachineID: "goodmach",
Features: []string{
"controlled-feature", "other-controlled-feature",
},
})

// No machine feature.
yes(cs, &Access{
OrgID: uptr(1),
AppID: uptr(1),
})

// Machine feature not controlled by the caveat.
yes(cs, &Access{
OrgID: uptr(1),
AppID: uptr(1),
MachineFeature: ptr("uncontrolled-feature"),
Machine: ptr("anymach"),
})

// Machine feature is controlled, and machine ID matches.
yes(cs, &Access{
OrgID: uptr(1),
AppID: uptr(1),
MachineFeature: ptr("controlled-feature"),
Machine: ptr("goodmach"),
})

// Machine feature is controlled, and machine ID matches.
yes(cs, &Access{
OrgID: uptr(1),
AppID: uptr(1),
MachineFeature: ptr("other-controlled-feature"),
Machine: ptr("goodmach"),
})

// Machine feature is controlled, and machine ID does not match.
no(cs, &Access{
OrgID: uptr(1),
AppID: uptr(1),
MachineFeature: ptr("controlled-feature"),
Machine: ptr("badmach"),
}, resset.ErrUnauthorizedForResource)
}