I'm working through the IAM policy examples for the AWS Go SDK and trying to do the opposite of the Create Policy
example -- basically, get all of the IAM policy in the account, get the default policy versions, then unmarshal that json document into a struct so it is easily parsed.
I got this far but I'm stuck with how go handles a conditional struct type. In the AWS policy doc version response, the json data for StatementEntry
can be string
or []string
depending on the doc.
What would be a best practice? Add another struct and use retry logic in the error handling?
package main
import (
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
"log"
"net/url"
)
type PolicyDocument struct {
Version string
Statement []StatementEntry
}
type StatementEntry struct {
Effect string
Action []string
Resource []string
}
func main() {
sess, _ := session.NewSession(&aws.Config{
Region: aws.String("us-west-2")},
)
svc := iam.New(sess)
fmt.Printf("%s - %s
", arn, *result.Policy.Description)
results, _ := svc.ListPolicies(&iam.ListPoliciesInput{})
for _, policy := range results.Policies {
arn := policy.Arn
version := policy.DefaultVersionId
pv, _ := svc.GetPolicyVersion(&iam.GetPolicyVersionInput{
PolicyArn: arn,
VersionId: version,
})
decodedValue, err := url.QueryUnescape(aws.StringValue(pv.PolicyVersion.Document))
if err != nil {
log.Fatal(err)
return
}
//fmt.Println(decodedValue)
data := []byte(decodedValue)
var doc PolicyDocument
err1 := json.Unmarshal(data, &doc)
if err1 != nil {
log.Fatal(err1)
}
fmt.Printf("
----
%v
---
", doc)
}
}
Example PolicyDocuments
are this:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:DescribeInstancePatchStates",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DeregisterTargets",
"ssm:GetParameter"
],
"Resource": "*"
}
]
}
And this (for Resource []string
in the StatementEntry
):
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::SageMaker"
]
},
{
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::SageMaker/*"
]
}
]
}
You can achieve this by using a custom type for the slice with an Unmarshal method that first unmarshals to an empty interface and then determines if it is a slice or a single string:
package main
import (
"fmt"
"encoding/json"
"errors"
)
type container struct {
Field customSlice
}
type customSlice []string
func (c *customSlice) UnmarshalJSON(data []byte) error {
var tmp interface{}
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
slice, ok := tmp.([]interface{})
if ok {
for _, item := range slice {
*c = append(*c, item.(string))
}
return nil
}
theString, ok := tmp.(string)
if ok {
*c = append(*c, theString)
return nil
}
return errors.New("Field neither slice or string")
}
func main() {
jsonInputSlice := `{"Field":["a"]}`
jsonInputString := `{"Field":"a"}`
var containerSlice container
var containerString container
err := json.Unmarshal([]byte(jsonInputSlice), &containerSlice)
if err != nil {
panic(err)
}
fmt.Println(containerSlice)
err = json.Unmarshal([]byte(jsonInputString), &containerString)
if err != nil {
panic(err)
}
fmt.Println(containerString)
}
You can do this in multiple ways. One of the way is you define Resource
as interface{}
and do the parsing later. Playground - https://play.golang.org/p/PiLaa0DySEj
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type PolicyDocument struct {
Version string
Statement []StatementEntry
}
type StatementEntry struct {
Effect string
Action []string
Resource interface{}
}
func main() {
data := `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:DeleteParameter",
"ssm:DescribeInstancePatchStates",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DeregisterTargets",
"ssm:GetParameter"
],
"Resource": "*"
}
]
}`
convertPolicy(data)
data = `{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::SageMaker"
]
},
{
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::SageMaker/*"
]
}
]
}`
convertPolicy(data)
}
func convertPolicy(data string) {
var doc PolicyDocument
err1 := json.Unmarshal([]byte(data), &doc)
if err1 != nil {
fmt.Println(err1)
}
//find out the type of resource string or []string
for _, statement := range doc.Statement {
fmt.Println("statement.Resource was of type - ", reflect.TypeOf(statement.Resource))
if reflect.TypeOf(statement.Resource).Name() != "string" {
// we will convert the []interface to []string
x := statement.Resource.([]interface{})
y := make([]string, len(x))
for i := 0; i < len(x); i++ {
y[i] = x[i].(string)
}
statement.Resource = y
fmt.Println("statement.Resource is converted to type - ", reflect.TypeOf(statement.Resource))
}
}
fmt.Printf("
----
%v
---
", doc)
}