Skip to content
Merged
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
105 changes: 104 additions & 1 deletion lib/cloud/aws/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ type Statement struct {
// Resources is a list of resources.
Resources SliceOrString `json:"Resource,omitempty"`
// Principals is a list of principals.
Principals map[string]SliceOrString `json:"Principal,omitempty"`
// It can be a single string (eg "*") or a map.
Principals StringOrMap `json:"Principal,omitempty"`
// Conditions is a list of conditions that must be satisfied for the action to be allowed.
// Example:
// Condition:
Expand All @@ -104,6 +105,52 @@ func (s *Statement) ensureResources(resources []string) {
}
}

// EqualStatement returns whether the receive statement is the same.
func (s *Statement) EqualStatement(other *Statement) bool {
if s.Effect != other.Effect {
return false
}

if !slices.Equal(s.Actions, other.Actions) {
return false
}

if len(s.Principals) != len(other.Principals) {
return false
}

for principalKind, principalList := range s.Principals {
expectedPrincipalList := other.Principals[principalKind]
if !slices.Equal(principalList, expectedPrincipalList) {
return false
}
}

if !slices.Equal(s.Resources, other.Resources) {
return false
}

if len(s.Conditions) != len(other.Conditions) {
return false
}
for conditionKind, conditionOp := range s.Conditions {
expectedConditionOp := other.Conditions[conditionKind]

if len(conditionOp) != len(expectedConditionOp) {
return false
}

for conditionOpKind, conditionOpList := range conditionOp {
expectedConditionOpList := expectedConditionOp[conditionOpKind]
if !slices.Equal(conditionOpList, expectedConditionOpList) {
return false
}
}
}

return true
}

// ParsePolicyDocument returns parsed AWS IAM policy document.
func ParsePolicyDocument(document string) (*PolicyDocument, error) {
// Policy document returned from AWS API can be URL-encoded:
Expand Down Expand Up @@ -281,6 +328,62 @@ func (s SliceOrString) MarshalJSON() ([]byte, error) {
}
}

// StringOrMap defines a type that can be either a single string or a map.
//
// For almost every use case a map is used. Example:
// "Principal": { "Service": ["ecs.amazonaws.com", "elasticloadbalancing.amazonaws.com"]}
//
// For special use cases, like public/anonynous access, a "*" can be used:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-anonymous
type StringOrMap map[string]SliceOrString

// UnmarshalJSON implements json.Unmarshaller.
// If it contains a string and not a map, it will create a map with a single entry:
// { "str": [] }
// The only known example is for allowing anything, by using the "*"
// (See examples here // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-anonymous)
func (s *StringOrMap) UnmarshalJSON(bytes []byte) error {
// Check if input is a map.
var mapInput map[string]SliceOrString
mapErr := json.Unmarshal(bytes, &mapInput)
if mapErr == nil {
*s = mapInput
return nil
}

// Check if input is a single string.
var str string
strErr := json.Unmarshal(bytes, &str)
if strErr == nil {
*s = StringOrMap{
str: SliceOrString{},
}
return nil
}

// Failed both format.
return trace.NewAggregate(mapErr, strErr)
}

// MarshalJSON implements json.Marshaler.
// It returns "*" if the map has a single key and that key has 0 items.
// The only known example is for allowing anything, by using the "*"
// (See examples here // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#principal-anonymous)
// The regular Marshal method is used otherwise.
func (s StringOrMap) MarshalJSON() ([]byte, error) {
switch len(s) {
case 0:
return json.Marshal(map[string]SliceOrString{})
case 1:
if values, isWildcard := s[wildcard]; isWildcard && len(values) == 0 {
return json.Marshal(wildcard)
}
fallthrough
default:
return json.Marshal(map[string]SliceOrString(s))
}
}

// Policies set of IAM Policy helper functions defined as an interface to make
// easier for other packages to mock and test with it.
type Policies interface {
Expand Down
20 changes: 19 additions & 1 deletion lib/cloud/aws/policy_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import (
"github.com/gravitational/trace"
)

var allResources = []string{"*"}
var wildcard = "*"
var allResources = []string{wildcard}

// StatementForIAMEditRolePolicy returns a IAM Policy Statement which allows editting Role Policy
// of the resources.
Expand Down Expand Up @@ -185,6 +186,23 @@ func StatementForListRDSDatabases() *Statement {
}
}

// StatementForS3BucketPublicRead returns the statement that
// allows public/anonynous access to s3 bucket/prefix objects.
func StatementForS3BucketPublicRead(s3bucketName, objectPrefix string) *Statement {
return &Statement{
Effect: EffectAllow,
Principals: StringOrMap{
wildcard: SliceOrString{},
},
Actions: []string{
"s3:GetObject",
},
Resources: []string{
fmt.Sprintf("arn:aws:s3:::%s/%s/*", s3bucketName, objectPrefix),
},
}
}

// ExternalAuditStoragePolicyConfig holds options for the External Audit Storage
// IAM policy.
type ExternalAuditStoragePolicyConfig struct {
Expand Down
Loading