/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package aclmgmt

import (
	"fmt"

	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	pb "github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric/common/policies"
	"github.com/hyperledger/fabric/core/aclmgmt/resources"
	"github.com/hyperledger/fabric/core/policy"
	"github.com/hyperledger/fabric/protoutil"
)

const (
	CHANNELREADERS = policies.ChannelApplicationReaders
	CHANNELWRITERS = policies.ChannelApplicationWriters
)

type defaultACLProvider interface {
	ACLProvider
	IsPtypePolicy(resName string) bool
}

// defaultACLProvider used if resource-based ACL Provider is not provided or
// if it does not contain a policy for the named resource
type defaultACLProviderImpl struct {
	policyChecker policy.PolicyChecker

	// peer wide policy (currently not used)
	pResourcePolicyMap map[string]string

	// channel specific policy
	cResourcePolicyMap map[string]string
}

func newDefaultACLProvider(policyChecker policy.PolicyChecker) defaultACLProvider {
	d := &defaultACLProviderImpl{
		policyChecker:      policyChecker,
		pResourcePolicyMap: map[string]string{},
		cResourcePolicyMap: map[string]string{},
	}

	// -------------- _lifecycle --------------
	d.pResourcePolicyMap[resources.Lifecycle_InstallChaincode] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_QueryInstalledChaincode] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_GetInstalledChaincodePackage] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_QueryInstalledChaincodes] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_ApproveChaincodeDefinitionForMyOrg] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_QueryApprovedChaincodeDefinition] = policy.Admins
	d.pResourcePolicyMap[resources.Lifecycle_QueryApprovedChaincodeDefinitions] = policy.Admins

	d.cResourcePolicyMap[resources.Lifecycle_CommitChaincodeDefinition] = CHANNELWRITERS
	d.cResourcePolicyMap[resources.Lifecycle_QueryChaincodeDefinition] = CHANNELWRITERS
	d.cResourcePolicyMap[resources.Lifecycle_QueryChaincodeDefinitions] = CHANNELWRITERS
	d.cResourcePolicyMap[resources.Lifecycle_CheckCommitReadiness] = CHANNELWRITERS

	// -------------- snapshot ---------------
	d.pResourcePolicyMap[resources.Snapshot_submitrequest] = policy.Admins
	d.pResourcePolicyMap[resources.Snapshot_cancelrequest] = policy.Admins
	d.pResourcePolicyMap[resources.Snapshot_listpending] = policy.Admins

	// -------------- LSCC --------------
	// p resources (implemented by the chaincode currently)
	d.pResourcePolicyMap[resources.Lscc_Install] = policy.Admins
	d.pResourcePolicyMap[resources.Lscc_GetInstalledChaincodes] = policy.Admins

	// c resources
	d.cResourcePolicyMap[resources.Lscc_Deploy] = ""  // ACL check covered by PROPOSAL
	d.cResourcePolicyMap[resources.Lscc_Upgrade] = "" // ACL check covered by PROPOSAL
	d.cResourcePolicyMap[resources.Lscc_ChaincodeExists] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Lscc_GetDeploymentSpec] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Lscc_GetChaincodeData] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Lscc_GetInstantiatedChaincodes] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Lscc_GetCollectionsConfig] = CHANNELREADERS

	// -------------- QSCC --------------
	// p resources (none)

	// c resources
	d.cResourcePolicyMap[resources.Qscc_GetChainInfo] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Qscc_GetBlockByNumber] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Qscc_GetBlockByHash] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Qscc_GetTransactionByID] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Qscc_GetBlockByTxID] = CHANNELREADERS

	// --------------- CSCC resources -----------
	// p resources (implemented by the chaincode currently)
	d.pResourcePolicyMap[resources.Cscc_JoinChain] = policy.Admins
	d.pResourcePolicyMap[resources.Cscc_JoinChainBySnapshot] = policy.Admins
	d.pResourcePolicyMap[resources.Cscc_JoinBySnapshotStatus] = policy.Admins
	d.pResourcePolicyMap[resources.Cscc_GetChannels] = policy.Members

	// c resources
	d.cResourcePolicyMap[resources.Cscc_GetConfigBlock] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Cscc_GetChannelConfig] = CHANNELREADERS

	// ---------------- non-scc resources ------------
	// Peer resources
	d.cResourcePolicyMap[resources.Peer_Propose] = CHANNELWRITERS
	d.cResourcePolicyMap[resources.Peer_ChaincodeToChaincode] = CHANNELWRITERS

	// Event resources
	d.cResourcePolicyMap[resources.Event_Block] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Event_FilteredBlock] = CHANNELREADERS

	// Gateway resources
	d.cResourcePolicyMap[resources.Gateway_CommitStatus] = CHANNELREADERS
	d.cResourcePolicyMap[resources.Gateway_ChaincodeEvents] = CHANNELREADERS

	return d
}

func (d *defaultACLProviderImpl) IsPtypePolicy(resName string) bool {
	_, ok := d.pResourcePolicyMap[resName]
	return ok
}

// CheckACL provides default (v 1.0) behavior by mapping resources to their ACL for a channel.
func (d *defaultACLProviderImpl) CheckACL(resName string, channelID string, idinfo interface{}) error {
	// the default behavior is to use p type if defined and use channeless policy checks
	policy := d.pResourcePolicyMap[resName]
	if policy != "" {
		channelID = ""
	} else {
		policy = d.cResourcePolicyMap[resName]
		if policy == "" {
			aclLogger.Errorf("Unmapped policy for %s", resName)
			return fmt.Errorf("Unmapped policy for %s", resName)
		}
	}
	aclLogger.Debugw("Applying default access policy for resource", "channel", channelID, "policy", policy, "resource", resName)

	switch typedData := idinfo.(type) {
	case *pb.SignedProposal:
		return d.policyChecker.CheckPolicy(channelID, policy, typedData)
	case *common.Envelope:
		sd, err := protoutil.EnvelopeAsSignedData(typedData)
		if err != nil {
			return err
		}
		return d.policyChecker.CheckPolicyBySignedData(channelID, policy, sd)
	case *protoutil.SignedData:
		return d.policyChecker.CheckPolicyBySignedData(channelID, policy, []*protoutil.SignedData{typedData})
	case []*protoutil.SignedData:
		return d.policyChecker.CheckPolicyBySignedData(channelID, policy, typedData)
	default:
		aclLogger.Errorf("Unmapped id on checkACL %s", resName)
		return fmt.Errorf("Unknown id on checkACL %s", resName)
	}
}

// CheckACL provides default behavior by mapping channelless resources to their ACL.
func (d *defaultACLProviderImpl) CheckACLNoChannel(resName string, idinfo interface{}) error {
	policy := d.pResourcePolicyMap[resName]
	if policy == "" {
		aclLogger.Errorf("Unmapped channelless policy for %s", resName)
		return fmt.Errorf("Unmapped channelless policy for %s", resName)
	}

	switch typedData := idinfo.(type) {
	case *pb.SignedProposal:
		return d.policyChecker.CheckPolicyNoChannel(policy, typedData)
	case *common.Envelope:
		sd, err := protoutil.EnvelopeAsSignedData(typedData)
		if err != nil {
			return err
		}
		return d.policyChecker.CheckPolicyNoChannelBySignedData(policy, sd)
	case []*protoutil.SignedData:
		return d.policyChecker.CheckPolicyNoChannelBySignedData(policy, typedData)
	default:
		aclLogger.Errorf("Unmapped id on channelless checkACL %s", resName)
		return fmt.Errorf("Unknown id on channelless checkACL %s", resName)
	}
}
