Skip to content

Commit

Permalink
JSON formatting of IAM: Workaround for optional fields
Browse files Browse the repository at this point in the history
AWS IAM is very strict and doesn't support `Resource: []` for example.
We implement a custom MarshalJSON method to work around that.
  • Loading branch information
justinsb committed Sep 9, 2020
1 parent d8895c5 commit 6fa8be2
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 6 deletions.
2 changes: 1 addition & 1 deletion pkg/model/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ func (b *IAMModelBuilder) buildAWSIAMRolePolicy(role iam.Subject) (fi.Resource,
Statement: []*iam.Statement{
{
Effect: "Allow",
Principal: &iam.Principal{
Principal: iam.Principal{
Federated: "arn:aws:iam::" + b.AWSAccountID + ":oidc-provider/" + oidcProvider,
},
Action: stringorslice.String("sts:AssumeRoleWithWebIdentity"),
Expand Down
103 changes: 99 additions & 4 deletions pkg/model/iam/iam_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,112 @@ type Condition map[string]interface{}
// http://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements.html#Statement
type Statement struct {
Effect StatementEffect
Principal *Principal `json:",omitempty"`
Action stringorslice.StringOrSlice `json:",omitempty"`
Resource stringorslice.StringOrSlice `json:",omitempty"`
Condition Condition `json:",omitempty"`
Principal Principal
Action stringorslice.StringOrSlice
Resource stringorslice.StringOrSlice
Condition Condition
}

type jsonWriter struct {
w io.Writer
err error
}

func (j *jsonWriter) Error() error {
return j.err
}

func (j *jsonWriter) WriteLiteral(b []byte) {
if j.err != nil {
return
}
_, err := j.w.Write(b)
if err != nil {
j.err = err
}
}

func (j *jsonWriter) StartObject() {
j.WriteLiteral([]byte("{"))
}

func (j *jsonWriter) EndObject() {
j.WriteLiteral([]byte("}"))
}

func (j *jsonWriter) Comma() {
j.WriteLiteral([]byte(","))
}

func (j *jsonWriter) Field(s string) {
if j.err != nil {
return
}
b, err := json.Marshal(s)
if err != nil {
j.err = err
return
}
j.WriteLiteral(b)
j.WriteLiteral([]byte(": "))
}

func (j *jsonWriter) Marshal(v interface{}) {
if j.err != nil {
return
}
b, err := json.Marshal(v)
if err != nil {
j.err = err
return
}
j.WriteLiteral(b)
}

// MarshalJSON formats the IAM statement for the AWS IAM restrictions.
// For example, `Resource: []` is not allowed, but golang would force us to use pointers.
func (s *Statement) MarshalJSON() ([]byte, error) {
var b bytes.Buffer

jw := &jsonWriter{w: &b}
jw.StartObject()
jw.Field("Effect")
jw.Marshal(s.Effect)

if !s.Principal.IsEmpty() {
jw.Comma()
jw.Field("Principal")
jw.Marshal(s.Principal)
}
if !s.Action.IsEmpty() {
jw.Comma()
jw.Field("Action")
jw.Marshal(s.Action)
}
if !s.Resource.IsEmpty() {
jw.Comma()
jw.Field("Resource")
jw.Marshal(s.Resource)
}
if len(s.Condition) != 0 {
jw.Comma()
jw.Field("Condition")
jw.Marshal(s.Condition)
}
jw.EndObject()

return b.Bytes(), jw.Error()
}

type Principal struct {
Federated string `json:",omitempty"`
Service string `json:",omitempty"`
}

func (p *Principal) IsEmpty() bool {
return *p == Principal{}
}

// Equal compares two IAM Statements and returns a bool
// TODO: Extend to support Condition Keys
func (l *Statement) Equal(r *Statement) bool {
Expand Down
20 changes: 20 additions & 0 deletions pkg/model/iam/iam_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ func TestRoundTrip(t *testing.T) {
},
JSON: "{\"Effect\":\"Deny\",\"Action\":[\"ec2:DescribeRegions\",\"ec2:DescribeInstances\"],\"Resource\":[\"a\",\"b\"]}",
},
{
IAM: &Statement{
Effect: StatementEffectDeny,
Principal: Principal{Federated: "federated"},
Condition: map[string]interface{}{
"foo": 1,
},
},
JSON: "{\"Effect\":\"Deny\",\"Principal\":{\"Federated\":\"federated\"},\"Condition\":{\"foo\":1}}",
},
{
IAM: &Statement{
Effect: StatementEffectDeny,
Principal: Principal{Service: "service"},
Condition: map[string]interface{}{
"bar": "baz",
},
},
JSON: "{\"Effect\":\"Deny\",\"Principal\":{\"Service\":\"service\"},\"Condition\":{\"bar\":\"baz\"}}",
},
}
for _, g := range grid {
actualJSON, err := json.Marshal(g.IAM)
Expand Down
4 changes: 4 additions & 0 deletions pkg/util/stringorslice/stringorslice.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ type StringOrSlice struct {
forceEncodeAsArray bool
}

func (s *StringOrSlice) IsEmpty() bool {
return len(s.values) == 0
}

// Slice will build a value that marshals to a JSON array
func Slice(v []string) StringOrSlice {
return StringOrSlice{values: v, forceEncodeAsArray: true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"Federated": "arn:aws:iam::123456789012:oidc-provider/api.minimal.example.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Resource": [],
"Condition": {
"StringEquals": {
"api.minimal.example.com:sub": "system:serviceaccount:kube-system:dns-controller"
Expand Down

0 comments on commit 6fa8be2

Please sign in to comment.