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 }