Skip to content

Commit

Permalink
flyio: add concept of "roles". replace NoAdminFeatures with roles
Browse files Browse the repository at this point in the history
  • Loading branch information
btoews committed Jul 29, 2024
1 parent 7a092df commit 47a33ed
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 85 deletions.
3 changes: 2 additions & 1 deletion caveat.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ const (
CavAuthConfineGoogleHD
CavAuthConfineGitHubOrg
CavAuthMaxValidity
CavNoAdminFeatures
CavFlyioIsMember
AttestationAuthFlyioUserID
AttestationAuthGitHubUserID
AttestationAuthGoogleUserID
CavAction
CavFlyioCommands
CavFlyioAppFeatureSet
CavFlyioStorageObjects
CavAllowedRoles

// allocate internal blocks of size 255 here
block255Min CaveatType = 1 << 16
Expand Down
63 changes: 63 additions & 0 deletions flyio/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,69 @@ func (f *Access) Validate() error {
return nil
}

const (
FeatureWireGuard = "wg"
FeatureDomains = "domain"
FeatureSites = "site"
FeatureRemoteBuilders = "builder"
FeatureAddOns = "addon"
FeatureChecks = "checks"
FeatureLFSC = "litefs-cloud"
FeatureMembership = "membership"
FeatureBilling = "billing"
FeatureDeletion = "deletion"
FeatureDocumentSigning = "document_signing"
FeatureAuthentication = "authentication"
)

var (
// MemberFeatures describes the level of access that non-admins are allowed
// for various org features.
MemberFeatures = map[string]resset.Action{
FeatureWireGuard: resset.ActionAll,
FeatureDomains: resset.ActionAll,
FeatureSites: resset.ActionAll,
FeatureRemoteBuilders: resset.ActionAll,
FeatureAddOns: resset.ActionAll,
FeatureChecks: resset.ActionAll,
FeatureLFSC: resset.ActionAll,

FeatureMembership: resset.ActionRead,
FeatureBilling: resset.ActionRead,
FeatureAuthentication: resset.ActionRead,

FeatureDeletion: resset.ActionNone,
FeatureDocumentSigning: resset.ActionNone,
}
)

// PermittedRolesGetter is an interface for Accesses capable of indicating what
// roles are allowed for the operation.
type PermittedRolesGetter interface {
macaroon.Access

// GetPermittedRoles returns a slice of roles that are allowed to perform the
// operation.
GetPermittedRoles() []Role
}

var _ PermittedRolesGetter = (*Access)(nil)

// GetPermittedRoles implements macaroon.PermittedRolesGetter. We require RoleAdmin
// for unrecognized organization features or features for which the attempted
// action is not allowed by ordinary members.
func (a *Access) GetPermittedRoles() []Role {
if a.Feature == nil {
return []Role{RoleMember}
}

if memberAllowed, ok := MemberFeatures[*a.Feature]; ok && a.Action.IsSubsetOf(memberAllowed) {
return []Role{RoleMember}
}

return []Role{RoleAdmin}
}

// OrgIDGetter is an interface allowing other packages to implement Accesses
// that work with Caveats defined in this package.
type OrgIDGetter interface {
Expand Down
152 changes: 85 additions & 67 deletions flyio/caveats.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flyio

import (
"fmt"
"strings"

"golang.org/x/exp/slices"

Expand All @@ -20,10 +21,11 @@ const (
CavMachineFeatureSet = macaroon.CavFlyioMachineFeatureSet
CavFromMachineSource = macaroon.CavFlyioFromMachineSource
CavClusters = macaroon.CavFlyioClusters
CavNoAdminFeatures = macaroon.CavNoAdminFeatures
CavIsMember = macaroon.CavFlyioIsMember
CavCommands = macaroon.CavFlyioCommands
CavAppFeatureSet = macaroon.CavFlyioAppFeatureSet
CavStorageObjects = macaroon.CavFlyioStorageObjects
CavAllowedRoles = macaroon.CavAllowedRoles
)

type FromMachine struct {
Expand Down Expand Up @@ -239,83 +241,99 @@ func (c *Clusters) Prohibits(a macaroon.Access) error {
return c.Clusters.Prohibits(f.GetCluster(), f.GetAction())
}

// Role is used by the AllowedRoles and IsMember caveats.
type Role uint32

const (
FeatureWireGuard = "wg"
FeatureDomains = "domain"
FeatureSites = "site"
FeatureRemoteBuilders = "builder"
FeatureAddOns = "addon"
FeatureChecks = "checks"
FeatureLFSC = "litefs-cloud"
FeatureMembership = "membership"
FeatureBilling = "billing"
FeatureDeletion = "deletion"
FeatureDocumentSigning = "document_signing"
FeatureAuthentication = "authentication"
)
RoleMember Role = 1 << iota
RoleBillingManager
// add new roles here! don't forget to update roleNames

var (
MemberFeatures = map[string]resset.Action{
FeatureWireGuard: resset.ActionAll,
FeatureDomains: resset.ActionAll,
FeatureSites: resset.ActionAll,
FeatureRemoteBuilders: resset.ActionAll,
FeatureAddOns: resset.ActionAll,
FeatureChecks: resset.ActionAll,
FeatureLFSC: resset.ActionAll,

FeatureMembership: resset.ActionRead,
FeatureBilling: resset.ActionRead,
FeatureAuthentication: resset.ActionRead,

FeatureDeletion: resset.ActionNone,
FeatureDocumentSigning: resset.ActionNone,
}
RoleAdmin Role = 0xFFFFFFFF
)

// NoAdminFeatures is a shorthand for specifying that the token isn't allowed to
// access admin-only features. Same as:
//
// resset.IfPresent{
// Ifs: macaroon.NewCaveatSet(&FeatureSet{
// "memberFeatureOne": resset.ActionAll,
// "memberFeatureTwo": resset.ActionAll,
// "memberFeatureNNN": resset.ActionAll,
// }),
// Else: resset.ActionAll
// }
type NoAdminFeatures struct{}

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

func (c *NoAdminFeatures) Prohibits(a macaroon.Access) error {
f, isFlyioAccess := a.(FeatureGetter)
if !isFlyioAccess {
return fmt.Errorf("%w: access isnt FeatureGetter", macaroon.ErrInvalidAccess)
var roleNames = map[Role]string{
// put roles that are a combination of other roles at the top
RoleAdmin: "admin",

// put singular roles at the bottom
RoleMember: "member",
RoleBillingManager: "billing_manager",
}

// HasAllRoles returns whether other is a subset of r.
func (r Role) HasAllRoles(other Role) bool {
return r&other == other
}

func (r Role) String() string {
if r == 0 {
return "none"
}
if f.GetFeature() == nil {
return nil

if nr, ok := roleNames[r]; ok {
return nr
}
if *f.GetFeature() == "" {
return fmt.Errorf("%w admin org features", resset.ErrUnauthorizedForResource)

var (
names []string
combined Role
)

for namedRole, name := range roleNames {
if r.HasAllRoles(namedRole) {
names = append(names, name)
combined |= namedRole

if combined == r {
return strings.Join(names, "+")
}
}
}

memberPermission, ok := MemberFeatures[*f.GetFeature()]
if !ok {
return fmt.Errorf("%w %s", resset.ErrUnauthorizedForResource, *f.GetFeature())
return fmt.Sprintf("invalid(%d)", r)
}

// AllowedRoles is a bitmask of roles that may be assumed. Only usable with
// Accesses implementing PermittedRolesGetter. Checks that a role returned by
// [GetPermittedRoles] matches the mask.
type AllowedRoles Role

func init() { macaroon.RegisterCaveatType(new(AllowedRoles)) }
func (c *AllowedRoles) CaveatType() macaroon.CaveatType { return CavAllowedRoles }
func (c *AllowedRoles) Name() string { return "AllowedRoles" }

func (c *AllowedRoles) Prohibits(a macaroon.Access) error {
f, isFlyioAccess := a.(PermittedRolesGetter)
if !isFlyioAccess {
return fmt.Errorf("%w: access isn't PermittedRolesGetter", macaroon.ErrInvalidAccess)
}
if !f.GetAction().IsSubsetOf(memberPermission) {
return fmt.Errorf(
"%w %s access to %s",
resset.ErrUnauthorizedForAction,
f.GetAction().Remove(memberPermission),
*f.GetFeature(),
)

permittedRoles := f.GetPermittedRoles()
for _, permitted := range permittedRoles {
if Role(*c).HasAllRoles(permitted) {
return nil
}
}

return nil
return fmt.Errorf("%w: allowed roles (%v) not permitted (%v)", ErrUnauthorizedForRole, *c, permittedRoles)
}

// IsMember is an alias for RoleMask(RoleMember). It used to be called
// NoAdminFeatures.
type IsMember struct{}

func init() {
macaroon.RegisterCaveatType(&IsMember{})
macaroon.RegisterCaveatJSONAlias(CavIsMember, "NoAdminFeatures")
}

func (c *IsMember) CaveatType() macaroon.CaveatType { return CavIsMember }
func (c *IsMember) Name() string { return "IsMember" }

func (c *IsMember) Prohibits(a macaroon.Access) error {
ar := AllowedRoles(RoleMember)
return ar.Prohibits(a)
}

// Commands is a list of commands allowed by this token.
Expand Down
Loading

0 comments on commit 47a33ed

Please sign in to comment.