Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions plugins/Snowflake/v1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Snowflake plugin

A simple data source for Snowflake that supports Snowflake SQL queries. Requires an OAuth connection.
43 changes: 43 additions & 0 deletions plugins/Snowflake/v1/configValidation.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}

36 changes: 36 additions & 0 deletions plugins/Snowflake/v1/dataStreams/scripts/sqlQuery-post.js
Original file line number Diff line number Diff line change
@@ -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");
}
80 changes: 80 additions & 0 deletions plugins/Snowflake/v1/dataStreams/sqlQuery.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"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": [],
"valueColumn": "{{typeof valueColumn !== 'undefined' ? valueColumn : undefined}}",
"errorOnEmptyResults": "{{typeof errorOnEmptyResults !== 'undefined' ? errorOnEmptyResults : undefined}}"

},
"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"
}
99 changes: 99 additions & 0 deletions plugins/Snowflake/v1/docs/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Before you start

## Creating an OAuth integration in Snowflake

The Snowflake data source authenticates using OAuth.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Line 6 of plugins/Snowflake/v1/docs/setup.md contains a typo: "bby" should be "by" in the sentence "register SquaredUp with your Snowflake account bby creating a custom integration." This is a minor documentation error that should be corrected before merging.

Extended reasoning...

The typo 'bby' (a double-b error) appears on line 6 of docs/setup.md in the newly added Snowflake plugin documentation. The affected sentence reads: 'Before configuring the data source you will need to register SquaredUp with your Snowflake account bby creating a custom integration.'

The correct word should be 'by', making the sentence: 'Before configuring the data source you will need to register SquaredUp with your Snowflake account by creating a custom integration.'

This is a straightforward typographical error introduced in this PR. The file docs/setup.md is a new file added as part of the Snowflake plugin, so the typo was introduced in this change.

While the error does not affect the functionality of the plugin in any way (it is purely documentation), it presents a poor first impression to users following the setup guide. The sentence appears in the introductory section explaining OAuth setup, which users will read first when configuring the plugin.

The fix is trivial: replace 'bby' with 'by' on line 6 of plugins/Snowflake/v1/docs/setup.md.

Step-by-step proof: Reading the diff for docs/setup.md, line 6 (the 6th non-blank added line) reads: 'Before configuring the data source you will need to register SquaredUp with your Snowflake account bby creating a custom integration.' The word 'bby' is clearly 'by' with an extra 'b' character.

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
Comment on lines +16 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Both the US and EU region OAuth sections in docs/setup.md are labeled (default), but only one region can be the default — the EU section's (default) label is a copy-paste error. Remove (default) from the EU region heading so users aren't confused about which region applies to them.

Extended reasoning...

What the bug is and how it manifests

In plugins/Snowflake/v1/docs/setup.md, two consecutive OAuth integration code blocks guide users to create a Snowflake security integration with the correct redirect URI for their SquaredUp region. Both section headings currently read (default): line 15 says "If your SquaredUp account is in the US region (default):" and line 26 says "If your SquaredUp account is in the EU region (default):". By definition, only a single region can be the default.

The specific code path that triggers it

This is purely a documentation issue in the newly added setup.md file. The EU section heading was authored by copy-pasting the US section heading and updating the region name and redirect URI, but the (default) label was accidentally left in place.

Why existing checks don't prevent it

There is no automated lint or validation step for documentation prose in this repository, so the copy-paste error passed undetected. Both code blocks differ only in the OAUTH_REDIRECT_URI value (https://app.squaredup.com/... vs https://eu.app.squaredup.com/...), which means the actual functional content is correct — only the label is wrong.

Impact

A new EU user reading the setup guide sees both sections labeled (default) and is uncertain which applies to them. They may follow the US instructions and configure the wrong redirect URI, causing OAuth authorization to fail entirely. Conversely, a US user skimming the page could be misled into thinking the EU section is equally authoritative as the default path.

How to fix it

Remove the word (default) from line 26 so it reads: "If your SquaredUp account is in the EU region:"

Step-by-step proof

  1. Open plugins/Snowflake/v1/docs/setup.md.
  2. Line 15: "If your SquaredUp account is in the US region (default):" — correct, US is the default.
  3. Line 26: "If your SquaredUp account is in the EU region (default):" — incorrect, EU cannot also be the default.
  4. A user in the EU reads both labels and reasonably concludes either section applies, potentially selecting the US section and setting OAUTH_REDIRECT_URI = 'https://app.squaredup.com/settings/pluginsoauth2', which will cause OAuth login to fail for their EU account because SquaredUp will reject the redirect.

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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The line 'For example: https://<your-opensearch-host>:9200' in the Snowflake account identifier section of docs/setup.md is a copy-paste error from an OpenSearch plugin. It should be removed, as the correct format is already documented on the line above: <org_name>-<account_name>, e.g. ABCDEFG-XYZ12345.

Extended reasoning...

What the bug is: Line 71 of plugins/Snowflake/v1/docs/setup.md contains the text For example: https://:9200`` inside the "Snowflake account identifier" configuration section. This is an OpenSearch/Elasticsearch URL referencing port 9200 (the default OpenSearch port) and has absolutely no relevance to Snowflake.

The specific code path that triggers it: Any user reading the setup documentation to configure the Snowflake plugin will encounter this misleading example when trying to determine what value to enter for the "Snowflake account identifier" field.

Why existing code doesn't prevent it: This is a documentation error — there is no automated check to catch copy-paste mistakes in markdown files. The surrounding text on the previous line already correctly describes the format (<org_name>-<account_name>, e.g. ABCDEFG-XYZ12345), making the erroneous "For example" line all the more confusing since it contradicts the correct description immediately above it.

Impact: A user reading this documentation would see two conflicting pieces of information: one saying the format is <org_name>-<account_name> and another saying it looks like https://<your-opensearch-host>:9200. This would confuse users and may cause them to enter an incorrect value for the account identifier field, resulting in failed authentication.

How to fix it: Simply remove line 71 (For example: https://:9200``) entirely. The correct format and example (ABCDEFG-XYZ12345) is already present on the preceding line, so no replacement is needed.

Step-by-step proof:

  1. Open plugins/Snowflake/v1/docs/setup.md and navigate to the "Snowflake account identifier" section (around line 63).
  2. Line 70 correctly states: The account identifier It is in the format <org_name>-<account_name>, e.g. ABCDEFG-XYZ12345
  3. Line 71 then states: For example: https://:9200`` — this references an OpenSearch host URL with port 9200.
  4. Compare with any OpenSearch plugin docs; this example belongs there, not in a Snowflake account identifier section.
  5. A user following this doc would be confused about whether to enter ABCDEFG-XYZ12345 or https://myhost:9200 as their account identifier.


The account identifier is in the format <org_name>-<account_name>.

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.
1 change: 1 addition & 0 deletions plugins/Snowflake/v1/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions plugins/Snowflake/v1/metadata.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
50 changes: 50 additions & 0 deletions plugins/Snowflake/v1/ui.json
Original file line number Diff line number Diff line change
@@ -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 <org_name>-<account_name>, e.g. ABCDEFG-XYZ12345",
"validation": {
"required": true
},
"placeholder": "<org_name>-<account_name>, 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
}
}
]
Loading