diff --git a/caveat.go b/caveat.go index b33c409..3c50218 100644 --- a/caveat.go +++ b/caveat.go @@ -45,6 +45,7 @@ const ( CavFlyioStorageObjects CavAllowedRoles CavFlyioFlySrc + CavFlyioMachineFeatureID // allocate internal blocks of size 255 here block255Min CaveatType = 1 << 16 diff --git a/flyio/caveats.go b/flyio/caveats.go index 6befa60..1ba3df8 100644 --- a/flyio/caveats.go +++ b/flyio/caveats.go @@ -27,6 +27,7 @@ const ( CavStorageObjects = macaroon.CavFlyioStorageObjects CavAllowedRoles = macaroon.CavAllowedRoles CavFlySrc = macaroon.CavFlyioFlySrc + CavMachineFeatureID = macaroon.CavFlyioMachineFeatureID ) type FromMachine struct { @@ -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 + } +} diff --git a/flyio/caveats.md b/flyio/caveats.md index f58de5d..412f13c 100644 --- a/flyio/caveats.md +++ b/flyio/caveats.md @@ -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 diff --git a/flyio/caveats_test.go b/flyio/caveats_test.go index 04f8f07..61bb13d 100644 --- a/flyio/caveats_test.go +++ b/flyio/caveats_test.go @@ -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) @@ -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) +}