diff --git a/plugins/Snowflake/v1/README.md b/plugins/Snowflake/v1/README.md new file mode 100644 index 0000000..9853655 --- /dev/null +++ b/plugins/Snowflake/v1/README.md @@ -0,0 +1,3 @@ +# Snowflake plugin + +A simple data source for Snowflake that supports Snowflake SQL queries. Requires an OAuth connection. \ No newline at end of file diff --git a/plugins/Snowflake/v1/configValidation.json b/plugins/Snowflake/v1/configValidation.json new file mode 100644 index 0000000..4b1b524 --- /dev/null +++ b/plugins/Snowflake/v1/configValidation.json @@ -0,0 +1,43 @@ +{ + "steps": [ + { + "displayName": "API access", + "dataStream": { + "name": "sqlQuery", + "config": { + "query": "show databases" + } + }, + "success": "User credentials has Snowflake query permissions.", + "error": "User does not have permission to access the Snowflake API (query 'SHOW DATABASES' failed).", + "required": true + }, + { + "displayName": "Compute access", + "dataStream": { + "name": "sqlQuery", + "config": { + "query": "select 1/1", + "errorOnEmptyResults": true + } + }, + "success": "User has access to warehouse.", + "error": "User does not have access to a warehouse (query 'SELECT 1/1' failed). Check user's role is configured with a default warehouse and has warehouse permissions.", + "required": true + }, + { + "displayName": "Database access", + "dataStream": { + "name": "sqlQuery", + "config": { + "query": "show databases", + "errorOnEmptyResults": true + } + }, + "success": "User has access to at least one database.", + "error": "User does not have permission to access any databases. Check user's default role or specify a role.", + "required": false + } + ] +} + diff --git a/plugins/Snowflake/v1/dataStreams/scripts/sqlQuery-post.js b/plugins/Snowflake/v1/dataStreams/scripts/sqlQuery-post.js new file mode 100644 index 0000000..1437115 --- /dev/null +++ b/plugins/Snowflake/v1/dataStreams/scripts/sqlQuery-post.js @@ -0,0 +1,36 @@ +result = data.data.map( r => r.reduce((obj, value, i) => { + const columnName = data.resultSetMetaData.rowType[i].name; + obj[columnName] = value; + return obj; +}, {})); + +// support value column for autoComplete queries +if (context.config.valueColumn) { + metadata = [ + { + name: context.config.valueColumn, + role: "value" + } + ] +} else { + const typeMapping = { + "text": "string", + "fixed": "number", + "real": "number", + "varchar": "string", + "date": "date", + "timestamp": "date" + }; + + metadata = data.resultSetMetaData.rowType.map( c => { + return { + name: c.name, + shape: typeMapping[c.type] || "string" + } + }); +} + +// used for validation queries +if (context.config.errorOnEmptyResults === true && data.data.length === 0) { + throw new Error("No results"); +} \ No newline at end of file diff --git a/plugins/Snowflake/v1/dataStreams/sqlQuery.json b/plugins/Snowflake/v1/dataStreams/sqlQuery.json new file mode 100644 index 0000000..586f2a3 --- /dev/null +++ b/plugins/Snowflake/v1/dataStreams/sqlQuery.json @@ -0,0 +1,77 @@ +{ + "name": "sqlQuery", + "displayName": "SQL Query", + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "paging": { + "mode": "none" + }, + "expandInnerObjects": true, + "endpointPath": "/v2/statements/", + "postBody": { + "database": "{{typeof database !== 'undefined' ? database : undefined}}", + "schema": "{{typeof schema !== 'undefined' ? schema : undefined}}", + "statement": "{{query}}" + }, + "postRequestScript": "sqlQuery-post.js", + "getArgs": [], + "headers": [] + }, + "ui": [ + { + "name": "database", + "type": "autocomplete", + "label": "Database", + "validation": { + "required": false + }, + "isMulti": false, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "sqlQuery", + "dataSourceConfig": { + "valueColumn": "name", + "query": "show databases" + } + } + }, + { + "name": "schema", + "type": "autocomplete", + "label": "Schema", + "validation": { + "required": false + }, + "isMulti": false, + "allowCustomValues": true, + "data": { + "source": "dataStream", + "dataStreamName": "sqlQuery", + "dataSourceConfig": { + "valueColumn": "name", + "database": { + "fieldName": "database", + "required": true + }, + "query": "show schemas" + } + } + }, + { + "help": "Enter a query using Snowflake SQL syntax. You can also use parameters like {{timeframe.start}}, e.g. event_time BETWEEN '{{timeframe.start}}' AND '{{timeframe.end}}'", + "name": "query", + "language": "sql", + "label": "SQL query", + "type": "code", + "validation": { + "required": true + } + } + ], + "manualConfigApply": true, + "supportsNoneTimeframe": true, + "requiresParameterTimeframe": true, + "defaultTimeframe": "none" +} \ No newline at end of file diff --git a/plugins/Snowflake/v1/docs/setup.md b/plugins/Snowflake/v1/docs/setup.md new file mode 100644 index 0000000..36f2215 --- /dev/null +++ b/plugins/Snowflake/v1/docs/setup.md @@ -0,0 +1,99 @@ +# Before you start + +## Creating an OAuth integration in Snowflake + +The Snowflake data source authenticates using OAuth. + +Before configuring the data source you will need to register SquaredUp with your Snowflake account by creating a custom integration. + +Sample Snowflake commands for creating the integration are provided below. + +For more information on creating a Snowflake integration see: +https://docs.snowflake.com/en/sql-reference/sql/create-security-integration-oauth-snowflake + + +If your SquaredUp account is in the US region (default): + +``` +CREATE SECURITY INTEGRATION oauth_squaredup + TYPE = oauth + OAUTH_CLIENT = custom + OAUTH_CLIENT_TYPE = 'CONFIDENTIAL' + OAUTH_REDIRECT_URI = 'https://app.squaredup.com/settings/pluginsoauth2' + COMMENT = 'Used by SquaredUp to connect to this Snowflake account' +``` + +If your SquaredUp account is in the EU region: + +``` +CREATE SECURITY INTEGRATION oauth_squaredup + TYPE = oauth + OAUTH_CLIENT = custom + OAUTH_CLIENT_TYPE = 'CONFIDENTIAL' + OAUTH_REDIRECT_URI = 'https://eu.app.squaredup.com/settings/pluginsoauth2' + COMMENT = 'Used by SquaredUp to connect to this Snowflake account' +``` + +Once your integration is created, run: + +``` +SELECT + oauth:OAUTH_CLIENT_SECRET::STRING AS OAUTH_CLIENT_SECRET, + oauth:OAUTH_CLIENT_ID::STRING AS OAUTH_CLIENT_ID +FROM (SELECT PARSE_JSON(SYSTEM$SHOW_OAUTH_CLIENT_SECRETS('oauth_squaredup')) AS oauth) + +``` + +Use the values of the `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` columns in your configuration below. + + +## Creating a read-only user + +To connect to Snowflake you will need the credentials for a Snowflake user. + +By default, it is NOT possible to connect via OAuth using an ACCOUNTADMIN role. Snowflake automatically adds privileged roles to the blocked role list used for OAuth authorization, see https://docs.snowflake.com/en/sql-reference/parameters#oauth-add-privileged-roles-to-blocked-list + +We recommend a dedicated 'squaredup' user account that is assigned read only role. For more information on Snowflake users and roles, see https://docs.snowflake.com/en/user-guide/security-access-control-configure. + +Ensure the user has a default role set, or specify the role when configuring the data source (see below). If the user does not have a default role and no role is specified, the connection will use the PUBLIC role, which typically does not have any permissions to databases. + + +# Configuration + +## Snowflake account identifier + +Enter your Snowflake account identifier. + +This can be found in the Snowflake portal under 'Your Username' > Account > Account Identifier. + +The account identifier is in the format -. + +For example: `ABCDEFG-XYZ12345` + +Alternatively, run the following Snowflake query: + +``` +SELECT CURRENT_ORGANIZATION_NAME() || '-' || CURRENT_ACCOUNT_NAME(); +``` + +## Snowflake OAuth client ID + +The client ID for your Snowflake OAuth application. + +Enter the `OAUTH_CLIENT_ID` value from the integration you created above. + +## Snowflake OAuth client secret + +The client secret for your Snowflake OAuth application. + +Enter the `OAUTH_CLIENT_SECRET` value from the integration you created above. + +## Role (optional) + +Restrict OAuth connection to a specific role. If not specified, the user's default role is used. + +If you have created a custom role for your database, for example a read-only role, enter its name here. + +## Authorize + +Click the Sign-in button to authorize SquaredUp to access Snowflake. diff --git a/plugins/Snowflake/v1/icon.svg b/plugins/Snowflake/v1/icon.svg new file mode 100644 index 0000000..c7a0eba --- /dev/null +++ b/plugins/Snowflake/v1/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/Snowflake/v1/metadata.json b/plugins/Snowflake/v1/metadata.json new file mode 100644 index 0000000..beafd55 --- /dev/null +++ b/plugins/Snowflake/v1/metadata.json @@ -0,0 +1,50 @@ +{ + "name": "snowflake", + "displayName": "Snowflake", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "description": "Query data from Snowflake.", + "category": "Database", + "type": "hybrid", + "schemaVersion": "2.0", + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "queryArgs": [], + "headers": [], + "oauth2TokenExtraArgs": [], + "oauth2ClientSecret": "{{oauth2ClientSecret}}", + "oauth2ClientSecretLocationDuringAuth": "header", + "oauth2AuthUrl": "https://{{accountId}}.snowflakecomputing.com/oauth/authorize", + "authMode": "oauth2", + "oauth2GrantType": "authCode", + "baseUrl": "https://{{accountId}}.snowflakecomputing.com/api", + "oauth2TokenExtraHeaders": [ + { + "value": "application/x-www-form-urlencoded", + "key": "Content-Type" + } + ], + "oauth2ClientId": "{{oauth2ClientId}}", + "oauth2TokenUrl": "https://{{accountId}}.snowflakecomputing.com/oauth/token-request", + "oauth2AuthExtraArgs": [], + "oauth2Scope": "refresh_token {{oauth2Role? 'session:role:' + oauth2Role : ''}}" + } + }, + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Snowflake/v1/docs/setup.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Snowflake/v1", + "label": "Repository" + } + ] +} \ No newline at end of file diff --git a/plugins/Snowflake/v1/ui.json b/plugins/Snowflake/v1/ui.json new file mode 100644 index 0000000..fcaaed9 --- /dev/null +++ b/plugins/Snowflake/v1/ui.json @@ -0,0 +1,50 @@ +[ + { + "type": "text", + "name": "accountId", + "label": "Snowflake account identifier", + "help": "Enter your Snowflake account identifier. Find this in the portal under Your Username > Account > Account Identifier. It is in the format -, e.g. ABCDEFG-XYZ12345", + "validation": { + "required": true + }, + "placeholder": "-, e.g. ABCDEFG-XYZ12345" + }, + { + "type": "text", + "name": "oauth2ClientId", + "label": "Snowflake OAuth client ID", + "help": "The client ID for your Snowflake OAuth application. See documentation for details on how to set up an OAuth application in Snowflake and obtain the client ID.", + "validation": { + "required": true + }, + "placeholder": "Enter your Snowflake OAuth client ID" + }, + { + "type": "password", + "name": "oauth2ClientSecret", + "label": "Snowflake OAuth secret", + "help": "The client secret for your Snowflake OAuth application. See documentation for details on how to set up an OAuth application in Snowflake and obtain the client secret.", + "validation": { + "required": true + }, + "placeholder": "Enter your Snowflake OAuth secret" + }, + { + "type": "text", + "name": "oauth2Role", + "label": "Role (optional)", + "help": "Scope OAuth connection to a specific role. If not specified, the user's default role is used.", + "validation": { + "required": false + }, + "placeholder": "Enter your Snowflake OAuth role" + }, + { + "type": "oAuth2", + "name": "oauth2AuthCodeSignIn", + "label": "Authorize", + "validation": { + "required": true + } + } +] \ No newline at end of file