Skip to content

Commit

Permalink
fix: add external credentials support (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillmakhonin-brt authored Jun 20, 2024
1 parent 7fddd18 commit ab6a898
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 16 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ Available configuration fields are as follows:
| Server Hostname | Databricks Server Hostname (without http). i.e. `XXX.cloud.databricks.com` |
| Server Port | Databricks Server Port (default `443`) |
| HTTP Path | HTTP Path value for the existing cluster or SQL warehouse. i.e. `sql/1.0/endpoints/XXX` |
| Authentication Method | PAT (Personal Access Token) or M2M (Machine to Machine) OAuth Authentication |
| Client ID | Databricks Service Principal Client ID. (only if M2M OAuth is chosen as Auth Method) |
| Client Secret | Databricks Service Principal Client Secret. (only if M2M OAuth is chosen as Auth Method) |
| Authentication Method | PAT (Personal Access Token), M2M (Machine to Machine) OAuth or OAuth2 Client Credentials Authentication |
| Client ID | Databricks Service Principal Client ID. (only if OAuth / OAuth2 is chosen as Auth Method) |
| Client Secret | Databricks Service Principal Client Secret. (only if OAuth / OAuth2 is chosen as Auth Method) |
| Access Token | Personal Access Token for Databricks. (only if PAT is chosen as Auth Method) |
| OAuth2 Token Endpoint | URL of OAuth2 endpoint (only if OAuth2 Client Credentials Authentication is chosen as Auth Method) |
| Code Auto Completion | If enabled the SQL editor will fetch catalogs/schemas/tables/columns from Databricks to provide suggestions. |

### Supported Macros
Expand Down
71 changes: 71 additions & 0 deletions pkg/integrations/oauth2_client_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package integrations

import (
"context"
"github.com/databricks/databricks-sql-go/auth"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"net/http"
"sync"
"time"
)

type oauth2ClientCredentials struct {
clientID string
clientSecret string
tokenUrl string
tokenSource oauth2.TokenSource
mx sync.Mutex
}

func (c *oauth2ClientCredentials) Authenticate(r *http.Request) error {
c.mx.Lock()
defer c.mx.Unlock()
if c.tokenSource != nil {
token, err := c.tokenSource.Token()
if err != nil {
return err
}
token.SetAuthHeader(r)
return nil
}

config := clientcredentials.Config{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
TokenURL: c.tokenUrl,
}

// Create context with 1m timeout to cancel token fetching
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
if cancel != nil {
log.DefaultLogger.Debug("ignoring defer for timeout to not cancel original request")
}

c.tokenSource = config.TokenSource(ctx)

log.DefaultLogger.Debug("token fetching started")
token, err := c.tokenSource.Token()

if err != nil {
log.DefaultLogger.Error("token fetching failed", "err", err)
return err
} else {
log.DefaultLogger.Debug("token fetched successfully")
}
token.SetAuthHeader(r)

return nil

}

func NewOauth2ClientCredentials(clientID, clientSecret, tokenUrl string) auth.Authenticator {
return &oauth2ClientCredentials{
clientID: clientID,
clientSecret: clientSecret,
tokenUrl: tokenUrl,
tokenSource: nil,
mx: sync.Mutex{},
}
}
43 changes: 32 additions & 11 deletions pkg/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import (
"encoding/json"
"fmt"
dbsql "github.com/databricks/databricks-sql-go"
"github.com/databricks/databricks-sql-go/auth"
"github.com/databricks/databricks-sql-go/auth/oauth/m2m"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
"github.com/mullerpeter/databricks-grafana/pkg/integrations"
"reflect"
"strconv"
"strings"
Expand All @@ -36,11 +38,12 @@ var (
)

type DatasourceSettings struct {
Path string `json:"path"`
Hostname string `json:"hostname"`
Port string `json:"port"`
AuthenticationMethod string `json:"authenticationMethod"`
ClientId string `json:"clientId"`
Path string `json:"path"`
Hostname string `json:"hostname"`
Port string `json:"port"`
AuthenticationMethod string `json:"authenticationMethod"`
ClientId string `json:"clientId"`
ExternalCredentialsUrl string `json:"externalCredentialsUrl"`
}

// NewSampleDatasource creates a new datasource instance.
Expand All @@ -61,12 +64,30 @@ func NewSampleDatasource(_ context.Context, settings backend.DataSourceInstanceS
port = portInt
}

if datasourceSettings.AuthenticationMethod == "m2m" {
authenticator := m2m.NewAuthenticator(
datasourceSettings.ClientId,
settings.DecryptedSecureJSONData["clientSecret"],
datasourceSettings.Hostname,
)
if datasourceSettings.AuthenticationMethod == "m2m" || datasourceSettings.AuthenticationMethod == "oauth2_client_credentials" {
var authenticator auth.Authenticator

if datasourceSettings.AuthenticationMethod == "oauth2_client_credentials" {
if datasourceSettings.ExternalCredentialsUrl == "" {
log.DefaultLogger.Info("Authentication Method missing Credentials Url", "err", nil)
return nil, fmt.Errorf("authentication Method missing Credentials Url")
}
authenticator = integrations.NewOauth2ClientCredentials(
datasourceSettings.ClientId,
settings.DecryptedSecureJSONData["clientSecret"],
datasourceSettings.ExternalCredentialsUrl,
)
} else if datasourceSettings.AuthenticationMethod == "m2m" {
authenticator = m2m.NewAuthenticatorWithScopes(
datasourceSettings.ClientId,
settings.DecryptedSecureJSONData["clientSecret"],
datasourceSettings.Hostname,
[]string{},
)
} else {
log.DefaultLogger.Info("Authentication Method Parse Error", "err", nil)
return nil, fmt.Errorf("authentication Method Parse Error")
}

connector, err := dbsql.NewConnector(
dbsql.WithServerHostname(datasourceSettings.Hostname),
Expand Down
29 changes: 27 additions & 2 deletions src/components/ConfigEditor/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ export class ConfigEditor extends PureComponent<Props, State> {
});
}

onExternalCredentialsUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
const { onOptionsChange, options } = this.props;
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
externalCredentialsUrl: event.target.value,
},
});
};

render() {
const { options } = this.props;
const { secureJsonFields } = options;
Expand Down Expand Up @@ -162,7 +173,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
onChange={this.onPathChange}
/>
</InlineField>
<InlineField label="Authentication Method" labelWidth={30} tooltip="PAT (Personal Access Token) or M2M (Machine to Machine) OAuth Authentication">
<InlineField label="Authentication Method" labelWidth={30} tooltip="PAT (Personal Access Token), M2M (Machine to Machine) OAuth or OAuth 2.0 Client Credentials (not Databricks M2M) Authentication">
<Select
onChange={({ value }) => {
this.onAuthenticationMethodChange(value);
Expand All @@ -176,12 +187,26 @@ export class ConfigEditor extends PureComponent<Props, State> {
value: 'm2m',
label: 'M2M Oauth',
},
{
value: 'oauth2_client_credentials',
label: 'OAuth2 Client Credentials',
},
]}
value={jsonData.authenticationMethod || 'dsn'}
backspaceRemovesValue
/>
</InlineField>
{jsonData.authenticationMethod === 'm2m' ? (
{jsonData.authenticationMethod === 'oauth2_client_credentials' && (
<InlineField label="OAuth2 Token Endpoint" labelWidth={30} tooltip="HTTP URL to token endpoint">
<Input
value={jsonData.externalCredentialsUrl || ''}
placeholder="http://localhost:2020"
width={40}
onChange={this.onExternalCredentialsUrlChange}
/>
</InlineField>
)}
{(jsonData.authenticationMethod === 'm2m' || jsonData.authenticationMethod === 'oauth2_client_credentials') ? (
<>
<InlineField label="Client ID" labelWidth={30} tooltip="Databricks Service Principal Client ID">
<Input
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface MyDataSourceOptions extends DataSourceJsonData {
autoCompletion?: boolean;
authenticationMethod?: string;
clientId?: string;
externalCredentialsUrl?: string;
}

/**
Expand Down

0 comments on commit ab6a898

Please sign in to comment.