Skip to content

Commit

Permalink
try to fix #106
Browse files Browse the repository at this point in the history
Signed-off-by: Sam Yuan <[email protected]>
  • Loading branch information
SamYuan1990 committed Mar 4, 2023
1 parent d97cfb6 commit de5fd6b
Show file tree
Hide file tree
Showing 2 changed files with 366 additions and 2 deletions.
354 changes: 354 additions & 0 deletions pkg/chaincode/signaturepolicy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package chaincode

import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"

"github.com/Knetic/govaluate"
cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
mb "github.com/hyperledger/fabric-protos-go-apiv2/msp"
"google.golang.org/protobuf/proto"
)

// Gate values
const (
GateAnd = "And"
GateOr = "Or"
GateOutOf = "OutOf"
)

// Role values for principals
const (
RoleAdmin = "admin"
RoleMember = "member"
RoleClient = "client"
RolePeer = "peer"
RoleOrderer = "orderer"
)

var regex = regexp.MustCompile(
fmt.Sprintf("^([[:alnum:].-]+)([.])(%s|%s|%s|%s|%s)$",
RoleAdmin, RoleMember, RoleClient, RolePeer, RoleOrderer),
)

var regexErr = regexp.MustCompile("^No parameter '([^']+)' found[.]$")

// a stub function - it returns the same string as it's passed.
// This will be evaluated by second/third passes to convert to a proto policy
func outof(args ...interface{}) (interface{}, error) {
toret := "outof("

if len(args) < 2 {
return nil, fmt.Errorf("expected at least two arguments to NOutOf. Given %d", len(args))
}

arg0 := args[0]
// govaluate treats all numbers as float64 only. But and/or may pass int/string. Allowing int/string for flexibility of caller
if n, ok := arg0.(float64); ok {
toret += strconv.Itoa(int(n))
} else if n, ok := arg0.(int); ok {
toret += strconv.Itoa(n)
} else if n, ok := arg0.(string); ok {
toret += n
} else {
return nil, fmt.Errorf("unexpected type %s", reflect.TypeOf(arg0))
}

for _, arg := range args[1:] {
toret += ", "

switch t := arg.(type) {
case string:
if regex.MatchString(t) {
toret += "'" + t + "'"
} else {
toret += t
}
default:
return nil, fmt.Errorf("unexpected type %s", reflect.TypeOf(arg))
}
}

return toret + ")", nil
}

func and(args ...interface{}) (interface{}, error) {
args = append([]interface{}{len(args)}, args...)
return outof(args...)
}

func or(args ...interface{}) (interface{}, error) {
args = append([]interface{}{1}, args...)
return outof(args...)
}

func firstPass(args ...interface{}) (interface{}, error) {
toret := "outof(ID"
for _, arg := range args {
toret += ", "

switch t := arg.(type) {
case string:
if regex.MatchString(t) {
toret += "'" + t + "'"
} else {
toret += t
}
case float32:
case float64:
toret += strconv.Itoa(int(t))
default:
return nil, fmt.Errorf("unexpected type %s", reflect.TypeOf(arg))
}
}

return toret + ")", nil
}

// SignedBy creates a SignaturePolicy requiring a given signer's signature
func SignedBy(index int32) *cb.SignaturePolicy {
return &cb.SignaturePolicy{
Type: &cb.SignaturePolicy_SignedBy{
SignedBy: index,
},
}
}

// NOutOf creates a policy which requires N out of the slice of policies to evaluate to true
func NOutOf(n int32, policies []*cb.SignaturePolicy) *cb.SignaturePolicy {
return &cb.SignaturePolicy{
Type: &cb.SignaturePolicy_NOutOf_{
NOutOf: &cb.SignaturePolicy_NOutOf{
N: n,
Rules: policies,
},
},
}
}

func secondPass(args ...interface{}) (interface{}, error) {
/* general sanity check, we expect at least 3 args */
if len(args) < 3 {
return nil, fmt.Errorf("at least 3 arguments expected, got %d", len(args))
}

/* get the first argument, we expect it to be the context */
var ctx *policyContext
switch v := args[0].(type) {
case *policyContext:
ctx = v
default:
return nil, fmt.Errorf("unrecognized type, expected the context, got %s", reflect.TypeOf(args[0]))
}

/* get the second argument, we expect an integer telling us
how many of the remaining we expect to have*/
var t int
switch arg := args[1].(type) {
case float64:
t = int(arg)
default:
return nil, fmt.Errorf("unrecognized type, expected a number, got %s", reflect.TypeOf(args[1]))
}

/* get the n in the t out of n */
n := len(args) - 2

/* sanity check - t should be positive, permit equal to n+1, but disallow over n+1 */
if t < 0 || t > n+1 {
return nil, fmt.Errorf("invalid t-out-of-n predicate, t %d, n %d", t, n)
}

policies := make([]*cb.SignaturePolicy, 0)

/* handle the rest of the arguments */
for _, principal := range args[2:] {
switch t := principal.(type) {
/* if it's a string, we expect it to be formed as
<MSP_ID> . <ROLE>, where MSP_ID is the MSP identifier
and ROLE is either a member, an admin, a client, a peer or an orderer*/
case string:
/* split the string */
subm := regex.FindAllStringSubmatch(t, -1)
if subm == nil || len(subm) != 1 || len(subm[0]) != 4 {
return nil, fmt.Errorf("error parsing principal %s", t)
}

/* get the right role */
var r mb.MSPRole_MSPRoleType

switch subm[0][3] {
case RoleMember:
r = mb.MSPRole_MEMBER
case RoleAdmin:
r = mb.MSPRole_ADMIN
case RoleClient:
r = mb.MSPRole_CLIENT
case RolePeer:
r = mb.MSPRole_PEER
case RoleOrderer:
r = mb.MSPRole_ORDERER
default:
return nil, fmt.Errorf("error parsing role %s", t)
}

/* build the principal we've been told */
mspRole, err := proto.Marshal(&mb.MSPRole{MspIdentifier: subm[0][1], Role: r})
if err != nil {
return nil, fmt.Errorf("error marshalling msp role: %s", err)
}

p := &mb.MSPPrincipal{
PrincipalClassification: mb.MSPPrincipal_ROLE,
Principal: mspRole,
}
ctx.principals = append(ctx.principals, p)

/* create a SignaturePolicy that requires a signature from
the principal we've just built*/
dapolicy := SignedBy(int32(ctx.IDNum))
policies = append(policies, dapolicy)

/* increment the identity counter. Note that this is
suboptimal as we are not reusing identities. We
can deduplicate them easily and make this puppy
smaller. For now it's fine though */
// TODO: deduplicate principals
ctx.IDNum++

/* if we've already got a policy we're good, just append it */
case *cb.SignaturePolicy:
policies = append(policies, t)

default:
return nil, fmt.Errorf("unrecognized type, expected a principal or a policy, got %s", reflect.TypeOf(principal))
}
}

return NOutOf(int32(t), policies), nil
}

type policyContext struct {
IDNum int
principals []*mb.MSPPrincipal
}

func newContext() *policyContext {
return &policyContext{IDNum: 0, principals: make([]*mb.MSPPrincipal, 0)}
}

func FromString(policy string) (*cb.SignaturePolicyEnvelope, error) {
// first we translate the and/or business into outof gates
intermediate, err := govaluate.NewEvaluableExpressionWithFunctions(
policy, map[string]govaluate.ExpressionFunction{
GateAnd: and,
strings.ToLower(GateAnd): and,
strings.ToUpper(GateAnd): and,
GateOr: or,
strings.ToLower(GateOr): or,
strings.ToUpper(GateOr): or,
GateOutOf: outof,
strings.ToLower(GateOutOf): outof,
strings.ToUpper(GateOutOf): outof,
},
)
if err != nil {
return nil, err
}

intermediateRes, err := intermediate.Evaluate(map[string]interface{}{})
if err != nil {
// attempt to produce a meaningful error
if regexErr.MatchString(err.Error()) {
sm := regexErr.FindStringSubmatch(err.Error())
if len(sm) == 2 {
return nil, fmt.Errorf("unrecognized token '%s' in policy string", sm[1])
}
}

return nil, err
}

resStr, ok := intermediateRes.(string)
if !ok {
return nil, fmt.Errorf("invalid policy string '%s'", policy)
}

// we still need two passes. The first pass just adds an extra
// argument ID to each of the outof calls. This is
// required because govaluate has no means of giving context
// to user-implemented functions other than via arguments.
// We need this argument because we need a global place where
// we put the identities that the policy requires
exp, err := govaluate.NewEvaluableExpressionWithFunctions(
resStr,
map[string]govaluate.ExpressionFunction{"outof": firstPass},
)
if err != nil {
return nil, err
}

res, err := exp.Evaluate(map[string]interface{}{})
if err != nil {
// attempt to produce a meaningful error
if regexErr.MatchString(err.Error()) {
sm := regexErr.FindStringSubmatch(err.Error())
if len(sm) == 2 {
return nil, fmt.Errorf("unrecognized token '%s' in policy string", sm[1])
}
}

return nil, err
}

resStr, ok = res.(string)
if !ok {
return nil, fmt.Errorf("invalid policy string '%s'", policy)
}

ctx := newContext()
parameters := make(map[string]interface{}, 1)
parameters["ID"] = ctx

exp, err = govaluate.NewEvaluableExpressionWithFunctions(
resStr,
map[string]govaluate.ExpressionFunction{"outof": secondPass},
)
if err != nil {
return nil, err
}

res, err = exp.Evaluate(parameters)
if err != nil {
// attempt to produce a meaningful error
if regexErr.MatchString(err.Error()) {
sm := regexErr.FindStringSubmatch(err.Error())
if len(sm) == 2 {
return nil, fmt.Errorf("unrecognized token '%s' in policy string", sm[1])
}
}

return nil, err
}

rule, ok := res.(*cb.SignaturePolicy)
if !ok {
return nil, fmt.Errorf("invalid policy string '%s'", policy)
}

p := &cb.SignaturePolicyEnvelope{
Identities: ctx.principals,
Version: 0,
Rule: rule,
}

return p, nil
}
14 changes: 12 additions & 2 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
. "github.com/onsi/gomega"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)

const (
Expand Down Expand Up @@ -246,7 +247,16 @@ var _ = Describe("e2e", func() {
})

time.Sleep(time.Duration(20) * time.Second)

PolicyStr := "AND ('Org1MSP.peer','Org2MSP.peer')"
signaturePolicyEnvelope, err := chaincode.FromString(PolicyStr)
Expect(err).NotTo(HaveOccurred())
applicationPolicy := &peer.ApplicationPolicy{
Type: &peer.ApplicationPolicy_SignaturePolicy{
SignaturePolicy: signaturePolicyEnvelope,
},
}
data, err := proto.Marshal(applicationPolicy)
Expect(err).NotTo(HaveOccurred())
chaincodeDef := &chaincode.Definition{
ChannelName: channelName,
PackageID: "",
Expand All @@ -255,7 +265,7 @@ var _ = Describe("e2e", func() {
EndorsementPlugin: "",
ValidationPlugin: "",
Sequence: 1,
ValidationParameter: nil,
ValidationParameter: data,
InitRequired: false,
Collections: nil,
}
Expand Down

0 comments on commit de5fd6b

Please sign in to comment.