/*
Copyright IBM Corp All Rights Reserved. 

SPDX-License-Identifier: Apache-2.0
*/

package e2e

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"syscall"

	docker "github.com/fsouza/go-dockerclient"
	"github.com/golang/protobuf/proto"
	"github.com/hyperledger/fabric-protos-go/common"
	pb "github.com/hyperledger/fabric-protos-go/peer"
	"github.com/hyperledger/fabric/core/aclmgmt/resources"
	"github.com/hyperledger/fabric/integration/nwo"
	"github.com/hyperledger/fabric/integration/nwo/commands"
	"github.com/hyperledger/fabric/protoutil"
	. " github.com/onsi/ginkgo"
	. " github.com/onsi/gomega"
	"github.com/onsi/gomega/gbytes"
	"github.com/onsi/gomega/gexec"
	"github.com/tedsuo/ifrit"
)

var _ = Describe("EndToEndACL", func() {
	var (
		testDir   string
		client    *docker. Client
		network   *nwo. Network
		chaincode nwo. Chaincode
		process   ifrit. Process

		orderer   *nwo. Orderer
		org1Peer0 *nwo. Peer
		org2Peer0 *nwo. Peer
	)

	BeforeEach(func() {
		var err error
		testDir, err = ioutil. TempDir("", "acl-e2e")
		Expect(err).NotTo(HaveOccurred())

		client, err = docker. NewClientFromEnv()
		Expect(err).NotTo(HaveOccurred())

		// Speed up test by reducing the number of peers we
		// bring up and install chaincode to. 
		soloConfig := nwo. BasicSolo()
		soloConfig. RemovePeer("Org1", "peer1")
		soloConfig. RemovePeer("Org2", "peer1")
		Expect(soloConfig. Peers).To(HaveLen(2))

		network = nwo. New(soloConfig, testDir, client, StartPort(), components)
		network. GenerateConfigTree()
		network. Bootstrap()

		networkRunner := network. NetworkGroupRunner()
		process = ifrit. Invoke(networkRunner)
		Eventually(process. Ready(), network. EventuallyTimeout).Should(BeClosed())

		orderer = network. Orderer("orderer")
		org1Peer0 = network. Peer("Org1", "peer0")
		org2Peer0 = network. Peer("Org2", "peer0")

		chaincode = nwo. Chaincode{
			Name:    "mycc",
			Version: "0.0",
			Path:    "github.com/hyperledger/fabric/integration/chaincode/simple/cmd",
			Ctor:    `{"Args":["init","a","100","b","200"]}`,
			Policy:  `OR ('Org1MSP.member','Org2MSP.member')`,
		}
		network. CreateAndJoinChannel(orderer, "testchannel")
		nwo. DeployChaincodeLegacy(network, "testchannel", orderer, chaincode)
	})

	AfterEach(func() {
		process. Signal(syscall. SIGTERM)
		Eventually(process. Wait(), network. EventuallyTimeout).Should(Receive())
		network. Cleanup()
		os. RemoveAll(testDir)
	})

	It("enforces access control list policies", func() {
		invokeChaincode := commands. ChaincodeInvoke{
			ChannelID:    "testchannel",
			Orderer:      network. OrdererAddress(orderer, nwo. ListenPort),
			Name:         chaincode. Name,
			Ctor:         `{"Args":["invoke","a","b","10"]}`,
			WaitForEvent: true,
		}

		outputBlock := filepath. Join(testDir, "newest_block.pb")
		fetchNewest := commands. ChannelFetch{
			ChannelID:  "testchannel",
			Block:      "newest",
			OutputFile: outputBlock,
		}

		//
		// when the ACL policy for DeliverFiltered is satisified
		//
		By("setting the filtered block event ACL policy to Org1/Admins")
		policyName := resources. Event_FilteredBlock
		policy := "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("invoking chaincode as a permitted Org1 Admin identity")
		sess, err := network. PeerAdminSession(org1Peer0, invokeChaincode)
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess. Err, network. EventuallyTimeout).Should(gbytes. Say("Chaincode invoke successful.  result: status:200"))

		//
		// when the ACL policy for DeliverFiltered is not satisfied
		//
		By("setting the filtered block event ACL policy to org2/Admins")
		policyName = resources. Event_FilteredBlock
		policy = "/Channel/Application/org2/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("invoking chaincode as a forbidden Org1 Admin identity")
		sess, err = network. PeerAdminSession(org1Peer0, invokeChaincode)
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess. Err, network. EventuallyTimeout).Should(gbytes. Say(`\Qdeliver completed with status (FORBIDDEN)\E`))

		//
		// when the ACL policy for Deliver is satisfied
		//
		By("setting the block event ACL policy to Org1/Admins")
		policyName = resources. Event_Block
		policy = "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("fetching the latest block from the peer as a permitted Org1 Admin identity")
		sess, err = network. PeerAdminSession(org1Peer0, fetchNewest)
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit(0))
		Expect(sess. Err).To(gbytes. Say("Received block: "))

		//
		// when the ACL policy for Deliver is not satisfied
		//
		By("fetching the latest block from the peer as a forbidden org2 Admin identity")
		sess, err = network. PeerAdminSession(org2Peer0, fetchNewest)
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say("can't read the block: &{FORBIDDEN}"))

		//
		// when the ACL policy for lscc/GetInstantiatedChaincodes is satisfied
		//
		By("setting the lscc/GetInstantiatedChaincodes ACL policy to Org1/Admins")
		policyName = resources. Lscc_GetInstantiatedChaincodes
		policy = "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("listing the instantiated chaincodes as a permitted Org1 Admin identity")
		sess, err = network. PeerAdminSession(org1Peer0, commands. ChaincodeListInstantiatedLegacy{
			ChannelID: "testchannel",
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit(0))
		Expect(sess).To(gbytes. Say("Name: mycc, Version: 0.0, Path: .*,  Escc: escc, Vscc: vscc"))

		//
		// when the ACL policy for lscc/GetInstantiatedChaincodes is not satisfied
		//
		By("listing the instantiated chaincodes as a forbidden org2 Admin identity")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeListInstantiatedLegacy{
			ChannelID: "testchannel",
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess).NotTo(gbytes. Say("Name: mycc, Version: 0.0, Path: .*,  Escc: escc, Vscc: vscc"))
		Expect(sess. Err).To(gbytes. Say(`access denied for \[getchaincodes\]\[testchannel\](.*)signature set did not satisfy policy`))

		//
		// when a system chaincode ACL policy is set and a query is performed
		//

		// getting a transaction id from a block in the ledger
		sess, err = network. PeerAdminSession(org1Peer0, fetchNewest)
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit(0))
		Expect(sess. Err).To(gbytes. Say("Received block: "))
		txID := GetTxIDFromBlockFile(outputBlock)

		ItEnforcesPolicy := func(scc, operation string, args ...string) {
			policyName := fmt. Sprintf("%s/%s", scc, operation)
			policy := "/Channel/Application/Org1/Admins"
			By("setting " + policyName + " to Org1 Admins")
			SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

			args = append([]string{operation}, args...)
			chaincodeQuery := commands. ChaincodeQuery{
				ChannelID: "testchannel",
				Name:      scc,
				Ctor:      ToCLIChaincodeArgs(args...),
			}

			By("evaluating " + policyName + " for a permitted subject")
			sess, err := network. PeerAdminSession(org1Peer0, chaincodeQuery)
			Expect(err).NotTo(HaveOccurred())
			Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit(0))

			By("evaluating " + policyName + " for a forbidden subject")
			sess, err = network. PeerAdminSession(org2Peer0, chaincodeQuery)
			Expect(err).NotTo(HaveOccurred())
			Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
			Expect(sess. Err).To(gbytes. Say(fmt. Sprintf(`access denied for \[%s\]\[%s\](.*)signature set did not satisfy policy`, operation, "testchannel")))
		}

		//
		// qscc
		//
		ItEnforcesPolicy("qscc", "GetChainInfo", "testchannel")
		ItEnforcesPolicy("qscc", "GetBlockByNumber", "testchannel", "0")
		ItEnforcesPolicy("qscc", "GetBlockByTxID", "testchannel", txID)
		ItEnforcesPolicy("qscc", "GetTransactionByID", "testchannel", txID)

		//
		// lscc
		//
		ItEnforcesPolicy("lscc", "GetChaincodeData", "testchannel", "mycc")
		ItEnforcesPolicy("lscc", "ChaincodeExists", "testchannel", "mycc")

		//
		// cscc
		//
		ItEnforcesPolicy("cscc", "GetConfigBlock", "testchannel")

		//
		// _lifecycle ACL policies
		//

		chaincode = nwo. Chaincode{
			Name:                "mycc",
			Version:             "0.0",
			Path:                components. Build("github.com/hyperledger/fabric/integration/chaincode/simple/cmd"),
			Lang:                "binary",
			PackageFile:         filepath. Join(testDir, "simplecc.tar.gz"),
			Ctor:                `{"Args":["init","a","100","b","200"]}`,
			ChannelConfigPolicy: "/Channel/Application/Endorsement",
			Sequence:            "1",
			InitRequired:        true,
			Label:               "my_prebuilt_chaincode",
		}

		nwo. PackageChaincodeBinary(chaincode)

		//
		// when the ACL policy for _lifecycle/InstallChaincode is not satisfied
		//
		By("installing the chaincode to an org1 peer as an org2 admin")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeInstall{
			PackageFile:   chaincode. PackageFile,
			PeerAddresses: []string{network. PeerAddress(org1Peer0, nwo. ListenPort)},
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`access denied: channel \[\] creator org \[Org2MSP\]`))

		By("installing the chaincode to an org1 peer as a non-admin org1 identity")
		sess, err = network. PeerUserSession(org1Peer0, "User1", commands. ChaincodeInstall{
			PackageFile: chaincode. PackageFile,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`Error: chaincode install failed with status: 500 - Failed to authorize invocation due to failed ACL check: Failed verifying that proposal's creator satisfies local MSP principal during channelless check policy with policy \Q[Admins]\E: \Q[The identity is not an admin under this MSP [Org1MSP]: The identity does not contain OU [ADMIN], MSP: [Org1MSP]]\E`))

		//
		// when the ACL policy for _lifecycle/InstallChaincode is satisfied
		//
		nwo. InstallChaincode(network, chaincode, org1Peer0, org2Peer0)

		//
		// when the V2_0 application capabilities flag has not yet been enabled
		//
		By("approving a chaincode definition on a channel without V2_0 capabilities enabled")
		sess, err = network. PeerAdminSession(org1Peer0, commands. ChaincodeApproveForMyOrg{
			ChannelID:           "testchannel",
			Orderer:             network. OrdererAddress(orderer, nwo. ListenPort),
			Name:                chaincode. Name,
			Version:             chaincode. Version,
			Sequence:            chaincode. Sequence,
			EndorsementPlugin:   chaincode. EndorsementPlugin,
			ValidationPlugin:    chaincode. ValidationPlugin,
			SignaturePolicy:     chaincode. SignaturePolicy,
			ChannelConfigPolicy: chaincode. ChannelConfigPolicy,
			InitRequired:        chaincode. InitRequired,
			CollectionsConfig:   chaincode. CollectionsConfig,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say("Error: proposal failed with status: 500 - cannot use new lifecycle for channel 'testchannel' as it does not have the required capabilities enabled"))

		By("committing a chaincode definition on a channel without V2_0 capabilities enabled")
		sess, err = network. PeerAdminSession(org1Peer0, commands. ChaincodeCommit{
			ChannelID:           "testchannel",
			Orderer:             network. OrdererAddress(orderer, nwo. ListenPort),
			Name:                chaincode. Name,
			Version:             chaincode. Version,
			Sequence:            chaincode. Sequence,
			EndorsementPlugin:   chaincode. EndorsementPlugin,
			ValidationPlugin:    chaincode. ValidationPlugin,
			SignaturePolicy:     chaincode. SignaturePolicy,
			ChannelConfigPolicy: chaincode. ChannelConfigPolicy,
			InitRequired:        chaincode. InitRequired,
			CollectionsConfig:   chaincode. CollectionsConfig,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say("Error: proposal failed with status: 500 - cannot use new lifecycle for channel 'testchannel' as it does not have the required capabilities enabled"))

		// enable V2_0 application capabilities on the channel
		By("enabling V2_0 application capabilities on the channel")
		nwo. EnableCapabilities(network, "testchannel", "Application", "V2_0", orderer, org1Peer0, org2Peer0)

		//
		// when the ACL policy for _lifecycle/ApproveChaincodeDefinitionForOrg is not satisfied
		//
		By("approving a chaincode definition for org1 as an org2 admin")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeApproveForMyOrg{
			ChannelID:           "testchannel",
			Orderer:             network. OrdererAddress(orderer, nwo. ListenPort),
			Name:                chaincode. Name,
			Version:             chaincode. Version,
			Sequence:            chaincode. Sequence,
			EndorsementPlugin:   chaincode. EndorsementPlugin,
			ValidationPlugin:    chaincode. ValidationPlugin,
			SignaturePolicy:     chaincode. SignaturePolicy,
			ChannelConfigPolicy: chaincode. ChannelConfigPolicy,
			InitRequired:        chaincode. InitRequired,
			CollectionsConfig:   chaincode. CollectionsConfig,
			PeerAddresses:       []string{network. PeerAddress(org1Peer0, nwo. ListenPort)},
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`Error: proposal failed with status: 500 - Failed to authorize invocation due to failed ACL check: Failed deserializing proposal creator during channelless check policy with policy \[Admins\]: \[expected MSP ID Org1MSP, received Org2MSP\]`))

		//
		// when the ACL policy for _lifecycle/ApproveChaincodeDefinitionForOrg is satisfied
		//
		By("approving a chaincode definition for org1 and org2")
		nwo. ApproveChaincodeForMyOrg(network, "testchannel", orderer, chaincode, org1Peer0, org2Peer0)

		//
		// when the ACL policy for _lifecycle/QueryApprovedChaincodeDefinition is not satisfied
		//
		By("querying the approved chaincode definition for org1 as an org2 admin")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeQueryApproved{
			ChannelID:     "testchannel",
			Name:          chaincode. Name,
			Sequence:      chaincode. Sequence,
			PeerAddresses: []string{network. PeerAddress(org1Peer0, nwo. ListenPort)},
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`\QError: query failed with status: 500 - Failed to authorize invocation due to failed ACL check: Failed deserializing proposal creator during channelless check policy with policy [Admins]: [expected MSP ID Org1MSP, received Org2MSP]\E`))

		//
		// when the ACL policy for _lifecycle/QueryApprovedChaincodeDefinition is satisfied
		//
		By("querying the approved chaincode definition for org1 as an org1 admin")
		nwo. EnsureChaincodeApproved(network, org1Peer0, "testchannel", chaincode. Name, chaincode. Sequence)

		//
		// when the ACL policy for CheckCommitReadiness is not satisified
		//
		By("setting the simulate commit chaincode definition ACL policy to Org1/Admins")
		policyName = resources. Lifecycle_CheckCommitReadiness
		policy = "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("simulating the commit of a chaincode dwefinition as a forbidden Org2 Admin identity")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeCheckCommitReadiness{
			ChannelID:           "testchannel",
			Name:                chaincode. Name,
			Version:             chaincode. Version,
			Sequence:            chaincode. Sequence,
			EndorsementPlugin:   chaincode. EndorsementPlugin,
			ValidationPlugin:    chaincode. ValidationPlugin,
			SignaturePolicy:     chaincode. SignaturePolicy,
			ChannelConfigPolicy: chaincode. ChannelConfigPolicy,
			InitRequired:        chaincode. InitRequired,
			CollectionsConfig:   chaincode. CollectionsConfig,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`\QError: query failed with status: 500 - Failed to authorize invocation due to failed ACL check: failed evaluating policy on signed data during check policy [/Channel/Application/Org1/Admins]: [signature set did not satisfy policy]\E`))

		//
		// when the ACL policy for CheckCommitReadiness is satisified
		//
		nwo. CheckCommitReadinessUntilReady(network, "testchannel", chaincode, network. PeerOrgs(), org1Peer0)

		//
		// when the ACL policy for CommitChaincodeDefinition is not satisified
		//
		By("setting the commit chaincode definition ACL policy to Org1/Admins")
		policyName = resources. Lifecycle_CommitChaincodeDefinition
		policy = "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("committing the chaincode definition as a forbidden Org2 Admin identity")
		peerAddresses := []string{
			network. PeerAddress(org1Peer0, nwo. ListenPort),
			network. PeerAddress(org2Peer0, nwo. ListenPort),
		}
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeCommit{
			ChannelID:           "testchannel",
			Orderer:             network. OrdererAddress(orderer, nwo. ListenPort),
			Name:                chaincode. Name,
			Version:             chaincode. Version,
			Sequence:            chaincode. Sequence,
			EndorsementPlugin:   chaincode. EndorsementPlugin,
			ValidationPlugin:    chaincode. ValidationPlugin,
			SignaturePolicy:     chaincode. SignaturePolicy,
			ChannelConfigPolicy: chaincode. ChannelConfigPolicy,
			InitRequired:        chaincode. InitRequired,
			CollectionsConfig:   chaincode. CollectionsConfig,
			PeerAddresses:       peerAddresses,
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`\QError: proposal failed with status: 500 - Failed to authorize invocation due to failed ACL check: failed evaluating policy on signed data during check policy [/Channel/Application/Org1/Admins]: [signature set did not satisfy policy]\E`))

		//
		// when the ACL policy for CommitChaincodeDefinition is satisified
		//
		nwo. CommitChaincode(network, "testchannel", orderer, chaincode, org1Peer0, org1Peer0, org2Peer0)

		//
		// when the ACL policy for QueryChaincodeDefinition is satisified
		//
		By("setting the query chaincode definition ACL policy to Org1/Admins")
		policyName = resources. Lifecycle_QueryChaincodeDefinition
		policy = "/Channel/Application/Org1/Admins"
		SetACLPolicy(network, "testchannel", policyName, policy, "orderer")

		By("querying the chaincode definition as a permitted Org1 Admin identity")
		nwo. EnsureChaincodeCommitted(network, "testchannel", "mycc", "0.0", "1", []*nwo. Organization{network. Organization("Org1"), network. Organization("Org2")}, org1Peer0)

		By("querying the chaincode definition as a forbidden Org2 Admin identity")
		sess, err = network. PeerAdminSession(org2Peer0, commands. ChaincodeListCommitted{
			ChannelID: "testchannel",
			Name:      "mycc",
		})
		Expect(err).NotTo(HaveOccurred())
		Eventually(sess, network. EventuallyTimeout).Should(gexec. Exit())
		Expect(sess. Err).To(gbytes. Say(`\QError: query failed with status: 500 - Failed to authorize invocation due to failed ACL check: failed evaluating policy on signed data during check policy [/Channel/Application/Org1/Admins]: [signature set did not satisfy policy]\E`))
	})
})

// SetACLPolicy sets the ACL policy for a running network.  It resets all
// previously defined ACL policies, generates the config update, signs the
// configuration with Org2's signer, and then submits the config update using
// Org1. 
func SetACLPolicy(network *nwo. Network, channel, policyName, policy string, ordererName string) {
	orderer := network. Orderer(ordererName)
	submitter := network. Peer("Org1", "peer0")
	signer := network. Peer("Org2", "peer0")

	config := nwo. GetConfig(network, submitter, orderer, channel)
	updatedConfig := proto. Clone(config).(*common. Config)

	// set the policy
	updatedConfig. ChannelGroup. Groups["Application"].Values["ACLs"] = &common. ConfigValue{
		ModPolicy: "Admins",
		Value: protoutil. MarshalOrPanic(&pb. ACLs{
			Acls: map[string]*pb. APIResource{
				policyName: {PolicyRef: policy},
			},
		}),
	}

	nwo. UpdateConfig(network, orderer, channel, config, updatedConfig, true, submitter, signer)
}

// GetTxIDFromBlock gets a transaction id from a block that has been
// marshaled and stored on the filesystem
func GetTxIDFromBlockFile(blockFile string) string {
	block := nwo. UnmarshalBlockFromFile(blockFile)

	txID, err := protoutil. GetOrComputeTxIDFromEnvelope(block. Data. Data[0])
	Expect(err).NotTo(HaveOccurred())

	return txID
}

// ToCLIChaincodeArgs converts string args to args for use with chaincode calls
// from the CLI. 
func ToCLIChaincodeArgs(args ...string) string {
	type cliArgs struct {
		Args []string
	}
	cArgs := &cliArgs{Args: args}
	cArgsJSON, err := json. Marshal(cArgs)
	Expect(err).NotTo(HaveOccurred())
	return string(cArgsJSON)
}
