feat_: connector revoke permissions and accounts (#5646)
* feat(connector)_: add `wallet_revokePermissions` endpoint * feat_: same behavior for `eth_accounts` and `eth_requestAccounts`
This commit is contained in:
parent
20d6d4eb9a
commit
d5a78e784a
|
@ -29,11 +29,13 @@ func NewAPI(s *Service) *API {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Accounts query and dapp permissions
|
// Accounts query and dapp permissions
|
||||||
r.Register("eth_accounts", &commands.AccountsCommand{Db: s.db})
|
// NOTE: Some dApps expect same behavior for both eth_accounts and eth_requestAccounts
|
||||||
r.Register("eth_requestAccounts", &commands.RequestAccountsCommand{
|
accountsCommand := &commands.RequestAccountsCommand{
|
||||||
ClientHandler: c,
|
ClientHandler: c,
|
||||||
AccountsCommand: commands.AccountsCommand{Db: s.db},
|
AccountsCommand: commands.AccountsCommand{Db: s.db},
|
||||||
})
|
}
|
||||||
|
r.Register("eth_accounts", accountsCommand)
|
||||||
|
r.Register("eth_requestAccounts", accountsCommand)
|
||||||
|
|
||||||
// Active chain per dapp management
|
// Active chain per dapp management
|
||||||
r.Register("eth_chainId", &commands.ChainIDCommand{
|
r.Register("eth_chainId", &commands.ChainIDCommand{
|
||||||
|
@ -45,8 +47,11 @@ func NewAPI(s *Service) *API {
|
||||||
NetworkManager: s.nm,
|
NetworkManager: s.nm,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Request permissions
|
// Permissions
|
||||||
r.Register("wallet_requestPermissions", &commands.RequestPermissionsCommand{})
|
r.Register("wallet_requestPermissions", &commands.RequestPermissionsCommand{})
|
||||||
|
r.Register("wallet_revokePermissions", &commands.RevokePermissionsCommand{
|
||||||
|
Db: s.db,
|
||||||
|
})
|
||||||
|
|
||||||
return &API{
|
return &API{
|
||||||
s: s,
|
s: s,
|
||||||
|
@ -102,6 +107,10 @@ func (api *API) RecallDAppPermission(origin string) error {
|
||||||
return persistence.DeleteDApp(api.s.db, origin)
|
return persistence.DeleteDApp(api.s.db, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) GetPermittedDAppsList() ([]persistence.DApp, error) {
|
||||||
|
return persistence.SelectAllDApps(api.s.db)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) RequestAccountsAccepted(args commands.RequestAccountsAcceptedArgs) error {
|
func (api *API) RequestAccountsAccepted(args commands.RequestAccountsAcceptedArgs) error {
|
||||||
return api.c.RequestAccountsAccepted(args)
|
return api.c.RequestAccountsAccepted(args)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,12 @@ func (c *RequestAccountsCommand) Execute(request RPCRequest) (interface{}, error
|
||||||
|
|
||||||
// FIXME: this may have a security issue in case some malicious software tries to fake the origin
|
// FIXME: this may have a security issue in case some malicious software tries to fake the origin
|
||||||
if dApp == nil {
|
if dApp == nil {
|
||||||
account, chainID, err := c.ClientHandler.RequestShareAccountForDApp(signal.ConnectorDApp{
|
connectorDApp := signal.ConnectorDApp{
|
||||||
URL: request.URL,
|
URL: request.URL,
|
||||||
Name: request.Name,
|
Name: request.Name,
|
||||||
IconURL: request.IconURL,
|
IconURL: request.IconURL,
|
||||||
})
|
}
|
||||||
|
account, chainID, err := c.ClientHandler.RequestShareAccountForDApp(connectorDApp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -59,6 +60,7 @@ func (c *RequestAccountsCommand) Execute(request RPCRequest) (interface{}, error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
signal.SendConnectorDAppPermissionGranted(connectorDApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormatAccountAddressToResponse(dApp.SharedAccount), nil
|
return FormatAccountAddressToResponse(dApp.SharedAccount), nil
|
||||||
|
|
|
@ -65,6 +65,7 @@ func TestRequestAccountsAcceptedAndRequestAgain(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
accountAddress := types.Address{0x03}
|
accountAddress := types.Address{0x03}
|
||||||
|
dAppPermissionGranted := false
|
||||||
|
|
||||||
signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) {
|
signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) {
|
||||||
var evt EventType
|
var evt EventType
|
||||||
|
@ -83,6 +84,8 @@ func TestRequestAccountsAcceptedAndRequestAgain(t *testing.T) {
|
||||||
ChainID: walletCommon.EthereumMainnet,
|
ChainID: walletCommon.EthereumMainnet,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
case signal.EventConnectorDAppPermissionGranted:
|
||||||
|
dAppPermissionGranted = true
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -104,6 +107,8 @@ func TestRequestAccountsAcceptedAndRequestAgain(t *testing.T) {
|
||||||
response, err = cmd.Execute(request)
|
response, err = cmd.Execute(request)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedResponse, response)
|
assert.Equal(t, expectedResponse, response)
|
||||||
|
|
||||||
|
assert.True(t, dAppPermissionGranted)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestAccountsRejected(t *testing.T) {
|
func TestRequestAccountsRejected(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
persistence "github.com/status-im/status-go/services/connector/database"
|
||||||
|
"github.com/status-im/status-go/signal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RevokePermissionsCommand struct {
|
||||||
|
Db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RevokePermissionsCommand) Execute(request RPCRequest) (interface{}, error) {
|
||||||
|
err := request.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
dApp, err := persistence.SelectDAppByUrl(c.Db, request.URL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dApp == nil {
|
||||||
|
return "", ErrDAppIsNotPermittedByUser
|
||||||
|
}
|
||||||
|
|
||||||
|
err = persistence.DeleteDApp(c.Db, dApp.URL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signal.SendConnectorDAppPermissionRevoked(signal.ConnectorDApp{
|
||||||
|
URL: request.URL,
|
||||||
|
Name: request.Name,
|
||||||
|
IconURL: request.IconURL,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/status-im/status-go/eth-node/types"
|
||||||
|
persistence "github.com/status-im/status-go/services/connector/database"
|
||||||
|
"github.com/status-im/status-go/signal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFailToRevokePermissionsWithMissingDAppFields(t *testing.T) {
|
||||||
|
cmd := &RequestPermissionsCommand{}
|
||||||
|
|
||||||
|
// Missing DApp fields
|
||||||
|
request, err := ConstructRPCRequest("wallet_revokePermissions", []interface{}{}, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := cmd.Execute(request)
|
||||||
|
assert.Equal(t, ErrRequestMissingDAppData, err)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFailToRevokePermissionsForUnpermittedDApp(t *testing.T) {
|
||||||
|
db, close := SetupTestDB(t)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
cmd := &RevokePermissionsCommand{Db: db}
|
||||||
|
|
||||||
|
request, err := ConstructRPCRequest("wallet_revokePermissions", []interface{}{}, &testDAppData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := cmd.Execute(request)
|
||||||
|
assert.Equal(t, ErrDAppIsNotPermittedByUser, err)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRevokePermissionsSucceeded(t *testing.T) {
|
||||||
|
db, close := SetupTestDB(t)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
cmd := &RevokePermissionsCommand{Db: db}
|
||||||
|
|
||||||
|
sharedAccount := types.BytesToAddress(types.FromHex("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg"))
|
||||||
|
dAppPermissionRevoked := false
|
||||||
|
|
||||||
|
signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) {
|
||||||
|
var evt EventType
|
||||||
|
err := json.Unmarshal(s, &evt)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
switch evt.Type {
|
||||||
|
case signal.EventConnectorDAppPermissionRevoked:
|
||||||
|
dAppPermissionRevoked = true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
err := PersistDAppData(db, testDAppData, sharedAccount, 0x123)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
request, err := ConstructRPCRequest("wallet_revokePermissions", []interface{}{}, &testDAppData)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := cmd.Execute(request)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, result)
|
||||||
|
|
||||||
|
dApp, err := persistence.SelectDAppByUrl(db, testDAppData.URL)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, dApp)
|
||||||
|
|
||||||
|
assert.True(t, dAppPermissionRevoked)
|
||||||
|
}
|
|
@ -36,22 +36,22 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) {
|
||||||
|
|
||||||
api := NewAPI(service)
|
api := NewAPI(service)
|
||||||
|
|
||||||
// Try to request accounts without permission
|
|
||||||
request := "{\"method\":\"eth_accounts\",\"params\":[],\"url\":\"http://testDAppURL123\",\"name\":\"testDAppName\",\"iconUrl\":\"http://testDAppIconUrl\"}"
|
|
||||||
response, err := api.CallRPC(request)
|
|
||||||
assert.Empty(t, response)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Equal(t, commands.ErrDAppIsNotPermittedByUser, err)
|
|
||||||
|
|
||||||
accountAddress := types.BytesToAddress(types.FromHex("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg"))
|
accountAddress := types.BytesToAddress(types.FromHex("0x6d0aa2a774b74bb1d36f97700315adf962c69fcg"))
|
||||||
expectedHash := types.BytesToHash(types.FromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"))
|
expectedHash := types.BytesToHash(types.FromHex("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"))
|
||||||
|
|
||||||
|
dAppPermissionRevoked := false
|
||||||
|
dAppPermissionGranted := false
|
||||||
|
|
||||||
signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) {
|
signal.SetMobileSignalHandler(signal.MobileSignalHandler(func(s []byte) {
|
||||||
var evt commands.EventType
|
var evt commands.EventType
|
||||||
err := json.Unmarshal(s, &evt)
|
err := json.Unmarshal(s, &evt)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
switch evt.Type {
|
switch evt.Type {
|
||||||
|
case signal.EventConnectorDAppPermissionRevoked:
|
||||||
|
dAppPermissionRevoked = true
|
||||||
|
case signal.EventConnectorDAppPermissionGranted:
|
||||||
|
dAppPermissionGranted = true
|
||||||
case signal.EventConnectorSendRequestAccounts:
|
case signal.EventConnectorSendRequestAccounts:
|
||||||
var ev signal.ConnectorSendRequestAccountsSignal
|
var ev signal.ConnectorSendRequestAccountsSignal
|
||||||
err := json.Unmarshal(evt.Event, &ev)
|
err := json.Unmarshal(evt.Event, &ev)
|
||||||
|
@ -77,10 +77,12 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Request accounts, now for real
|
// Request accounts, now for real
|
||||||
request = "{\"method\": \"eth_requestAccounts\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }"
|
request := "{\"method\": \"eth_requestAccounts\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }"
|
||||||
response, err = api.CallRPC(request)
|
response, err := api.CallRPC(request)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, commands.FormatAccountAddressToResponse(accountAddress), response)
|
assert.Equal(t, commands.FormatAccountAddressToResponse(accountAddress), response)
|
||||||
|
assert.Equal(t, true, dAppPermissionGranted)
|
||||||
|
assert.Equal(t, false, dAppPermissionRevoked)
|
||||||
|
|
||||||
// Request to switch ethereum chain
|
// Request to switch ethereum chain
|
||||||
expectedChainID, err := chainutils.GetHexChainID(walletCommon.ChainID(walletCommon.EthereumMainnet).String())
|
expectedChainID, err := chainutils.GetHexChainID(walletCommon.ChainID(walletCommon.EthereumMainnet).String())
|
||||||
|
@ -107,6 +109,19 @@ func TestRequestAccountsSwitchChainAndSendTransactionFlow(t *testing.T) {
|
||||||
response, err = api.CallRPC(request)
|
response, err = api.CallRPC(request)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedHash.Hex(), response)
|
assert.Equal(t, expectedHash.Hex(), response)
|
||||||
|
|
||||||
|
// Revoke permissions
|
||||||
|
request = "{\"method\": \"wallet_revokePermissions\", \"params\": [], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }"
|
||||||
|
_, err = api.CallRPC(request)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Check if the account was revoked
|
||||||
|
request = fmt.Sprintf("{\"method\": \"eth_sendTransaction\", \"params\":[{\"from\":\"%s\",\"to\":\"0x0200000000000000000000000000000000000000\",\"value\":\"0x12345\",\"data\":\"0x307830\"}], \"url\": \"http://testDAppURL123\", \"name\": \"testDAppName\", \"iconUrl\": \"http://testDAppIconUrl\" }", accountAddress.Hex())
|
||||||
|
response, err = api.CallRPC(request)
|
||||||
|
assert.Empty(t, response)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, commands.ErrDAppIsNotPermittedByUser, err)
|
||||||
|
assert.Equal(t, true, dAppPermissionRevoked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForwardedRPCs(t *testing.T) {
|
func TestForwardedRPCs(t *testing.T) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
const upsertDAppQuery = "INSERT INTO connector_dapps (url, name, icon_url, shared_account, chain_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(url) DO UPDATE SET name = excluded.name, icon_url = excluded.icon_url, shared_account = excluded.shared_account, chain_id = excluded.chain_id"
|
const upsertDAppQuery = "INSERT INTO connector_dapps (url, name, icon_url, shared_account, chain_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(url) DO UPDATE SET name = excluded.name, icon_url = excluded.icon_url, shared_account = excluded.shared_account, chain_id = excluded.chain_id"
|
||||||
const selectDAppByUrlQuery = "SELECT name, icon_url, shared_account, chain_id FROM connector_dapps WHERE url = ?"
|
const selectDAppByUrlQuery = "SELECT name, icon_url, shared_account, chain_id FROM connector_dapps WHERE url = ?"
|
||||||
|
const selectDAppsQuery = "SELECT url, name, icon_url, shared_account, chain_id FROM connector_dapps"
|
||||||
const deleteDAppQuery = "DELETE FROM connector_dapps WHERE url = ?"
|
const deleteDAppQuery = "DELETE FROM connector_dapps WHERE url = ?"
|
||||||
|
|
||||||
type DApp struct {
|
type DApp struct {
|
||||||
|
@ -34,6 +35,25 @@ func SelectDAppByUrl(db *sql.DB, url string) (*DApp, error) {
|
||||||
return dApp, err
|
return dApp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SelectAllDApps(db *sql.DB) ([]DApp, error) {
|
||||||
|
rows, err := db.Query(selectDAppsQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var dApps []DApp
|
||||||
|
for rows.Next() {
|
||||||
|
dApp := DApp{}
|
||||||
|
err = rows.Scan(&dApp.URL, &dApp.Name, &dApp.IconURL, &dApp.SharedAccount, &dApp.ChainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dApps = append(dApps, dApp)
|
||||||
|
}
|
||||||
|
return dApps, nil
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteDApp(db *sql.DB, url string) error {
|
func DeleteDApp(db *sql.DB, url string) error {
|
||||||
_, err := db.Exec(deleteDAppQuery, url)
|
_, err := db.Exec(deleteDAppQuery, url)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -80,3 +80,16 @@ func TestInsertAndRemoveDApp(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, dAppBack)
|
require.Empty(t, dAppBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectAllDApps(t *testing.T) {
|
||||||
|
db, close := setupTestDB(t)
|
||||||
|
defer close()
|
||||||
|
|
||||||
|
err := UpsertDApp(db, &testDApp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dApps, err := SelectAllDApps(db)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, dApps, 1)
|
||||||
|
require.Equal(t, testDApp, dApps[0])
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package signal
|
||||||
const (
|
const (
|
||||||
EventConnectorSendRequestAccounts = "connector.sendRequestAccounts"
|
EventConnectorSendRequestAccounts = "connector.sendRequestAccounts"
|
||||||
EventConnectorSendTransaction = "connector.sendTransaction"
|
EventConnectorSendTransaction = "connector.sendTransaction"
|
||||||
|
EventConnectorDAppPermissionGranted = "connector.dAppPermissionGranted"
|
||||||
|
EventConnectorDAppPermissionRevoked = "connector.dAppPermissionRevoked"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConnectorDApp struct {
|
type ConnectorDApp struct {
|
||||||
|
@ -40,3 +42,11 @@ func SendConnectorSendTransaction(dApp ConnectorDApp, chainID uint64, txArgs str
|
||||||
TxArgs: txArgs,
|
TxArgs: txArgs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SendConnectorDAppPermissionGranted(dApp ConnectorDApp) {
|
||||||
|
send(EventConnectorDAppPermissionGranted, dApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendConnectorDAppPermissionRevoked(dApp ConnectorDApp) {
|
||||||
|
send(EventConnectorDAppPermissionRevoked, dApp)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue