github.com/jpmorganchase/quorum@v21.1.0+incompatible/multitenancy/authorization_provider.go (about)

     1  package multitenancy
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"strings"
     9  
    10  	"github.com/ethereum/go-ethereum/common"
    11  	"github.com/ethereum/go-ethereum/log"
    12  	"github.com/jpmorganchase/quorum-security-plugin-sdk-go/proto"
    13  )
    14  
    15  var (
    16  	ErrNotAuthorized               = errors.New("not authorized")
    17  	CtxKeyAuthorizeCreateFunc      = "AUTHORIZE_CREATE_FUNC"
    18  	CtxKeyAuthorizeMessageCallFunc = "AUTHORIZE_MESSAGE_CALL_FUNC"
    19  )
    20  
    21  // AccountAuthorizationProvider performs authorization checks for Ethereum Account
    22  // based on what is entitled in the proto.PreAuthenticatedAuthenticationToken
    23  // and what is asked in ContractSecurityAttribute list.
    24  // Note: place holder for future, this is to protect Value Transfer between accounts.
    25  type AccountAuthorizationProvider interface {
    26  	IsAuthorized(ctx context.Context, authToken *proto.PreAuthenticatedAuthenticationToken, attr *AccountStateSecurityAttribute) (bool, error)
    27  }
    28  
    29  type AuthorizeCreateFunc func() bool
    30  
    31  // AuthorizeMessageCallFunc returns if a contract is authorized to be read / write
    32  type AuthorizeMessageCallFunc func(contractAddress common.Address) (authorizedRead bool, authorizedWrite bool, err error)
    33  
    34  // ContractAuthorizationProvider performs authorization checks for contract
    35  // based on what is entitled in the proto.PreAuthenticatedAuthenticationToken
    36  // and what is asked in ContractSecurityAttribute list.
    37  type ContractAuthorizationProvider interface {
    38  	IsAuthorized(ctx context.Context, authToken *proto.PreAuthenticatedAuthenticationToken, attributes ...*ContractSecurityAttribute) (bool, error)
    39  }
    40  
    41  type DefaultContractAuthorizationProvider struct {
    42  }
    43  
    44  // isAuthorized performs authorization check for one security attribute against
    45  // the granted access inside the pre-authenticated access token.
    46  func (cm *DefaultContractAuthorizationProvider) isAuthorized(authToken *proto.PreAuthenticatedAuthenticationToken, attr *ContractSecurityAttribute) (bool, error) {
    47  	query := url.Values{}
    48  	switch attr.Visibility {
    49  	case VisibilityPublic:
    50  		switch attr.Action {
    51  		case ActionRead, ActionWrite, ActionCreate:
    52  			if (attr.To == common.Address{}) {
    53  				query.Set(QueryOwnedEOA, toHexAddress(attr.From))
    54  			} else {
    55  				query.Set(QueryOwnedEOA, toHexAddress(attr.To))
    56  			}
    57  		}
    58  	case VisibilityPrivate:
    59  		switch attr.Action {
    60  		case ActionRead, ActionWrite:
    61  			if (attr.To == common.Address{}) {
    62  				query.Set(QueryOwnedEOA, toHexAddress(attr.From))
    63  			} else {
    64  				query.Set(QueryOwnedEOA, toHexAddress(attr.To))
    65  			}
    66  			for _, tm := range attr.Parties {
    67  				query.Add(QueryFromTM, tm)
    68  			}
    69  		case ActionCreate:
    70  			query.Set(QueryFromTM, attr.PrivateFrom)
    71  		}
    72  	}
    73  	// construct request permission identifier
    74  	request, err := url.Parse(fmt.Sprintf("%s://%s/%s/%s?%s", attr.Visibility, toHexAddress(attr.From), attr.Action, "contracts", query.Encode()))
    75  	if err != nil {
    76  		return false, err
    77  	}
    78  	// compare the contract security attribute with the consolidate list
    79  	for _, granted := range authToken.GetAuthorities() {
    80  		pi, err := url.Parse(granted.GetRaw())
    81  		if err != nil {
    82  			continue
    83  		}
    84  		granted := pi.String()
    85  		ask := request.String()
    86  		isMatched := match(attr, request, pi)
    87  		log.Debug("Checking contract access", "passed", isMatched, "granted", granted, "ask", ask)
    88  		if isMatched {
    89  			return true, nil
    90  		}
    91  	}
    92  	return false, nil
    93  }
    94  
    95  // IsAuthorized performs authorization check for each security attribute against
    96  // the granted access inside the pre-authenticated access token.
    97  //
    98  // All security attributes must pass.
    99  func (cm *DefaultContractAuthorizationProvider) IsAuthorized(_ context.Context, authToken *proto.PreAuthenticatedAuthenticationToken, attributes ...*ContractSecurityAttribute) (bool, error) {
   100  	if len(attributes) == 0 {
   101  		return false, nil
   102  	}
   103  	for _, attr := range attributes {
   104  		isMatched, err := cm.isAuthorized(authToken, attr)
   105  		if err != nil {
   106  			return false, err
   107  		}
   108  		if !isMatched {
   109  			return false, nil
   110  		}
   111  	}
   112  	return true, nil
   113  }
   114  
   115  func toHexAddress(a common.Address) string {
   116  	if (a == common.Address{}) {
   117  		return AnyEOAAddress
   118  	}
   119  	return strings.ToLower(a.Hex())
   120  }
   121  
   122  func match(attr *ContractSecurityAttribute, ask, granted *url.URL) bool {
   123  	askScheme := strings.ToLower(ask.Scheme)
   124  	if allowedPublic(askScheme) {
   125  		return true
   126  	}
   127  
   128  	isPathMatched := matchPath(strings.ToLower(ask.Path), strings.ToLower(granted.Path))
   129  	return askScheme == strings.ToLower(granted.Scheme) && //Note: "askScheme" here is "private" since we checked VisibilityPublic above.
   130  		matchHost(attr.Action, strings.ToLower(ask.Host), strings.ToLower(granted.Host)) && //whether i have permission to execute using this ethereum address
   131  		isPathMatched && //is our permission for the same action (read, write, deploy)
   132  		matchQuery(attr, ask.Query(), granted.Query())
   133  }
   134  
   135  func allowedPublic(scheme string) bool {
   136  	return scheme == string(VisibilityPublic)
   137  }
   138  
   139  func matchHost(a ContractAction, ask string, granted string) bool {
   140  	// for READ action, we use owned.eoa query param instead
   141  	return granted == AnyEOAAddress || ask == granted || a == ActionRead
   142  }
   143  
   144  func matchPath(ask string, granted string) bool {
   145  	return strings.HasPrefix(granted, "/_") || ask == granted
   146  }
   147  
   148  func matchQuery(attr *ContractSecurityAttribute, ask, granted url.Values) bool {
   149  	// if asking nothing, we should bail out
   150  	if len(ask) == 0 || len(ask[QueryFromTM]) == 0 {
   151  		return false
   152  	}
   153  	// possible scenarios:
   154  	// 1. read/write -> from.tm -> at least 1 of the same key must appear in both lists
   155  	// 2. read/write - owned.eoa/to.eoa -> check subset
   156  	// 3. create -> from.tm/owned.eoa/to.eoa -> check subset
   157  	for k, askValues := range ask {
   158  		grantedValues := granted[k]
   159  		switch attr.Action {
   160  		case ActionRead, ActionWrite:
   161  			// Scenario 1
   162  			if k == QueryFromTM {
   163  				if isIntersectionEmpty(grantedValues, askValues) {
   164  					return false
   165  				}
   166  			}
   167  			//Scenario 2
   168  			if k == QueryOwnedEOA || k == QueryToEOA {
   169  				if !subset(grantedValues, askValues) {
   170  					return false
   171  				}
   172  			}
   173  		case ActionCreate:
   174  			//Scenario 3
   175  			if !subset(grantedValues, askValues) {
   176  				return false
   177  			}
   178  		default:
   179  			// we don't know, better reject
   180  			log.Error("unsupported action", "action", attr.Action)
   181  			return false
   182  		}
   183  	}
   184  	return true
   185  }
   186  
   187  func subset(grantedValues, askValues []string) bool {
   188  	for _, askValue := range askValues {
   189  		found := false
   190  		sanitizedAskValue := askValue
   191  		if strings.HasPrefix(askValue, "0x") {
   192  			sanitizedAskValue = strings.ToLower(askValue)
   193  		}
   194  		for _, grantedValue := range grantedValues {
   195  			sanitizedGrantedValue := grantedValue
   196  			if strings.HasPrefix(grantedValue, "0x") {
   197  				sanitizedGrantedValue = strings.ToLower(grantedValue)
   198  			}
   199  			if sanitizedGrantedValue == AnyEOAAddress || sanitizedAskValue == sanitizedGrantedValue {
   200  				found = true
   201  				break
   202  			}
   203  		}
   204  		if !found {
   205  			return false
   206  		}
   207  	}
   208  	return true
   209  }
   210  
   211  func isIntersectionEmpty(grantedValues, askValues []string) bool {
   212  	grantedMap := make(map[string]bool)
   213  	for _, grantedVal := range grantedValues {
   214  		grantedMap[grantedVal] = true
   215  	}
   216  	for _, askVal := range askValues {
   217  		if grantedMap[askVal] {
   218  			return false
   219  		}
   220  	}
   221  	return true
   222  }