Permissions api (#1524)

* Add permissions api

* Integrate permissions service

* Reduce cyclomatic complexity of the MakeNode function
This commit is contained in:
Dmitry Shulyak 2019-07-21 08:41:30 +03:00 committed by GitHub
parent 9a0502fa8f
commit 9723b64827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 606 additions and 11 deletions

View File

@ -571,7 +571,11 @@ func (b *StatusBackend) SelectAccount(walletAddress, chatAddress, password strin
if err != nil {
return err
}
return b.startBrowsers(password)
err = b.startBrowsers(password)
if err != nil {
return err
}
return b.startPermissions(password)
}
func (b *StatusBackend) startWallet(password string) error {
@ -609,6 +613,22 @@ func (b *StatusBackend) startBrowsers(password string) error {
return svc.StartDatabase(path, password)
}
func (b *StatusBackend) startPermissions(password string) error {
if !b.statusNode.Config().PermissionsConfig.Enabled {
return nil
}
svc, err := b.statusNode.PermissionsService()
if err != nil {
return err
}
account, err := b.accountManager.SelectedWalletAccount()
if err != nil {
return err
}
path := path.Join(b.statusNode.Config().DataDir, fmt.Sprintf("permissions-%x.sql", account.Address))
return svc.StartDatabase(path, password)
}
// SendDataNotification sends data push notifications to users.
// dataPayloadJSON is a JSON string that looks like this:
// {

View File

@ -27,6 +27,7 @@ import (
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/incentivisation"
"github.com/status-im/status-go/services/peer"
"github.com/status-im/status-go/services/permissions"
"github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/shhext"
"github.com/status-im/status-go/services/status"
@ -49,6 +50,7 @@ var (
ErrIncentivisationServiceRegistrationFailure = errors.New("failed to register the Incentivisation service")
ErrWalletServiceRegistrationFailure = errors.New("failed to register the Wallet service")
ErrBrowsersServiceRegistrationFailure = errors.New("failed to register the Browsers service")
ErrPermissionsServiceRegistrationFailure = errors.New("failed to register the Permissions service")
)
// All general log messages in this package should be routed through this logger.
@ -80,14 +82,22 @@ func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
return nil, fmt.Errorf(ErrNodeMakeFailureFormat, err.Error())
}
err = activateServices(stack, config, db)
if err != nil {
return nil, err
}
return stack, nil
}
func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
// start Ethereum service if we are not expected to use an upstream server
if !config.UpstreamConfig.Enabled {
if err := activateLightEthService(stack, config); err != nil {
return nil, fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrLightEthRegistrationFailure, err)
}
} else {
if config.LightEthConfig.Enabled {
return nil, fmt.Errorf("%v: %v", ErrLightEthRegistrationFailureUpstreamEnabled, err)
return ErrLightEthRegistrationFailureUpstreamEnabled
}
logger.Info("LES protocol is disabled")
@ -98,39 +108,43 @@ func MakeNode(config *params.NodeConfig, db *leveldb.DB) (*node.Node, error) {
// upstream, we don't start any of these, so we need to start our own
// implementation.
if err := activatePersonalService(stack, config); err != nil {
return nil, fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrPersonalServiceRegistrationFailure, err)
}
}
// start Whisper service.
if err := activateShhService(stack, config, db); err != nil {
return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
}
// start incentivisation service
if err := activateIncentivisationService(stack, config); err != nil {
return nil, fmt.Errorf("%v: %v", ErrIncentivisationServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrIncentivisationServiceRegistrationFailure, err)
}
// start status service.
if err := activateStatusService(stack, config); err != nil {
return nil, fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err)
}
// start peer service
if err := activatePeerService(stack); err != nil {
return nil, fmt.Errorf("%v: %v", ErrPeerServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrPeerServiceRegistrationFailure, err)
}
if err := activateWalletService(stack, config.WalletConfig); err != nil {
return nil, fmt.Errorf("%v: %v", ErrWalletServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrWalletServiceRegistrationFailure, err)
}
if err := activateBrowsersService(stack, config.BrowsersConfig); err != nil {
return nil, fmt.Errorf("%v: %v", ErrBrowsersServiceRegistrationFailure, err)
return fmt.Errorf("%v: %v", ErrBrowsersServiceRegistrationFailure, err)
}
return stack, nil
if err := activatePermissionsService(stack, config.PermissionsConfig); err != nil {
return fmt.Errorf("%v: %v", ErrPermissionsServiceRegistrationFailure, err)
}
return nil
}
// newGethNodeConfig returns default stack configuration for mobile client node
@ -301,6 +315,16 @@ func activateBrowsersService(stack *node.Node, config params.BrowsersConfig) err
})
}
func activatePermissionsService(stack *node.Node, config params.PermissionsConfig) error {
if !config.Enabled {
logger.Info("dapps permissions service is disabled")
return nil
}
return stack.Register(func(*node.ServiceContext) (node.Service, error) {
return permissions.NewService(), nil
})
}
func registerMailServer(whisperService *whisper.Whisper, config *params.WhisperConfig) (err error) {
var mailServer mailserver.WMailServer
whisperService.RegisterServer(&mailServer)

View File

@ -31,6 +31,7 @@ import (
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/browsers"
"github.com/status-im/status-go/services/peer"
"github.com/status-im/status-go/services/permissions"
"github.com/status-im/status-go/services/shhext"
"github.com/status-im/status-go/services/status"
"github.com/status-im/status-go/services/wallet"
@ -614,6 +615,17 @@ func (n *StatusNode) BrowsersService() (s *browsers.Service, err error) {
return
}
// PermissionsService returns browsers.Service instance if it was started.
func (n *StatusNode) PermissionsService() (s *permissions.Service, err error) {
n.mu.RLock()
defer n.mu.RUnlock()
err = n.gethService(&s)
if err == node.ErrServiceUnknown {
err = ErrServiceUnknown
}
return
}
// AccountManager exposes reference to node's accounts manager
func (n *StatusNode) AccountManager() (*accounts.Manager, error) {
n.mu.RLock()

View File

@ -349,6 +349,9 @@ type NodeConfig struct {
// BrowsersConfig extra configuration for browsers.Service.
BrowsersConfig BrowsersConfig
// PermissionsConfig extra configuration for permissions.Service.
PermissionsConfig PermissionsConfig
// SwarmConfig extra configuration for Swarm and ENS
SwarmConfig SwarmConfig `json:"SwarmConfig," validate:"structonly"`
@ -374,6 +377,11 @@ type BrowsersConfig struct {
Enabled bool
}
// PermissionsConfig extra configuration for permissions.Service.
type PermissionsConfig struct {
Enabled bool
}
// ShhextConfig defines options used by shhext service.
type ShhextConfig struct {
PFSEnabled bool

View File

@ -0,0 +1,38 @@
Dapps permissions service
=========================
To enable:
```json
{
"PermissionsConfig": {
"Enabled": true,
},
APIModules: "permissions"
}
```
API
---
#### permissions_addDappPermissions
Stores provided permissions for dapp. On update replaces previous version of the object.
```json
{
"dapp": "first",
"permissions": [
"r",
"x"
]
}
```
#### permissions_getDappPermissions
Returns all permissions for dapps. Order is not deterministic.
#### permissions_deleteDappPermissions
Delete dapp by a name.

View File

@ -0,0 +1,41 @@
package permissions
import (
"context"
"errors"
)
var (
// ErrServiceNotInitialized returned when permissions is not initialized/started,.
ErrServiceNotInitialized = errors.New("permissions service is not initialized")
)
func NewAPI(s *Service) *API {
return &API{s}
}
// API is class with methods available over RPC.
type API struct {
s *Service
}
func (api *API) AddDappPermissions(ctx context.Context, perms DappPermissions) error {
if api.s.db == nil {
return ErrServiceNotInitialized
}
return api.s.db.AddPermissions(perms)
}
func (api *API) GetDappPermissions(ctx context.Context) ([]DappPermissions, error) {
if api.s.db == nil {
return nil, ErrServiceNotInitialized
}
return api.s.db.GetPermissions()
}
func (api *API) DeleteDappPermissions(ctx context.Context, name string) error {
if api.s.db == nil {
return ErrServiceNotInitialized
}
return api.s.db.DeletePermission(name)
}

View File

@ -0,0 +1,96 @@
package permissions
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"testing"
"github.com/stretchr/testify/require"
)
func setupTestDB(t *testing.T) (*Database, func()) {
tmpfile, err := ioutil.TempFile("", "perm-tests-")
require.NoError(t, err)
db, err := InitializeDB(tmpfile.Name(), "perm-tests")
require.NoError(t, err)
return db, func() {
require.NoError(t, db.Close())
require.NoError(t, os.Remove(tmpfile.Name()))
}
}
func setupTestAPI(t *testing.T) (*API, func()) {
db, cancel := setupTestDB(t)
return &API{s: &Service{db: db}}, cancel
}
func TestDappPermissionsStored(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
expected := []DappPermissions{
{
Name: "first",
Permissions: []string{"r", "w"},
},
{
Name: "second",
Permissions: []string{"r", "x"},
},
{
Name: "third",
},
}
for _, perms := range expected {
require.NoError(t, api.AddDappPermissions(context.TODO(), perms))
}
rst, err := api.GetDappPermissions(context.TODO())
require.NoError(t, err)
// sort in lexicographic order by name
sort.Slice(rst, func(i, j int) bool {
return rst[i].Name < rst[j].Name
})
require.Equal(t, expected, rst)
data, err := json.Marshal(rst)
require.NoError(t, err)
fmt.Println(string(data))
}
func TestDappPermissionsReplacedOnUpdated(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
perms := DappPermissions{
Name: "first",
Permissions: []string{"r", "w"},
}
require.NoError(t, api.AddDappPermissions(context.TODO(), perms))
perms.Permissions = append(perms.Permissions, "x")
require.NoError(t, api.AddDappPermissions(context.TODO(), perms))
rst, err := api.GetDappPermissions(context.TODO())
require.NoError(t, err)
require.Len(t, rst, 1)
require.Equal(t, perms, rst[0])
}
func TestDappPermissionsDeleted(t *testing.T) {
api, cancel := setupTestAPI(t)
defer cancel()
perms := DappPermissions{
Name: "first",
}
require.NoError(t, api.AddDappPermissions(context.TODO(), perms))
rst, err := api.GetDappPermissions(context.TODO())
require.NoError(t, err)
require.Len(t, rst, 1)
require.NoError(t, api.DeleteDappPermissions(context.TODO(), perms.Name))
rst, err = api.GetDappPermissions(context.TODO())
require.NoError(t, err)
require.Len(t, rst, 0)
}

View File

@ -0,0 +1,135 @@
package permissions
import (
"database/sql"
"github.com/status-im/status-go/services/permissions/migrations"
"github.com/status-im/status-go/sqlite"
)
// Database sql wrapper for operations with browser objects.
type Database struct {
db *sql.DB
}
// Close closes database.
func (db Database) Close() error {
return db.db.Close()
}
// InitializeDB creates db file at a given path and applies migrations.
func InitializeDB(path, password string) (*Database, error) {
db, err := sqlite.OpenDB(path, password)
if err != nil {
return nil, err
}
err = migrations.Migrate(db)
if err != nil {
return nil, err
}
return &Database{db: db}, nil
}
type DappPermissions struct {
Name string `json:"dapp"`
Permissions []string `json:"permissions,omitempty"`
}
func (db *Database) AddPermissions(perms DappPermissions) (err error) {
var (
tx *sql.Tx
insert *sql.Stmt
)
tx, err = db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
insert, err = tx.Prepare("INSERT OR REPLACE INTO dapps(name) VALUES(?)")
if err != nil {
return
}
_, err = insert.Exec(perms.Name)
insert.Close()
if err != nil {
return
}
if len(perms.Permissions) == 0 {
return
}
insert, err = tx.Prepare("INSERT INTO permissions(dapp_name, permission) VALUES(?, ?)")
if err != nil {
return
}
defer insert.Close()
for _, perm := range perms.Permissions {
_, err = insert.Exec(perms.Name, perm)
if err != nil {
return
}
}
return
}
func (db *Database) GetPermissions() (rst []DappPermissions, err error) {
var (
tx *sql.Tx
rows *sql.Rows
)
tx, err = db.db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
_ = tx.Rollback()
}()
// FULL and RIGHT joins are not supported
rows, err = tx.Query("SELECT name FROM dapps")
if err != nil {
return
}
dapps := map[string]*DappPermissions{}
for rows.Next() {
perms := DappPermissions{}
err = rows.Scan(&perms.Name)
if err != nil {
return nil, err
}
dapps[perms.Name] = &perms
}
rows, err = tx.Query("SELECT dapp_name, permission from permissions")
if err != nil {
return
}
var (
name string
permission string
)
for rows.Next() {
err = rows.Scan(&name, &permission)
if err != nil {
return
}
dapps[name].Permissions = append(dapps[name].Permissions, permission)
}
rst = make([]DappPermissions, 0, len(dapps))
for key := range dapps {
rst = append(rst, *dapps[key])
}
return rst, nil
}
func (db *Database) DeletePermission(name string) error {
_, err := db.db.Exec("DELETE FROM dapps WHERE name = ?", name)
return err
}

View File

@ -0,0 +1,127 @@
package migrations
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"strings"
)
func bindata_read(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
return buf.Bytes(), nil
}
var __0001_permissions_down_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x48\x49\x2c\x28\x28\xb6\xe6\x42\x12\x29\x48\x2d\xca\xcd\x2c\x2e\xce\xcc\xcf\x2b\xb6\xe6\x02\x04\x00\x00\xff\xff\xeb\x21\xe7\xd0\x2a\x00\x00\x00")
func _0001_permissions_down_sql() ([]byte, error) {
return bindata_read(
__0001_permissions_down_sql,
"0001_permissions.down.sql",
)
}
var __0001_permissions_up_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\xce\x31\x0f\x82\x30\x10\x05\xe0\xbd\xbf\xe2\x8d\x90\xf8\x0f\x9c\x6a\x79\x68\x63\x6d\x4d\x39\x02\x4c\x86\x44\x06\x06\x90\xc8\xff\x4f\x4c\xa3\x91\xc4\xc1\xf5\xee\xdd\x77\xcf\x44\x6a\x21\x44\x1f\x1c\x61\x4b\xf8\x20\x60\x6b\x2b\xa9\x70\xef\x97\x65\x45\xa6\xe6\x7e\x1a\x20\x6c\x05\xd7\x68\x2f\x3a\x76\x38\xb3\x53\x39\x1a\x2b\xa7\x50\x0b\x62\x68\x6c\xb1\x57\xea\x0f\xb5\x0c\xcf\x69\x5c\xd7\xf1\x31\x27\x30\xc1\xb7\x4d\x4d\x39\x5f\x3b\xb7\x53\x5b\xec\x77\x53\x86\x48\x7b\xf4\xe9\x73\xf6\x3d\xcf\x11\x59\x32\xd2\x1b\x7e\xda\x66\xef\x71\xf0\x28\xe8\x28\x84\xd1\x95\xd1\x05\x55\xfe\x0a\x00\x00\xff\xff\x9e\x9a\xc6\xf0\xe8\x00\x00\x00")
func _0001_permissions_up_sql() ([]byte, error) {
return bindata_read(
__0001_permissions_up_sql,
"0001_permissions.up.sql",
)
}
var _doc_go = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func doc_go() ([]byte, error) {
return bindata_read(
_doc_go,
"doc.go",
)
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
return f()
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){
"0001_permissions.down.sql": _0001_permissions_down_sql,
"0001_permissions.up.sql": _0001_permissions_up_sql,
"doc.go": doc_go,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() ([]byte, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"0001_permissions.down.sql": &_bintree_t{_0001_permissions_down_sql, map[string]*_bintree_t{
}},
"0001_permissions.up.sql": &_bintree_t{_0001_permissions_up_sql, map[string]*_bintree_t{
}},
"doc.go": &_bintree_t{doc_go, map[string]*_bintree_t{
}},
}}

View File

@ -0,0 +1,18 @@
package migrations
import (
"database/sql"
bindata "github.com/status-im/migrate/v4/source/go_bindata"
"github.com/status-im/status-go/sqlite"
)
// Migrate applies migrations.
func Migrate(db *sql.DB) error {
return sqlite.Migrate(db, bindata.Resource(
AssetNames(),
func(name string) ([]byte, error) {
return Asset(name)
},
))
}

View File

@ -0,0 +1,2 @@
DROP TABLE dapps;
DROP TABLE permissions;

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS dapps (
name TEXT PRIMARY KEY
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS permissions (
dapp_name TEXT NOT NULL,
permission TEXT NOT NULL,
FOREIGN KEY(dapp_name) REFERENCES dapps(name) ON DELETE CASCADE
)

View File

@ -0,0 +1,3 @@
package sql
//go:generate go-bindata -pkg migrations -o ../bindata.go ./

View File

@ -0,0 +1,62 @@
package permissions
import (
"sync"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// NewService initializes service instance.
func NewService() *Service {
return &Service{}
}
type Service struct {
mu sync.Mutex
db *Database
}
// Start a service.
func (s *Service) Start(*p2p.Server) error {
return nil
}
// StartDatabase after dbpath and password will become known.
func (s *Service) StartDatabase(dbpath, password string) (err error) {
s.mu.Lock()
defer s.mu.Unlock()
s.db, err = InitializeDB(dbpath, password)
return err
}
func (s *Service) StopDatabase() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.db != nil {
return s.db.Close()
}
return nil
}
// Stop a service.
func (s *Service) Stop() error {
return s.StopDatabase()
}
// APIs returns list of available RPC APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "permissions",
Version: "0.1.0",
Service: NewAPI(s),
Public: true,
},
}
}
// Protocols returns list of p2p protocols.
func (s *Service) Protocols() []p2p.Protocol {
return nil
}