policy-engine/iam/policy_test.go
Denis Kirillov 8cc5173d73 [#46] iam: Support namespaces when forming native rules
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2024-01-29 11:50:24 +03:00

503 lines
12 KiB
Go

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{
Statement: []Statement{{
Effect: AllowEffect,
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: true,
},
{
name: "general invalid effect",
policy: Policy{
Statement: []Statement{{
Effect: "dummy",
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
Resource: []string{Wildcard},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "general invalid principal block",
policy: Policy{
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{
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{
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{
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{
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{
Statement: []Statement{{
Effect: AllowEffect,
Resource: []string{},
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing resource block",
policy: Policy{
Statement: []Statement{{
Effect: AllowEffect,
}},
},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "missing statement block",
policy: Policy{},
typ: GeneralPolicyType,
isValid: false,
},
{
name: "identity based valid",
policy: Policy{
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",
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{
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{
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{
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",
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",
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)
}