diff --git a/CHANGELOG.md b/CHANGELOG.md index 52fac8b0..6fd7ebf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## HEAD +## v0.9.8 + +- BLOCK METRICS: Move tables to schema 'permissioned' and grant read-only access to role readonly + ## v0.9.7 - CLI: Add update-fee command diff --git a/cmd/block-metrics/docker-compose.yml b/cmd/block-metrics/docker-compose.yml index 199d9472..cc4fbef7 100644 --- a/cmd/block-metrics/docker-compose.yml +++ b/cmd/block-metrics/docker-compose.yml @@ -24,4 +24,4 @@ services: depends_on: - pgadmin4 image: alpine - command: echo -e "\n\nUse Firefox or get 'The CSRF session token is missing.' errors.\nBrowse to http://localhost:1111 to access pgadmin.\nLogin with 'admin@test.com/test'.\nConnect to server 'db' as 'postgres/root'.\n\n" + command: echo -e "\n\nBrowse to http://localhost:1111 to access pgadmin.\nHard-reload the page if you get 'The CSRF session token is missing.' errors.\nLogin with 'admin@test.com/test'.\nConnect to server 'db' as 'postgres/root'.\n\n" diff --git a/cmd/block-metrics/main.go b/cmd/block-metrics/main.go index d19987c4..a4fa4f4d 100644 --- a/cmd/block-metrics/main.go +++ b/cmd/block-metrics/main.go @@ -17,6 +17,8 @@ func main() { DBHost: os.Getenv("POSTGRES_HOST"), DBName: os.Getenv("POSTGRES_DB"), DBPass: os.Getenv("POSTGRES_PASSWORD"), + DBROPass: os.Getenv("POSTGRES_RO_PASSWORD"), + DBROUser: os.Getenv("POSTGRES_RO_USER"), DBSSL: os.Getenv("POSTGRES_SSL_ENABLE"), DBUser: os.Getenv("POSTGRES_USER"), FeeDenom: os.Getenv("FEE_DENOMINATION"), @@ -37,15 +39,14 @@ func run(conf pkg.Configuration) error { return fmt.Errorf("ensure database: %s", err) } - dbUri := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", conf.DBUser, conf.DBPass, - conf.DBHost, conf.DBName, conf.DBSSL) + dbUri := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", conf.DBUser, conf.DBPass, conf.DBHost, conf.DBName, conf.DBSSL) db, err := sql.Open("postgres", dbUri) if err != nil { return fmt.Errorf("cannot connect to postgres: %s", err) } defer db.Close() - if err := pkg.EnsureSchema(db); err != nil { + if err := pkg.EnsureSchema(db, conf.DBName, conf.DBROUser, conf.DBROPass); err != nil { return fmt.Errorf("ensure schema: %s", err) } diff --git a/cmd/block-metrics/pkg/config.go b/cmd/block-metrics/pkg/config.go index bbee2648..e3192922 100644 --- a/cmd/block-metrics/pkg/config.go +++ b/cmd/block-metrics/pkg/config.go @@ -1,11 +1,16 @@ package pkg type Configuration struct { + // database DBHost string - DBUser string - DBPass string DBName string DBSSL string + // Read-write user + DBUser string + DBPass string + // Read-only user + DBROUser string + DBROPass string // Denomination of the fee coin, eg uiov FeeDenom string // Tendermint light client daemon URL diff --git a/cmd/block-metrics/pkg/schema.go b/cmd/block-metrics/pkg/schema.go index e08805db..07b6285c 100644 --- a/cmd/block-metrics/pkg/schema.go +++ b/cmd/block-metrics/pkg/schema.go @@ -18,9 +18,45 @@ func EnsureDatabase(user, password, host, database, ssl string) error { return nil } -func EnsureSchema(pg *sql.DB) error { - // deal with the pesky TYPE 'action' that doesn't allow an IF NOT EXISTS clause +func EnsureSchema(pg *sql.DB, database, rouser, ropassword string) error { + // setup readonly on schema permissioned a la https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles/ rows, err := pg.Query(` + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name = 'permissioned'; + `) + if err != nil { + return fmt.Errorf("type query: %s", err) + } + if !rows.Next() { + sql := ` + CREATE SCHEMA permissioned; + ALTER DATABASE __db__ SET search_path = "$user", permissioned, public + REVOKE CREATE ON SCHEMA public FROM PUBLIC; + REVOKE ALL ON DATABASE __db__ FROM PUBLIC; + CREATE ROLE readonly; + GRANT CONNECT ON DATABASE __db__ TO readonly; + GRANT USAGE ON SCHEMA permissioned TO readonly; + ALTER DEFAULT PRIVILEGES IN SCHEMA permissioned GRANT SELECT ON TABLES TO readonly; + CREATE USER __rouser__ WITH PASSWORD '__ropassword__'; + GRANT readonly TO __rouser__; + ` + replacements := make(map[string]string) + replacements["__db__"] = database + replacements["__rouser__"] = rouser + replacements["__ropassword__"] = ropassword + for key, value := range replacements { + sql = strings.ReplaceAll(sql, key, value) + } + for _, query := range strings.Split(sql, "\n") { + if _, err := pg.Exec(query); err != nil { + return &QueryError{Query: query, Err: err} + } + } + } + + // deal with the pesky TYPE 'action' that doesn't allow an IF NOT EXISTS clause + rows, err = pg.Query(` SELECT pg_type.typname, pg_enum.enumlabel FROM pg_type JOIN pg_enum ON pg_enum.enumtypid = pg_type.oid @@ -61,7 +97,7 @@ func EnsureSchema(pg *sql.DB) error { } var schema = ` -CREATE TABLE IF NOT EXISTS blocks ( +CREATE TABLE IF NOT EXISTS permissioned.blocks ( block_height BIGINT NOT NULL PRIMARY KEY, block_hash TEXT NOT NULL UNIQUE, block_time TIMESTAMPTZ NOT NULL, @@ -69,17 +105,17 @@ CREATE TABLE IF NOT EXISTS blocks ( ); --- -CREATE TABLE IF NOT EXISTS transactions ( +CREATE TABLE IF NOT EXISTS permissioned.transactions ( id BIGSERIAL PRIMARY KEY, transaction_hash TEXT NOT NULL UNIQUE, - block_id BIGINT NOT NULL REFERENCES blocks(block_height), + block_id BIGINT NOT NULL REFERENCES permissioned.blocks(block_height), signatures BYTEA ARRAY, fee JSONB, memo text ); --- -CREATE TABLE IF NOT EXISTS domains ( +CREATE TABLE IF NOT EXISTS permissioned.domains ( id BIGSERIAL PRIMARY KEY, name TEXT, admin TEXT NOT NULL, @@ -89,9 +125,9 @@ CREATE TABLE IF NOT EXISTS domains ( ); --- -CREATE TABLE IF NOT EXISTS accounts ( +CREATE TABLE IF NOT EXISTS permissioned.accounts ( id BIGSERIAL PRIMARY KEY, - domain_id BIGINT NOT NULL REFERENCES domains(id), + domain_id BIGINT NOT NULL REFERENCES permissioned.domains(id), name TEXT, owner TEXT NOT NULL, metadata TEXT, @@ -100,25 +136,25 @@ CREATE TABLE IF NOT EXISTS accounts ( ); --- -CREATE TABLE IF NOT EXISTS resources ( +CREATE TABLE IF NOT EXISTS permissioned.resources ( id BIGSERIAL PRIMARY KEY, - account_id BIGINT REFERENCES accounts(id), + account_id BIGINT REFERENCES permissioned.accounts(id), uri TEXT, resource TEXT ); --- -CREATE TABLE IF NOT EXISTS certificates ( +CREATE TABLE IF NOT EXISTS permissioned.certificates ( id BIGSERIAL PRIMARY KEY, - account_id BIGINT REFERENCES accounts(id), + account_id BIGINT REFERENCES permissioned.accounts(id), certificate BYTEA ); --- -CREATE TABLE IF NOT EXISTS product_fees ( +CREATE TABLE IF NOT EXISTS permissioned.product_fees ( id BIGSERIAL PRIMARY KEY, - block BIGINT REFERENCES blocks(block_height), - account_id BIGINT REFERENCES accounts(id), + block BIGINT REFERENCES permissioned.blocks(block_height), + account_id BIGINT REFERENCES permissioned.accounts(id), action action, fee BIGINT, payer TEXT,