Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPLT-1049 Provision separate DB per user #132

Merged
merged 20 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5f36a18
feat: Add datasource to hasura
morgsmccauley Jul 13, 2023
d1f6166
chore: Add `pg`
morgsmccauley Jul 13, 2023
f2e3e3f
feat: Create PG DB restricted to user
morgsmccauley Jul 13, 2023
001425e
feat: Provision user DB and add to Hasura
morgsmccauley Jul 17, 2023
868ba2c
refactor: Use postgres pool
morgsmccauley Jul 17, 2023
46b3896
feat: Run user sql and track in hasura
morgsmccauley Jul 17, 2023
0c2a6e4
test: Test separate db per user provisioning
morgsmccauley Jul 17, 2023
2116c94
feat: Generate random passwords for user DBs
morgsmccauley Jul 17, 2023
f2c9269
feat: Escape user input before executing sql
morgsmccauley Jul 18, 2023
d68f2fd
fix: Correctly espace pg user password
morgsmccauley Jul 18, 2023
539a559
refactor: Get table names via hasura provided method
morgsmccauley Jul 18, 2023
07e61b5
refactor: Extract default schema to constant
morgsmccauley Jul 18, 2023
992cc20
feat: Add method to check if user api is provisioned
morgsmccauley Jul 18, 2023
ab8856c
feat: Provision separate DB per user in runner
morgsmccauley Jul 18, 2023
12cb3f8
refactor: Move default password length to fn signature
morgsmccauley Jul 18, 2023
1a35f43
refactor: Adjust provisioner function args
morgsmccauley Jul 18, 2023
fe998e5
feat: Provision schema per function
morgsmccauley Jul 18, 2023
aec4195
fix: Check both source/schema when verifying provisioning status
morgsmccauley Jul 18, 2023
8e82c4b
fix: Skip provisioning datasource if it exists
morgsmccauley Jul 18, 2023
58397e9
fix: Add trailing `_` to graphql typename prefix
morgsmccauley Jul 18, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 75 additions & 30 deletions indexer-js-queue-handler/__snapshots__/hasura-client.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`HasuraClient adds a datasource 1`] = `
{
"args": {
"configuration": {
"connection_info": {
"database_url": {
"connection_parameters": {
"database": "morgs_near",
"host": "localhost",
"password": "password",
"port": 5432,
"username": "morgs_near",
},
},
},
},
"customization": {
"root_fields": {
"namespace": "morgs_near",
},
"type_names": {
"prefix": "morgs_near_",
},
},
"name": "morgs_near",
},
"type": "pg_add_source",
}
`;

exports[`HasuraClient adds the specified permissions for the specified roles/table/schema 1`] = `
{
"args": [
Expand Down Expand Up @@ -152,51 +182,66 @@ exports[`HasuraClient adds the specified permissions for the specified roles/tab
}
`;

exports[`HasuraClient checks if a schema exists 1`] = `
exports[`HasuraClient checks if a schema exists within source 1`] = `
[
[
"mock-hasura-endpoint/v2/query",
{
"body": "{"type":"run_sql","args":{"sql":"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'schema'","read_only":true,"source":"source"}}",
"headers": {
"X-Hasura-Admin-Secret": "mock-hasura-admin-secret",
},
"method": "POST",
},
],
]
`;

exports[`HasuraClient checks if datasource exists 1`] = `
{
"args": {
"read_only": true,
"source": "default",
"sql": "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'name'",
},
"type": "run_sql",
"args": {},
"type": "export_metadata",
"version": 2,
}
`;

exports[`HasuraClient creates a schema 1`] = `
{
"args": {
"read_only": false,
"source": "default",
"sql": "CREATE schema name",
},
"type": "run_sql",
}
[
[
"mock-hasura-endpoint/v2/query",
{
"body": "{"type":"run_sql","args":{"sql":"CREATE schema schemaName","read_only":false,"source":"dbName"}}",
"headers": {
"X-Hasura-Admin-Secret": "mock-hasura-admin-secret",
},
"method": "POST",
},
],
]
`;

exports[`HasuraClient gets table names within a schema 1`] = `
{
"args": {
"read_only": true,
"source": "default",
"sql": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'schema'",
"source": "source",
},
"type": "run_sql",
"type": "pg_get_source_tables",
}
`;

exports[`HasuraClient runs migrations for the specified schema 1`] = `
{
"args": {
"read_only": false,
"source": "default",
"sql": "
set schema 'schema';
CREATE TABLE blocks (height numeric)
",
},
"type": "run_sql",
}
[
[
"mock-hasura-endpoint/v2/query",
{
"body": "{"type":"run_sql","args":{"sql":"\\n set schema 'schemaName';\\n CREATE TABLE blocks (height numeric)\\n ","read_only":false,"source":"dbName"}}",
"headers": {
"X-Hasura-Admin-Secret": "mock-hasura-admin-secret",
},
"method": "POST",
},
],
]
`;

exports[`HasuraClient tracks foreign key relationships 1`] = `
Expand Down
22 changes: 22 additions & 0 deletions indexer-js-queue-handler/__snapshots__/provisioner.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Provisioner provisionUserApi formats user input before executing the query 1`] = `
[
[
"CREATE DATABASE "databaseName UNION SELECT * FROM users --"",
[],
],
[
"CREATE USER morgs_near WITH PASSWORD 'pass; DROP TABLE users;--'",
[],
],
[
"GRANT ALL PRIVILEGES ON DATABASE "databaseName UNION SELECT * FROM users --" TO morgs_near",
[],
],
[
"REVOKE CONNECT ON DATABASE "databaseName UNION SELECT * FROM users --" FROM PUBLIC",
[],
],
]
`;
89 changes: 67 additions & 22 deletions indexer-js-queue-handler/hasura-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default class HasuraClient {
args: {
sql,
read_only: opts.readOnly,
source: 'default',
source: opts.source || 'default',
}
}),
});
Expand All @@ -36,7 +36,7 @@ export default class HasuraClient {
return JSON.parse(body)
};

async executeMetadataRequest (type, args) {
async executeMetadataRequest (type, args, version) {
const response = await this.deps.fetch(`${process.env.HASURA_ENDPOINT}/v1/metadata`, {
method: 'POST',
headers: {
Expand All @@ -45,6 +45,7 @@ export default class HasuraClient {
body: JSON.stringify({
type,
args,
...(version && { version })
}),
});

Expand All @@ -61,46 +62,61 @@ export default class HasuraClient {
return this.executeMetadataRequest('bulk', metadataRequests);
}

async isSchemaCreated (schemaName) {
async exportMetadata() {
const { metadata } = await this.executeMetadataRequest('export_metadata', {}, 2);
return metadata;
}

async doesSourceExist(source) {
const metadata = await this.exportMetadata();
return metadata.sources.filter(({ name }) => name === source).length > 0;
}

async doesSchemaExist(source, schemaName) {
const { result } = await this.executeSql(
`SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${schemaName}'`,
{ readOnly: true }
{ source, readOnly: true }
);

return result.length > 1;
};
}

createSchema (schemaName) {
createSchema (source, schemaName) {
return this.executeSql(
`CREATE schema ${schemaName}`,
{ readOnly: false }
{ source, readOnly: false }
);
}

runMigrations(schemaName, migration) {
runMigrations(source, schemaName, migration) {
return this.executeSql(
`
set schema '${schemaName}';
${migration}
`,
{ readOnly: false }
{ source, readOnly: false }
);
}

async getTableNames(schemaName) {
const { result } = await this.executeSql(
`SELECT table_name FROM information_schema.tables WHERE table_schema = '${schemaName}'`,
{ readOnly: true }
async getTableNames(schemaName, source) {
const tablesInSource = await this.executeMetadataRequest(
'pg_get_source_tables',
{
source
}
);
const [_columnNames, ...tableNames] = result;
return tableNames.flat();

return tablesInSource
.filter(({ name, schema }) => schema === schemaName)
.map(({ name }) => name);
};

async trackTables(schemaName, tableNames) {
async trackTables(schemaName, tableNames, source) {
return this.executeBulkMetadataRequest(
tableNames.map((name) => ({
type: 'pg_track_table',
args: {
source,
table: {
name,
schema: schemaName,
Expand All @@ -110,7 +126,7 @@ export default class HasuraClient {
);
}

async getForeignKeys(schemaName) {
async getForeignKeys(schemaName, source) {
const { result } = await this.executeSql(
`
SELECT
Expand Down Expand Up @@ -158,16 +174,16 @@ export default class HasuraClient {
q.table_name,
q.constraint_name) AS info;
`,
{ readOnly: true }
{ readOnly: true, source }
);

const [_, [foreignKeysJsonString]] = result;

return JSON.parse(foreignKeysJsonString);
}

async trackForeignKeyRelationships(schemaName) {
const foreignKeys = await this.getForeignKeys(schemaName);
async trackForeignKeyRelationships(schemaName, source) {
const foreignKeys = await this.getForeignKeys(schemaName, source);

if (foreignKeys.length === 0) {
return;
Expand All @@ -179,6 +195,7 @@ export default class HasuraClient {
{
type: "pg_create_array_relationship",
args: {
source,
name: foreignKey.table_name,
table: {
name: foreignKey.ref_table,
Expand All @@ -198,6 +215,7 @@ export default class HasuraClient {
{
type: "pg_create_object_relationship",
args: {
source,
name: pluralize.singular(foreignKey.ref_table),
table: {
name: foreignKey.table_name,
Expand All @@ -213,13 +231,14 @@ export default class HasuraClient {
);
}

async addPermissionsToTables(schemaName, tableNames, roleName, permissions) {
async addPermissionsToTables(schemaName, source, tableNames, roleName, permissions) {
return this.executeBulkMetadataRequest(
tableNames
.map((tableName) => (
permissions.map((permission) => ({
type: `pg_create_${permission}_permission`,
args: {
source,
table: {
name: tableName,
schema: schemaName,
Expand All @@ -234,11 +253,37 @@ export default class HasuraClient {
? { allow_aggregations: true }
: { backend_only: true }),
},
source: 'default'
},
}))
))
.flat()
);
}

async addDatasource(userName, password, databaseName) {
return this.executeMetadataRequest("pg_add_source", {
name: databaseName,
configuration: {
connection_info: {
database_url: {
connection_parameters: {
password,
database: databaseName,
username: userName,
host: process.env.PG_HOST,
port: Number(process.env.PG_PORT),
}
},
},
},
customization: {
root_fields: {
namespace: userName,
},
type_names: {
prefix: `${userName}_`,
},
},
});
}
}
Loading