package iam import ( "encoding/json" "fmt" "testing" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource/testutil" "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/stretchr/testify/require" ) func TestUnmarshalIAMPolicy(t *testing.T) { t.Run("simple fields", func(t *testing.T) { policy := `{ "Version": "2012-10-17", "Id": "PutObjPolicy", "Statement": { "Sid": "DenyObjectsThatAreNotSSEKMS", "Principal": "*", "Effect": "Deny", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*", "Condition": { "Null": { "s3:x-amz-server-side-encryption-aws-kms-key-id": "true" } } } }` expected := Policy{ Version: "2012-10-17", ID: "PutObjPolicy", Statement: []Statement{{ SID: "DenyObjectsThatAreNotSSEKMS", Principal: map[PrincipalType][]string{ "*": nil, }, Effect: DenyEffect, Action: []string{"s3:PutObject"}, Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, Conditions: map[string]Condition{ "Null": { "s3:x-amz-server-side-encryption-aws-kms-key-id": {"true"}, }, }, }}, } var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) require.Equal(t, expected, p) }) t.Run("complex fields", func(t *testing.T) { policy := `{ "Version": "2012-10-17", "Statement": [{ "Principal":{ "AWS":[ "arn:aws:iam::111122223333:user/JohnDoe" ] }, "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": [ "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*" ], "Condition": { "StringEquals": { "s3:RequestObjectTag/Department": ["Finance"] } } }] }` expected := Policy{ Version: "2012-10-17", Statement: []Statement{{ Principal: map[PrincipalType][]string{ AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"}, }, Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, Conditions: map[string]Condition{ "StringEquals": { "s3:RequestObjectTag/Department": {"Finance"}, }, }, }}, } var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) require.Equal(t, expected, p) raw, err := json.Marshal(expected) require.NoError(t, err) require.JSONEq(t, policy, string(raw)) }) t.Run("check principal AWS", func(t *testing.T) { policy := `{ "Statement": [{ "Principal":{ "AWS":"arn:aws:iam::111122223333:user/JohnDoe" } }] }` expected := Policy{ Statement: []Statement{{ Principal: map[PrincipalType][]string{ AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"}, }, }}, } var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) require.Equal(t, expected, p) }) t.Run("native example", func(t *testing.T) { policy := ` { "Version": "xyz", "Statement": [ { "Effect": "Allow", "Action": [ "native:*", "s3:PutObject", "s3:GetObject" ], "Resource": ["*"], "Principal": {"FrostFS": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"]}, "Condition": {"StringEquals": {"native::object::attribute": "iamuser-admin"}} } ] }` var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) }) t.Run("condition array", func(t *testing.T) { policy := ` { "Statement": [{ "Condition": {"StringLike": {"ec2:InstanceType": ["t1.*", "t2.*", "m3.*"]}} }] }` expected := Policy{ Statement: []Statement{{ Conditions: map[string]Condition{ "StringLike": {"ec2:InstanceType": {"t1.*", "t2.*", "m3.*"}}, }, }}, } var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) require.Equal(t, expected, p) }) t.Run("'Not*' fields", func(t *testing.T) { policy := ` { "Id": "PutObjPolicy", "Statement": [{ "NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]}, "Effect": "Deny", "NotAction": "s3:PutObject", "NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*" }] }` expected := Policy{ ID: "PutObjPolicy", Statement: []Statement{{ NotPrincipal: map[PrincipalType][]string{ AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}, }, Effect: DenyEffect, NotAction: []string{"s3:PutObject"}, NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, }}, } var p Policy err := json.Unmarshal([]byte(policy), &p) require.NoError(t, err) require.Equal(t, expected, p) }) } func TestValidatePolicies(t *testing.T) { for _, tc := range []struct { name string policy Policy typ PolicyType isValid bool }{ { name: "valid permission boundaries", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Resource: []string{Wildcard}, }}, }, typ: GeneralPolicyType, isValid: true, }, { name: "general invalid effect", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: "dummy", Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Resource: []string{Wildcard}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "general invalid principal block", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Resource: []string{Wildcard}, Principal: map[PrincipalType][]string{Wildcard: nil}, NotPrincipal: map[PrincipalType][]string{Wildcard: nil}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "general invalid not principal", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Resource: []string{Wildcard}, NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "general invalid principal type", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, Resource: []string{Wildcard}, NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "general invalid action block", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*", "cloudwatch:*", "ec2:*"}, NotAction: []string{"iam:*"}, Resource: []string{Wildcard}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "general invalid resource block", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Resource: []string{Wildcard}, NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "invalid resource block", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Resource: []string{}, NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "missing resource block", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "missing statement block", policy: Policy{}, typ: GeneralPolicyType, isValid: false, }, { name: "duplicate sid", policy: Policy{ Version: policyVersion, Statement: []Statement{ { SID: "sid", Effect: AllowEffect, Action: []string{"s3:*"}, Resource: []string{Wildcard}, }, { SID: "sid", Effect: AllowEffect, Action: []string{"cloudwatch:*"}, Resource: []string{Wildcard}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "missing version", policy: Policy{ Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:*"}, Resource: []string{Wildcard}, }}, }, typ: GeneralPolicyType, isValid: false, }, { name: "identity based valid", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, }}, }, typ: IdentityBasedPolicyType, isValid: true, }, { name: "identity based invalid because of id presence", policy: Policy{ ID: "some-id", Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, }}, }, typ: IdentityBasedPolicyType, isValid: false, }, { name: "identity based invalid because of principal presence", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: IdentityBasedPolicyType, isValid: false, }, { name: "identity based invalid because of not principal presence", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: IdentityBasedPolicyType, isValid: false, }, { name: "resource based valid principal", policy: Policy{ Version: policyVersion, Statement: []Statement{{ Effect: DenyEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: ResourceBasedPolicyType, isValid: true, }, { name: "resource based valid not principal", policy: Policy{ ID: "some-id", Version: policyVersion, Statement: []Statement{{ Effect: DenyEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}}, }}, }, typ: ResourceBasedPolicyType, isValid: true, }, { name: "resource based invalid missing principal", policy: Policy{ ID: "some-id", Version: policyVersion, Statement: []Statement{{ Effect: AllowEffect, Action: []string{"s3:PutObject"}, Resource: []string{Wildcard}, }}, }, typ: ResourceBasedPolicyType, isValid: false, }, } { t.Run(tc.name, func(t *testing.T) { err := tc.policy.Validate(tc.typ) if tc.isValid { require.NoError(t, err) } else { require.Error(t, err) } }) } } func TestProcessDenyFirst(t *testing.T) { identityBasedPolicyStr := ` { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": [ "arn:aws:iam::root:user/user-name" ] }, "Action": ["s3:PutObject" ], "Resource": "arn:aws:s3:::*" } ] } ` resourceBasedPolicyStr := ` { "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "*", "Action": "s3:*", "Resource": [ "arn:aws:s3:::test-bucket/*" ] } ] } ` var identityPolicy Policy err := json.Unmarshal([]byte(identityBasedPolicyStr), &identityPolicy) require.NoError(t, err) var resourcePolicy Policy err = json.Unmarshal([]byte(resourceBasedPolicyStr), &resourcePolicy) require.NoError(t, err) mockResolver := newMockUserResolver([]string{"root/user-name"}, []string{"test-bucket"}, "") identityNativePolicy, err := ConvertToNativeChain(identityPolicy, mockResolver) require.NoError(t, err) identityNativePolicy.MatchType = chain.MatchTypeFirstMatch resourceNativePolicy, err := ConvertToNativeChain(resourcePolicy, mockResolver) require.NoError(t, err) s := inmemory.NewInMemory() target := engine.NamespaceTarget("ns") _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, identityNativePolicy) require.NoError(t, err) _, _, err = s.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, target, resourceNativePolicy) require.NoError(t, err) resource := testutil.NewResource(fmt.Sprintf(native.ResourceFormatRootContainerObjects, mockResolver.containers["test-bucket"]), nil) request := testutil.NewRequest("PutObject", resource, map[string]string{native.PropertyKeyActorPublicKey: mockResolver.users["root/user-name"]}) status, found, err := s.IsAllowed(chain.Ingress, engine.NewRequestTarget("ns", ""), request) require.NoError(t, err) require.True(t, found) require.Equal(t, chain.AccessDenied, status) }