/*
 * MinIO Cloud Storage, (C) 2018 MinIO, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package policy

import (
	"encoding/json"
	"net"
	"reflect"
	"testing"

	"github.com/minio/minio/pkg/bucket/policy/condition"
)

func TestStatementIsAllowed(t *testing.T) {
	case1Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(GetBucketLocationAction, PutObjectAction),
		NewResourceSet(NewResource("*", "")),
		condition.NewFunctions(),
	)

	case2Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(GetObjectAction, PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(),
	)

	_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	func1, err := condition.NewIPAddressFunc(
		condition.AWSSourceIP,
		IPNet1,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}

	case3Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(GetObjectAction, PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func1),
	)

	case4Statement := NewStatement(
		Deny,
		NewPrincipal("*"),
		NewActionSet(GetObjectAction, PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func1),
	)

	anonGetBucketLocationArgs := Args{
		AccountName:     "Q3AM3UQ867SPQQA43P2F",
		Action:          GetBucketLocationAction,
		BucketName:      "mybucket",
		ConditionValues: map[string][]string{},
	}

	anonPutObjectActionArgs := Args{
		AccountName: "Q3AM3UQ867SPQQA43P2F",
		Action:      PutObjectAction,
		BucketName:  "mybucket",
		ConditionValues: map[string][]string{
			"x-amz-copy-source": {"mybucket/myobject"},
			"SourceIp":          {"192.168.1.10"},
		},
		ObjectName: "myobject",
	}

	anonGetObjectActionArgs := Args{
		AccountName:     "Q3AM3UQ867SPQQA43P2F",
		Action:          GetObjectAction,
		BucketName:      "mybucket",
		ConditionValues: map[string][]string{},
		ObjectName:      "myobject",
	}

	getBucketLocationArgs := Args{
		AccountName:     "Q3AM3UQ867SPQQA43P2F",
		Action:          GetBucketLocationAction,
		BucketName:      "mybucket",
		ConditionValues: map[string][]string{},
		IsOwner:         true,
	}

	putObjectActionArgs := Args{
		AccountName: "Q3AM3UQ867SPQQA43P2F",
		Action:      PutObjectAction,
		BucketName:  "mybucket",
		ConditionValues: map[string][]string{
			"x-amz-copy-source": {"mybucket/myobject"},
			"SourceIp":          {"192.168.1.10"},
		},
		IsOwner:    true,
		ObjectName: "myobject",
	}

	getObjectActionArgs := Args{
		AccountName:     "Q3AM3UQ867SPQQA43P2F",
		Action:          GetObjectAction,
		BucketName:      "mybucket",
		ConditionValues: map[string][]string{},
		IsOwner:         true,
		ObjectName:      "myobject",
	}

	testCases := []struct {
		statement      Statement
		args           Args
		expectedResult bool
	}{
		{case1Statement, anonGetBucketLocationArgs, true},
		{case1Statement, anonPutObjectActionArgs, true},
		{case1Statement, anonGetObjectActionArgs, false},
		{case1Statement, getBucketLocationArgs, true},
		{case1Statement, putObjectActionArgs, true},
		{case1Statement, getObjectActionArgs, false},

		{case2Statement, anonGetBucketLocationArgs, false},
		{case2Statement, anonPutObjectActionArgs, true},
		{case2Statement, anonGetObjectActionArgs, true},
		{case2Statement, getBucketLocationArgs, false},
		{case2Statement, putObjectActionArgs, true},
		{case2Statement, getObjectActionArgs, true},

		{case3Statement, anonGetBucketLocationArgs, false},
		{case3Statement, anonPutObjectActionArgs, true},
		{case3Statement, anonGetObjectActionArgs, false},
		{case3Statement, getBucketLocationArgs, false},
		{case3Statement, putObjectActionArgs, true},
		{case3Statement, getObjectActionArgs, false},

		{case4Statement, anonGetBucketLocationArgs, true},
		{case4Statement, anonPutObjectActionArgs, false},
		{case4Statement, anonGetObjectActionArgs, true},
		{case4Statement, getBucketLocationArgs, true},
		{case4Statement, putObjectActionArgs, false},
		{case4Statement, getObjectActionArgs, true},
	}

	for i, testCase := range testCases {
		result := testCase.statement.IsAllowed(testCase.args)

		if result != testCase.expectedResult {
			t.Fatalf("case %v: expected: %v, got: %v\n", i+1, testCase.expectedResult, result)
		}
	}
}

func TestStatementIsValid(t *testing.T) {
	_, IPNet1, err := net.ParseCIDR("192.168.1.0/24")
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	func1, err := condition.NewIPAddressFunc(
		condition.AWSSourceIP,
		IPNet1,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}

	func2, err := condition.NewStringEqualsFunc(
		condition.S3XAmzCopySource,
		"mybucket/myobject",
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}

	testCases := []struct {
		statement Statement
		expectErr bool
	}{
		// Invalid effect error.
		{NewStatement(
			Effect("foo"),
			NewPrincipal("*"),
			NewActionSet(GetBucketLocationAction, PutObjectAction),
			NewResourceSet(NewResource("*", "")),
			condition.NewFunctions(),
		), true},
		// Invalid principal error.
		{NewStatement(
			Allow,
			NewPrincipal(),
			NewActionSet(GetBucketLocationAction, PutObjectAction),
			NewResourceSet(NewResource("*", "")),
			condition.NewFunctions(),
		), true},
		// Empty actions error.
		{NewStatement(
			Allow,
			NewPrincipal("*"),
			NewActionSet(),
			NewResourceSet(NewResource("*", "")),
			condition.NewFunctions(),
		), true},
		// Empty resources error.
		{NewStatement(
			Allow,
			NewPrincipal("*"),
			NewActionSet(GetBucketLocationAction, PutObjectAction),
			NewResourceSet(),
			condition.NewFunctions(),
		), true},
		// Unsupported resource found for object action.
		{NewStatement(
			Allow,
			NewPrincipal("*"),
			NewActionSet(GetBucketLocationAction, PutObjectAction),
			NewResourceSet(NewResource("mybucket", "")),
			condition.NewFunctions(),
		), true},
		// Unsupported resource found for bucket action.
		{NewStatement(
			Allow,
			NewPrincipal("*"),
			NewActionSet(GetBucketLocationAction, PutObjectAction),
			NewResourceSet(NewResource("mybucket", "myobject*")),
			condition.NewFunctions(),
		), true},
		// Unsupported condition key for action.
		{NewStatement(
			Allow,
			NewPrincipal("*"),
			NewActionSet(GetObjectAction, PutObjectAction),
			NewResourceSet(NewResource("mybucket", "myobject*")),
			condition.NewFunctions(func1, func2),
		), true},
		{NewStatement(
			Deny,
			NewPrincipal("*"),
			NewActionSet(GetObjectAction, PutObjectAction),
			NewResourceSet(NewResource("mybucket", "myobject*")),
			condition.NewFunctions(func1),
		), false},
	}

	for i, testCase := range testCases {
		err := testCase.statement.isValid()
		expectErr := (err != nil)

		if expectErr != testCase.expectErr {
			t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
		}
	}
}

func TestStatementMarshalJSON(t *testing.T) {
	case1Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(),
	)
	case1Statement.SID = "SomeId1"
	case1Data := []byte(`{"Sid":"SomeId1","Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"]}`)

	func1, err := condition.NewNullFunc(
		condition.S3XAmzCopySource,
		true,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	case2Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func1),
	)
	case2Data := []byte(`{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-copy-source":[true]}}}`)

	func2, err := condition.NewNullFunc(
		condition.S3XAmzServerSideEncryption,
		false,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	case3Statement := NewStatement(
		Deny,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func2),
	)
	case3Data := []byte(`{"Effect":"Deny","Principal":{"AWS":["*"]},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::mybucket/myobject*"],"Condition":{"Null":{"s3:x-amz-server-side-encryption":[false]}}}`)

	case4Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(GetObjectAction, PutObjectAction),
		NewResourceSet(NewResource("mybucket", "myobject*")),
		condition.NewFunctions(func1, func2),
	)

	testCases := []struct {
		statement      Statement
		expectedResult []byte
		expectErr      bool
	}{
		{case1Statement, case1Data, false},
		{case2Statement, case2Data, false},
		{case3Statement, case3Data, false},
		// Invalid statement error.
		{case4Statement, nil, true},
	}

	for i, testCase := range testCases {
		result, err := json.Marshal(testCase.statement)
		expectErr := (err != nil)

		if expectErr != testCase.expectErr {
			t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
		}

		if !testCase.expectErr {
			if !reflect.DeepEqual(result, testCase.expectedResult) {
				t.Fatalf("case %v: result: expected: %v, got: %v", i+1, string(testCase.expectedResult), string(result))
			}
		}
	}
}

func TestStatementUnmarshalJSON(t *testing.T) {
	case1Data := []byte(`{
    "Sid": "SomeId1",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*"
}`)
	case1Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(),
	)
	case1Statement.SID = "SomeId1"

	case2Data := []byte(`{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*",
    "Condition": {
        "Null": {
            "s3:x-amz-copy-source": true
        }
    }
}`)
	func1, err := condition.NewNullFunc(
		condition.S3XAmzCopySource,
		true,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	case2Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func1),
	)

	case3Data := []byte(`{
    "Effect": "Deny",
    "Principal": {
        "AWS": "*"
    },
    "Action": [
        "s3:PutObject",
        "s3:GetObject"
    ],
    "Resource": "arn:aws:s3:::mybucket/myobject*",
    "Condition": {
        "Null": {
            "s3:x-amz-server-side-encryption": "false"
        }
    }
}`)
	func2, err := condition.NewNullFunc(
		condition.S3XAmzServerSideEncryption,
		false,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	case3Statement := NewStatement(
		Deny,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction, GetObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(func2),
	)

	case4Data := []byte(`{
    "Effect": "Allow",
    "Principal": "Q3AM3UQ867SPQQA43P2F",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*"
}`)

	case5Data := []byte(`{
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*"
}`)

	case6Data := []byte(`{
    "Effect": "Allow",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*"
}`)

	case7Data := []byte(`{
    "Effect": "Allow",
    "Principal": "*",
    "Resource": "arn:aws:s3:::mybucket/myobject*"
}`)

	case8Data := []byte(`{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObject"
}`)

	case9Data := []byte(`{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::mybucket/myobject*",
    "Condition": {
    }
}`)

	case10Data := []byte(`{
    "Effect": "Deny",
    "Principal": {
        "AWS": "*"
    },
    "Action": [
        "s3:PutObject",
        "s3:GetObject"
    ],
    "Resource": "arn:aws:s3:::mybucket/myobject*",
    "Condition": {
        "StringEquals": {
            "s3:x-amz-copy-source": "yourbucket/myobject*"
        }
    }
}`)

	testCases := []struct {
		data           []byte
		expectedResult Statement
		expectErr      bool
	}{
		{case1Data, case1Statement, false},
		{case2Data, case2Statement, false},
		{case3Data, case3Statement, false},
		// JSON unmarshaling error.
		{case4Data, Statement{}, true},
		// Invalid effect error.
		{case5Data, Statement{}, true},
		// empty principal error.
		{case6Data, Statement{}, true},
		// Empty action error.
		{case7Data, Statement{}, true},
		// Empty resource error.
		{case8Data, Statement{}, true},
		// Empty condition error.
		{case9Data, Statement{}, true},
		// Unsupported condition key error.
		{case10Data, Statement{}, true},
	}

	for i, testCase := range testCases {
		var result Statement
		err := json.Unmarshal(testCase.data, &result)
		expectErr := (err != nil)

		if expectErr != testCase.expectErr {
			t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
		}

		if !testCase.expectErr {
			if !reflect.DeepEqual(result, testCase.expectedResult) {
				t.Fatalf("case %v: result: expected: %v, got: %v", i+1, testCase.expectedResult, result)
			}
		}
	}
}

func TestStatementValidate(t *testing.T) {
	case1Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(PutObjectAction),
		NewResourceSet(NewResource("mybucket", "/myobject*")),
		condition.NewFunctions(),
	)

	func1, err := condition.NewNullFunc(
		condition.S3XAmzCopySource,
		true,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	func2, err := condition.NewNullFunc(
		condition.S3XAmzServerSideEncryption,
		false,
	)
	if err != nil {
		t.Fatalf("unexpected error. %v\n", err)
	}
	case2Statement := NewStatement(
		Allow,
		NewPrincipal("*"),
		NewActionSet(GetObjectAction, PutObjectAction),
		NewResourceSet(NewResource("mybucket", "myobject*")),
		condition.NewFunctions(func1, func2),
	)

	testCases := []struct {
		statement  Statement
		bucketName string
		expectErr  bool
	}{
		{case1Statement, "mybucket", false},
		{case2Statement, "mybucket", true},
		{case1Statement, "yourbucket", true},
	}

	for i, testCase := range testCases {
		err := testCase.statement.Validate(testCase.bucketName)
		expectErr := (err != nil)

		if expectErr != testCase.expectErr {
			t.Fatalf("case %v: error: expected: %v, got: %v", i+1, testCase.expectErr, expectErr)
		}
	}
}
