permissions.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. package ccq
  2. import (
  3. "context"
  4. "encoding/hex"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "os"
  10. "strings"
  11. "sync"
  12. "github.com/certusone/wormhole/node/pkg/common"
  13. "github.com/certusone/wormhole/node/pkg/query"
  14. "github.com/wormhole-foundation/wormhole/sdk/vaa"
  15. "go.uber.org/zap"
  16. "golang.org/x/time/rate"
  17. "github.com/gagliardetto/solana-go"
  18. "gopkg.in/godo.v2/watcher/fswatch"
  19. )
  20. type (
  21. Config struct {
  22. AllowAnythingSupported bool `json:"AllowAnythingSupported"`
  23. DefaultRateLimit float64 `json:"DefaultRateLimit"`
  24. DefaultBurstSize int `json:"DefaultBurstSize"`
  25. Permissions []User `json:"Permissions"`
  26. }
  27. User struct {
  28. UserName string `json:"userName"`
  29. ApiKey string `json:"apiKey"`
  30. AllowUnsigned bool `json:"allowUnsigned"`
  31. AllowAnything bool `json:"allowAnything"`
  32. RateLimit *float64 `json:"RateLimit"`
  33. BurstSize *int `json:"BurstSize"`
  34. LogResponses bool `json:"logResponses"`
  35. AllowedCalls []AllowedCall `json:"allowedCalls"`
  36. }
  37. AllowedCall struct {
  38. EthCall *EthCall `json:"ethCall"`
  39. EthCallByTimestamp *EthCallByTimestamp `json:"ethCallByTimestamp"`
  40. EthCallWithFinality *EthCallWithFinality `json:"ethCallWithFinality"`
  41. SolanaAccount *SolanaAccount `json:"solAccount"`
  42. SolanaPda *SolanaPda `json:"solPDA"`
  43. }
  44. EthCall struct {
  45. Chain int `json:"chain"`
  46. ContractAddress string `json:"contractAddress"`
  47. Call string `json:"call"`
  48. }
  49. EthCallByTimestamp struct {
  50. Chain int `json:"chain"`
  51. ContractAddress string `json:"contractAddress"`
  52. Call string `json:"call"`
  53. }
  54. EthCallWithFinality struct {
  55. Chain int `json:"chain"`
  56. ContractAddress string `json:"contractAddress"`
  57. Call string `json:"call"`
  58. }
  59. SolanaAccount struct {
  60. Chain int `json:"chain"`
  61. Account string `json:"account"`
  62. }
  63. SolanaPda struct {
  64. Chain int `json:"chain"`
  65. ProgramAddress string `json:"programAddress"`
  66. // As a future enhancement, we may want to specify the allowed seeds.
  67. }
  68. PermissionsMap map[string]*permissionEntry
  69. permissionEntry struct {
  70. userName string
  71. apiKey string
  72. rateLimiter *rate.Limiter
  73. allowUnsigned bool
  74. allowAnything bool
  75. logResponses bool
  76. allowedCalls allowedCallsForUser // Key is something like "ethCall:2:000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6:06fdde03"
  77. }
  78. allowedCallsForUser map[string]struct{}
  79. Permissions struct {
  80. lock sync.Mutex
  81. env common.Environment
  82. permMap PermissionsMap
  83. fileName string
  84. watcher *fswatch.Watcher
  85. }
  86. )
  87. // NewPermissions creates a Permissions object which contains the per-user permissions.
  88. func NewPermissions(fileName string, env common.Environment) (*Permissions, error) {
  89. permMap, err := parseConfigFile(fileName, env)
  90. if err != nil {
  91. return nil, err
  92. }
  93. return &Permissions{
  94. permMap: permMap,
  95. fileName: fileName,
  96. }, nil
  97. }
  98. // StartWatcher creates an fswatcher to watch for updates to the permissions file and reload it when it changes.
  99. func (perms *Permissions) StartWatcher(ctx context.Context, logger *zap.Logger, errC chan error) {
  100. logger = logger.With(zap.String("component", "perms"))
  101. perms.watcher = fswatch.NewWatcher(perms.fileName)
  102. fsChan := perms.watcher.Start()
  103. common.RunWithScissors(ctx, errC, "perm_file_watcher", func(ctx context.Context) error {
  104. for {
  105. select {
  106. case <-ctx.Done():
  107. return nil
  108. case notif := <-fsChan:
  109. if notif.Path != perms.fileName {
  110. return fmt.Errorf("permissions watcher received an update for an unexpected file: %s", notif.Path)
  111. }
  112. logger.Info("the permissions file has been updated", zap.String("fileName", notif.Path), zap.Int("event", int(notif.Event)))
  113. perms.Reload(logger)
  114. }
  115. }
  116. })
  117. }
  118. // Reload reloads the permissions file.
  119. func (perms *Permissions) Reload(logger *zap.Logger) {
  120. permMap, err := parseConfigFile(perms.fileName, perms.env)
  121. if err != nil {
  122. logger.Error("failed to reload the permissions file, sticking with the old one", zap.String("fileName", perms.fileName), zap.Error(err))
  123. permissionFileReloadsFailure.Inc()
  124. return
  125. }
  126. logger.Info("successfully reloaded the permissions file, switching to it", zap.String("fileName", perms.fileName))
  127. perms.lock.Lock()
  128. perms.permMap = permMap
  129. perms.lock.Unlock()
  130. permissionFileReloadsSuccess.Inc()
  131. }
  132. // StopWatcher stops the permissions file watcher.
  133. func (perms *Permissions) StopWatcher() {
  134. if perms.watcher != nil {
  135. perms.watcher.Stop()
  136. }
  137. }
  138. // GetUserEntry returns the permissions entry for a given API key. It uses the lock to protect against updates.
  139. func (perms *Permissions) GetUserEntry(apiKey string) (*permissionEntry, bool) {
  140. perms.lock.Lock()
  141. defer perms.lock.Unlock()
  142. userEntry, exists := perms.permMap[apiKey]
  143. return userEntry, exists
  144. }
  145. const ETH_CALL_SIG_LENGTH = 4
  146. // parseConfigFile parses the permissions config file into a map keyed by API key.
  147. func parseConfigFile(fileName string, env common.Environment) (PermissionsMap, error) {
  148. jsonFile, err := os.Open(fileName)
  149. if err != nil {
  150. return nil, fmt.Errorf(`failed to open permissions file "%s": %w`, fileName, err)
  151. }
  152. defer jsonFile.Close()
  153. byteValue, err := io.ReadAll(jsonFile)
  154. if err != nil {
  155. return nil, fmt.Errorf(`failed to read permissions file "%s": %w`, fileName, err)
  156. }
  157. retVal, err := parseConfig(byteValue, env)
  158. if err != nil {
  159. return nil, fmt.Errorf(`failed to parse permissions file "%s": %w`, fileName, err)
  160. }
  161. return retVal, err
  162. }
  163. // parseConfig parses the permissions config from a buffer into a map keyed by API key.
  164. func parseConfig(byteValue []byte, env common.Environment) (PermissionsMap, error) {
  165. config := Config{DefaultBurstSize: 1}
  166. if err := json.Unmarshal(byteValue, &config); err != nil {
  167. return nil, fmt.Errorf(`failed to unmarshal json: %w`, err)
  168. }
  169. // According to the docs, a burst size of zero does not allow any events. We don't want that!
  170. if config.DefaultBurstSize == 0 {
  171. return nil, errors.New("the default burst size may not be zero")
  172. }
  173. if config.AllowAnythingSupported && env == common.MainNet {
  174. return nil, fmt.Errorf(`the "allowAnythingSupported" flag is not supported in mainnet`)
  175. }
  176. ret := make(PermissionsMap)
  177. userNames := map[string]struct{}{}
  178. for _, user := range config.Permissions {
  179. // Since we log user names in all our error messages, make sure they are unique.
  180. if _, exists := userNames[user.UserName]; exists {
  181. return nil, fmt.Errorf(`UserName "%s" is a duplicate`, user.UserName)
  182. }
  183. userNames[user.UserName] = struct{}{}
  184. apiKey := strings.ToLower(user.ApiKey)
  185. if _, exists := ret[apiKey]; exists {
  186. return nil, fmt.Errorf(`API key "%s" is a duplicate`, apiKey)
  187. }
  188. if user.AllowAnything {
  189. if !config.AllowAnythingSupported {
  190. return nil, fmt.Errorf(`UserName "%s" has "allowAnything" specified when the feature is not enabled`, user.UserName)
  191. }
  192. if len(user.AllowedCalls) != 0 {
  193. return nil, fmt.Errorf(`UserName "%s" has "allowedCalls" specified with "allowAnything", which is not allowed`, user.UserName)
  194. }
  195. }
  196. var rateLimiter *rate.Limiter
  197. rateLimit := config.DefaultRateLimit
  198. if user.RateLimit != nil {
  199. rateLimit = *user.RateLimit
  200. }
  201. if rateLimit != 0 {
  202. burstSize := config.DefaultBurstSize
  203. if user.BurstSize != nil {
  204. burstSize = *user.BurstSize
  205. }
  206. if burstSize == 0 {
  207. return nil, errors.New("if rate limiting is enabled, the burst size may not be zero")
  208. }
  209. rateLimiter = rate.NewLimiter(rate.Limit(rateLimit), burstSize)
  210. }
  211. // Build the list of allowed calls for this API key.
  212. allowedCalls := make(allowedCallsForUser)
  213. for _, ac := range user.AllowedCalls {
  214. var chain int
  215. var callType, contractAddressStr, callStr, callKey string
  216. // var contractAddressStr string
  217. if ac.EthCall != nil {
  218. callType = "ethCall"
  219. chain = ac.EthCall.Chain
  220. contractAddressStr = ac.EthCall.ContractAddress
  221. callStr = ac.EthCall.Call
  222. } else if ac.EthCallByTimestamp != nil {
  223. callType = "ethCallByTimestamp"
  224. chain = ac.EthCallByTimestamp.Chain
  225. contractAddressStr = ac.EthCallByTimestamp.ContractAddress
  226. callStr = ac.EthCallByTimestamp.Call
  227. } else if ac.EthCallWithFinality != nil {
  228. callType = "ethCallWithFinality"
  229. chain = ac.EthCallWithFinality.Chain
  230. contractAddressStr = ac.EthCallWithFinality.ContractAddress
  231. callStr = ac.EthCallWithFinality.Call
  232. } else if ac.SolanaAccount != nil {
  233. // We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex.
  234. account := ac.SolanaAccount.Account
  235. if strings.HasPrefix(account, "0x") {
  236. buf, err := hex.DecodeString(account[2:])
  237. if err != nil {
  238. return nil, fmt.Errorf(`invalid solana account hex string "%s" for user "%s": %w`, account, user.UserName, err)
  239. }
  240. if len(buf) != query.SolanaPublicKeyLength {
  241. return nil, fmt.Errorf(`invalid solana account hex string "%s" for user "%s, must be %d bytes`, account, user.UserName, query.SolanaPublicKeyLength)
  242. }
  243. account = solana.PublicKey(buf).String()
  244. } else {
  245. // Make sure it is valid base58.
  246. _, err := solana.PublicKeyFromBase58(account)
  247. if err != nil {
  248. return nil, fmt.Errorf(`solana account string "%s" for user "%s" is not valid base58: %w`, account, user.UserName, err)
  249. }
  250. }
  251. callKey = fmt.Sprintf("solAccount:%d:%s", ac.SolanaAccount.Chain, account)
  252. } else if ac.SolanaPda != nil {
  253. // We assume the account is base58, but if it starts with "0x" it should be 32 bytes of hex.
  254. pa := ac.SolanaPda.ProgramAddress
  255. if strings.HasPrefix(pa, "0x") {
  256. buf, err := hex.DecodeString(pa[2:])
  257. if err != nil {
  258. return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s": %w`, pa, user.UserName, err)
  259. }
  260. if len(buf) != query.SolanaPublicKeyLength {
  261. return nil, fmt.Errorf(`invalid solana program address hex string "%s" for user "%s, must be %d bytes`, pa, user.UserName, query.SolanaPublicKeyLength)
  262. }
  263. pa = solana.PublicKey(buf).String()
  264. } else {
  265. // Make sure it is valid base58.
  266. _, err := solana.PublicKeyFromBase58(pa)
  267. if err != nil {
  268. return nil, fmt.Errorf(`solana program address string "%s" for user "%s" is not valid base58: %w`, pa, user.UserName, err)
  269. }
  270. }
  271. callKey = fmt.Sprintf("solPDA:%d:%s", ac.SolanaPda.Chain, pa)
  272. } else {
  273. return nil, fmt.Errorf(`unsupported call type for user "%s", must be "ethCall", "ethCallByTimestamp", "ethCallWithFinality", "solAccount" or "solPDA"`, user.UserName)
  274. }
  275. if callKey == "" {
  276. // Convert the contract address into a standard format like "000000000000000000000000b4fbf271143f4fbf7b91a5ded31805e42b2208d6".
  277. contractAddress := contractAddressStr
  278. if contractAddressStr != "*" {
  279. contractAddr, err := vaa.StringToAddress(contractAddressStr)
  280. if err != nil {
  281. return nil, fmt.Errorf(`invalid contract address "%s" for user "%s"`, contractAddressStr, user.UserName)
  282. }
  283. contractAddress = contractAddr.String()
  284. }
  285. // The call should be the ABI four byte hex hash of the function signature. Parse it into a standard form of "06fdde03".
  286. call, err := hex.DecodeString(strings.TrimPrefix(callStr, "0x"))
  287. if err != nil {
  288. return nil, fmt.Errorf(`invalid eth call "%s" for user "%s"`, callStr, user.UserName)
  289. }
  290. if len(call) != ETH_CALL_SIG_LENGTH {
  291. return nil, fmt.Errorf(`eth call "%s" for user "%s" has an invalid length, must be %d bytes`, callStr, user.UserName, ETH_CALL_SIG_LENGTH)
  292. }
  293. // The permission key is the chain, contract address and call formatted as a colon separated string.
  294. callKey = fmt.Sprintf("%s:%d:%s:%s", callType, chain, contractAddress, hex.EncodeToString(call))
  295. }
  296. if _, exists := allowedCalls[callKey]; exists {
  297. return nil, fmt.Errorf(`"%s" is a duplicate allowed call for user "%s"`, callKey, user.UserName)
  298. }
  299. allowedCalls[callKey] = struct{}{}
  300. }
  301. pe := &permissionEntry{
  302. userName: user.UserName,
  303. apiKey: apiKey,
  304. rateLimiter: rateLimiter,
  305. allowUnsigned: user.AllowUnsigned,
  306. allowAnything: user.AllowAnything,
  307. logResponses: user.LogResponses,
  308. allowedCalls: allowedCalls,
  309. }
  310. ret[apiKey] = pe
  311. }
  312. return ret, nil
  313. }