Skip to content

Commit

Permalink
move auth 3p caveats to their own package. add google/github caveats
Browse files Browse the repository at this point in the history
  • Loading branch information
btoews committed Oct 31, 2023
1 parent a5c10f5 commit 07a1429
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 48 deletions.
192 changes: 192 additions & 0 deletions auth/caveats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package auth

import (
"fmt"
"math"
"time"

"golang.org/x/exp/slices"

"github.com/superfly/macaroon"
)

const (
CavConfineUser = macaroon.CavAuthConfineUser
CavConfineOrganization = macaroon.CavAuthConfineOrganization
CavConfineGoogleHD = macaroon.CavAuthConfineGoogleHD
CavConfineGitHubOrg = macaroon.CavAuthConfineGitHubOrg
CavMaxValidity = macaroon.CavAuthMaxValidity
)

// ConfineOrganization is a requirement placed on 3P caveats, requiring that the
// authenticated used be associated with OrgID. It has no meaning in a 1P setting.
type ConfineOrganization struct {
ID uint64 `json:"id"`
}

func RequireOrganization(id uint64) *ConfineOrganization {
return &ConfineOrganization{id}
}

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(&ConfineOrganization{}) }
func (c *ConfineOrganization) CaveatType() macaroon.CaveatType { return CavConfineOrganization }
func (c *ConfineOrganization) Name() string { return "ConfineOrganization" }

// Implements macaroon.Caveat
func (c *ConfineOrganization) Prohibits(a macaroon.Access) error {
switch dr, isDR := a.(*DischargeRequest); {
case !isDR:
return macaroon.ErrInvalidAccess
case dr.Organization == nil:
return c
case dr.Organization.ID != c.ID:
return fmt.Errorf("%w (got %d)", c, dr.Organization.ID)
default:
return nil
}
}

// implements error
func (c *ConfineOrganization) Error() string {
return fmt.Sprintf("must authenticate with Fly.io account with access to organization %d", c.ID)
}

// ConfineUser is a caveat limiting this token to a specific user ID.
type ConfineUser struct {
ID uint64 `json:"id"`
}

func RequireUser(id uint64) *ConfineUser {
return &ConfineUser{id}
}

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(&ConfineUser{}) }
func (c *ConfineUser) CaveatType() macaroon.CaveatType { return CavConfineUser }
func (c *ConfineUser) Name() string { return "ConfineUser" }

// Implements macaroon.Caveat
func (c *ConfineUser) Prohibits(a macaroon.Access) error {
switch dr, isDR := a.(*DischargeRequest); {
case !isDR:
return macaroon.ErrInvalidAccess
case dr.User == nil:
return c
case dr.User.ID != c.ID:
return fmt.Errorf("%w (got %d)", c, dr.User.ID)
default:
return nil
}
}

// implements error
func (c *ConfineUser) Error() string {
return fmt.Sprintf("must authenticate with Fly.io account %d", c.ID)
}

// Implements macaroon.Caveat and error. Requires that the user is
// authenticated to Google with an account in the specified HD.
type ConfineGoogleHD string

func RequireGoogleHD(hd string) *ConfineGoogleHD {
return (*ConfineGoogleHD)(&hd)
}

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(new(ConfineGoogleHD)) }
func (c *ConfineGoogleHD) CaveatType() macaroon.CaveatType { return CavConfineGoogleHD }
func (c *ConfineGoogleHD) Name() string { return "ConfineGoogleHD" }

// Implements macaroon.Caveat
func (c *ConfineGoogleHD) Prohibits(a macaroon.Access) error {
switch dr, isDR := a.(*DischargeRequest); {
case !isDR:
return macaroon.ErrInvalidAccess
case dr.Google == nil:
return c
case dr.Google.HD != string(*c):
return fmt.Errorf("%w (got %s)", c, dr.Google.HD)
default:
return nil
}
}

// implements error
func (c *ConfineGoogleHD) Error() string {
return fmt.Sprintf("must authenticate with %s Google account", string(*c))
}

// Implements macaroon.Caveat and error. Requires that the user is
// authenticated to GitHub with an account that has access the specified org.
type ConfineGitHubOrg uint64

func RequireGitHubOrg(id uint64) *ConfineGitHubOrg {
return (*ConfineGitHubOrg)(&id)
}

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(new(ConfineGitHubOrg)) }
func (c *ConfineGitHubOrg) CaveatType() macaroon.CaveatType { return CavConfineGitHubOrg }
func (c *ConfineGitHubOrg) Name() string { return "ConfineGitHubOrg" }

// Implements macaroon.Caveat
func (c *ConfineGitHubOrg) Prohibits(a macaroon.Access) error {
switch dr, isDR := a.(*DischargeRequest); {
case !isDR:
return macaroon.ErrInvalidAccess
case dr.GitHub == nil:
return c
case !slices.Contains(dr.GitHub.OrgIDs, uint64(*c)):
return fmt.Errorf("%w (got %v)", c, dr.GitHub.OrgIDs)
default:
return nil
}
}

// implements error
func (c *ConfineGitHubOrg) Error() string {
return fmt.Sprintf("must authenticate with GitHub account with access to organization %d", uint64(*c))
}

// Implements macaroon.Caveat. Limits the validity window length (seconds) of
// discharges issued by 3ps.
type MaxValidity uint64

// Implements macaroon.Caveat
func init() { macaroon.RegisterCaveatType(new(MaxValidity)) }
func (c *MaxValidity) CaveatType() macaroon.CaveatType { return CavMaxValidity }
func (c *MaxValidity) Name() string { return "MaxValidity" }

// Implements macaroon.Caveat
func (c *MaxValidity) Prohibits(a macaroon.Access) error {
switch aa, isAuthAccess := a.(*DischargeRequest); {
case !isAuthAccess:
return macaroon.ErrInvalidAccess
case aa.Expiry.Sub(aa.Now()) > c.duration():
return fmt.Errorf(
"%w: %v exceeds max validity window (%v)",
macaroon.ErrUnauthorized,
aa.Expiry.Sub(aa.Now()),
c.duration(),
)
default:
return nil
}
}

func (c *MaxValidity) duration() time.Duration {
return time.Duration(*c) * time.Second
}

func GetMaxValidity(cs *macaroon.CaveatSet) (time.Duration, bool) {
max := time.Duration(math.MaxInt64)

for _, cav := range macaroon.GetCaveats[*MaxValidity](cs) {
if cavDur := cav.duration(); max > cavDur {
max = cavDur
}
}

return max, max != time.Duration(math.MaxInt64)
}
32 changes: 32 additions & 0 deletions auth/caveats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package auth

import (
"encoding/json"
"testing"

"github.com/alecthomas/assert/v2"
"github.com/superfly/macaroon"
)

func TestCaveatSerialization(t *testing.T) {
cs := macaroon.NewCaveatSet(
RequireUser(123),
RequireOrganization(123),
RequireGoogleHD("123"),
RequireGitHubOrg(123),
)

b, err := json.Marshal(cs)
assert.NoError(t, err)

cs2 := macaroon.NewCaveatSet()
err = json.Unmarshal(b, cs2)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)

b, err = cs.MarshalMsgpack()
assert.NoError(t, err)
cs2, err = macaroon.DecodeCaveats(b)
assert.NoError(t, err)
assert.Equal(t, cs, cs2)
}
31 changes: 31 additions & 0 deletions auth/discharge_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package auth

import "time"

// implements macaroon.Access
type DischargeRequest struct {
User *UserAuth
Organization *OrganizationAuth
Google *GoogleAuth
GitHub *GitHubAuth
Expiry time.Time
}

func (a *DischargeRequest) Now() time.Time { return time.Now() }
func (a *DischargeRequest) Validate() error { return nil }

type UserAuth struct {
ID uint64
}

type OrganizationAuth struct {
ID uint64
}

type GoogleAuth struct {
HD string
}

type GitHubAuth struct {
OrgIDs []uint64
}
10 changes: 5 additions & 5 deletions caveat.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ const (
CavFlyioFeatureSet
CavFlyioMutations
CavFlyioMachines
CavFlyioConfineUser
CavFlyioConfineOrganization
CavAuthConfineUser
CavAuthConfineOrganization
CavFlyioIsUser
Cav3P
CavBindToParentToken
Expand All @@ -32,9 +32,9 @@ const (
CavFlyioClusters
_ // fly.io reserved
_ // fly.io reserved
CavFlyioRequireGoogleHD
CavFlyioRequireGitHubOrg
CavFlyioDischargeExpiryLTE
CavAuthConfineGoogleHD
CavAuthConfineGitHubOrg
CavAuthMaxValidity

// Globally-recognized user-registerable caveat types may be requested via
// pull requests to this repository. Add a meaningful name of the caveat
Expand Down
51 changes: 10 additions & 41 deletions flyio/caveats.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@ import (
)

const (
CavOrganization = macaroon.CavFlyioOrganization
CavVolumes = macaroon.CavFlyioVolumes
CavApps = macaroon.CavFlyioApps
CavFeatureSet = macaroon.CavFlyioFeatureSet
CavMutations = macaroon.CavFlyioMutations
CavMachines = macaroon.CavFlyioMachines
CavConfineUser = macaroon.CavFlyioConfineUser
CavConfineOrganization = macaroon.CavFlyioConfineOrganization
CavIsUser = macaroon.CavFlyioIsUser
CavMachineFeatureSet = macaroon.CavFlyioMachineFeatureSet
CavFromMachineSource = macaroon.CavFlyioFromMachineSource
CavClusters = macaroon.CavFlyioClusters
CavOrganization = macaroon.CavFlyioOrganization
CavVolumes = macaroon.CavFlyioVolumes
CavApps = macaroon.CavFlyioApps
CavFeatureSet = macaroon.CavFlyioFeatureSet
CavMutations = macaroon.CavFlyioMutations
CavMachines = macaroon.CavFlyioMachines
CavIsUser = macaroon.CavFlyioIsUser
CavMachineFeatureSet = macaroon.CavFlyioMachineFeatureSet
CavFromMachineSource = macaroon.CavFlyioFromMachineSource
CavClusters = macaroon.CavFlyioClusters
)

type FromMachine struct {
Expand Down Expand Up @@ -76,35 +74,6 @@ func (c *Organization) Prohibits(a macaroon.Access) error {
}
}

// ConfineOrganization is a requirement placed on 3P caveats, requiring that the
// authenticated used be associated with OrgID. It has no meaning in a 1P setting.
type ConfineOrganization struct {
ID uint64 `json:"id"`
}

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

func (c *ConfineOrganization) Prohibits(macaroon.Access) error {
// ConfineOrganization is only used in 3P caveats and has no role in access validation.
return fmt.Errorf("%w (confine-organization)", macaroon.ErrBadCaveat)
}

// ConfineUser is a caveat limiting this token to a specific user ID.
type ConfineUser struct {
ID uint64 `json:"id"`
}

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

func (c *ConfineUser) Prohibits(macaroon.Access) error {
// ConfineUser is only used in 3P caveats and has no role in access validation.
return fmt.Errorf("%w (confine-user)", macaroon.ErrBadCaveat)
}

// Apps is a set of App caveats, with their RWX access levels. A token with this set can be used
// 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.
Expand Down
2 changes: 0 additions & 2 deletions flyio/caveats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ func TestCaveatSerialization(t *testing.T) {
&Volumes{Volumes: resset.New(resset.ActionRead, "123")},
&Machines{Machines: resset.New(resset.ActionRead, "123")},
&Mutations{Mutations: []string{"123"}},
&ConfineUser{ID: 123},
&ConfineOrganization{ID: 123},
&IsUser{ID: 123},
&MachineFeatureSet{Features: resset.New(resset.ActionRead, "123")},
&FromMachine{ID: "asdf"},
Expand Down

0 comments on commit 07a1429

Please sign in to comment.