Skip to content

Commit ab6a898

Browse files
fix: add external credentials support (#80)
1 parent 7fddd18 commit ab6a898

File tree

5 files changed

+135
-16
lines changed

5 files changed

+135
-16
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,11 @@ Available configuration fields are as follows:
6767
| Server Hostname | Databricks Server Hostname (without http). i.e. `XXX.cloud.databricks.com` |
6868
| Server Port | Databricks Server Port (default `443`) |
6969
| HTTP Path | HTTP Path value for the existing cluster or SQL warehouse. i.e. `sql/1.0/endpoints/XXX` |
70-
| Authentication Method | PAT (Personal Access Token) or M2M (Machine to Machine) OAuth Authentication |
71-
| Client ID | Databricks Service Principal Client ID. (only if M2M OAuth is chosen as Auth Method) |
72-
| Client Secret | Databricks Service Principal Client Secret. (only if M2M OAuth is chosen as Auth Method) |
70+
| Authentication Method | PAT (Personal Access Token), M2M (Machine to Machine) OAuth or OAuth2 Client Credentials Authentication |
71+
| Client ID | Databricks Service Principal Client ID. (only if OAuth / OAuth2 is chosen as Auth Method) |
72+
| Client Secret | Databricks Service Principal Client Secret. (only if OAuth / OAuth2 is chosen as Auth Method) |
7373
| Access Token | Personal Access Token for Databricks. (only if PAT is chosen as Auth Method) |
74+
| OAuth2 Token Endpoint | URL of OAuth2 endpoint (only if OAuth2 Client Credentials Authentication is chosen as Auth Method) |
7475
| Code Auto Completion | If enabled the SQL editor will fetch catalogs/schemas/tables/columns from Databricks to provide suggestions. |
7576

7677
### Supported Macros
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package integrations
2+
3+
import (
4+
"context"
5+
"github.com/databricks/databricks-sql-go/auth"
6+
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
7+
"golang.org/x/oauth2"
8+
"golang.org/x/oauth2/clientcredentials"
9+
"net/http"
10+
"sync"
11+
"time"
12+
)
13+
14+
type oauth2ClientCredentials struct {
15+
clientID string
16+
clientSecret string
17+
tokenUrl string
18+
tokenSource oauth2.TokenSource
19+
mx sync.Mutex
20+
}
21+
22+
func (c *oauth2ClientCredentials) Authenticate(r *http.Request) error {
23+
c.mx.Lock()
24+
defer c.mx.Unlock()
25+
if c.tokenSource != nil {
26+
token, err := c.tokenSource.Token()
27+
if err != nil {
28+
return err
29+
}
30+
token.SetAuthHeader(r)
31+
return nil
32+
}
33+
34+
config := clientcredentials.Config{
35+
ClientID: c.clientID,
36+
ClientSecret: c.clientSecret,
37+
TokenURL: c.tokenUrl,
38+
}
39+
40+
// Create context with 1m timeout to cancel token fetching
41+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
42+
if cancel != nil {
43+
log.DefaultLogger.Debug("ignoring defer for timeout to not cancel original request")
44+
}
45+
46+
c.tokenSource = config.TokenSource(ctx)
47+
48+
log.DefaultLogger.Debug("token fetching started")
49+
token, err := c.tokenSource.Token()
50+
51+
if err != nil {
52+
log.DefaultLogger.Error("token fetching failed", "err", err)
53+
return err
54+
} else {
55+
log.DefaultLogger.Debug("token fetched successfully")
56+
}
57+
token.SetAuthHeader(r)
58+
59+
return nil
60+
61+
}
62+
63+
func NewOauth2ClientCredentials(clientID, clientSecret, tokenUrl string) auth.Authenticator {
64+
return &oauth2ClientCredentials{
65+
clientID: clientID,
66+
clientSecret: clientSecret,
67+
tokenUrl: tokenUrl,
68+
tokenSource: nil,
69+
mx: sync.Mutex{},
70+
}
71+
}

pkg/plugin/plugin.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import (
77
"encoding/json"
88
"fmt"
99
dbsql "github.com/databricks/databricks-sql-go"
10+
"github.com/databricks/databricks-sql-go/auth"
1011
"github.com/databricks/databricks-sql-go/auth/oauth/m2m"
1112
"github.com/grafana/grafana-plugin-sdk-go/backend"
1213
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
1314
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
1415
"github.com/grafana/grafana-plugin-sdk-go/data"
1516
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
17+
"github.com/mullerpeter/databricks-grafana/pkg/integrations"
1618
"reflect"
1719
"strconv"
1820
"strings"
@@ -36,11 +38,12 @@ var (
3638
)
3739

3840
type DatasourceSettings struct {
39-
Path string `json:"path"`
40-
Hostname string `json:"hostname"`
41-
Port string `json:"port"`
42-
AuthenticationMethod string `json:"authenticationMethod"`
43-
ClientId string `json:"clientId"`
41+
Path string `json:"path"`
42+
Hostname string `json:"hostname"`
43+
Port string `json:"port"`
44+
AuthenticationMethod string `json:"authenticationMethod"`
45+
ClientId string `json:"clientId"`
46+
ExternalCredentialsUrl string `json:"externalCredentialsUrl"`
4447
}
4548

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

64-
if datasourceSettings.AuthenticationMethod == "m2m" {
65-
authenticator := m2m.NewAuthenticator(
66-
datasourceSettings.ClientId,
67-
settings.DecryptedSecureJSONData["clientSecret"],
68-
datasourceSettings.Hostname,
69-
)
67+
if datasourceSettings.AuthenticationMethod == "m2m" || datasourceSettings.AuthenticationMethod == "oauth2_client_credentials" {
68+
var authenticator auth.Authenticator
69+
70+
if datasourceSettings.AuthenticationMethod == "oauth2_client_credentials" {
71+
if datasourceSettings.ExternalCredentialsUrl == "" {
72+
log.DefaultLogger.Info("Authentication Method missing Credentials Url", "err", nil)
73+
return nil, fmt.Errorf("authentication Method missing Credentials Url")
74+
}
75+
authenticator = integrations.NewOauth2ClientCredentials(
76+
datasourceSettings.ClientId,
77+
settings.DecryptedSecureJSONData["clientSecret"],
78+
datasourceSettings.ExternalCredentialsUrl,
79+
)
80+
} else if datasourceSettings.AuthenticationMethod == "m2m" {
81+
authenticator = m2m.NewAuthenticatorWithScopes(
82+
datasourceSettings.ClientId,
83+
settings.DecryptedSecureJSONData["clientSecret"],
84+
datasourceSettings.Hostname,
85+
[]string{},
86+
)
87+
} else {
88+
log.DefaultLogger.Info("Authentication Method Parse Error", "err", nil)
89+
return nil, fmt.Errorf("authentication Method Parse Error")
90+
}
7091

7192
connector, err := dbsql.NewConnector(
7293
dbsql.WithServerHostname(datasourceSettings.Hostname),

src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,17 @@ export class ConfigEditor extends PureComponent<Props, State> {
129129
});
130130
}
131131

132+
onExternalCredentialsUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
133+
const { onOptionsChange, options } = this.props;
134+
onOptionsChange({
135+
...options,
136+
jsonData: {
137+
...options.jsonData,
138+
externalCredentialsUrl: event.target.value,
139+
},
140+
});
141+
};
142+
132143
render() {
133144
const { options } = this.props;
134145
const { secureJsonFields } = options;
@@ -162,7 +173,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
162173
onChange={this.onPathChange}
163174
/>
164175
</InlineField>
165-
<InlineField label="Authentication Method" labelWidth={30} tooltip="PAT (Personal Access Token) or M2M (Machine to Machine) OAuth Authentication">
176+
<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">
166177
<Select
167178
onChange={({ value }) => {
168179
this.onAuthenticationMethodChange(value);
@@ -176,12 +187,26 @@ export class ConfigEditor extends PureComponent<Props, State> {
176187
value: 'm2m',
177188
label: 'M2M Oauth',
178189
},
190+
{
191+
value: 'oauth2_client_credentials',
192+
label: 'OAuth2 Client Credentials',
193+
},
179194
]}
180195
value={jsonData.authenticationMethod || 'dsn'}
181196
backspaceRemovesValue
182197
/>
183198
</InlineField>
184-
{jsonData.authenticationMethod === 'm2m' ? (
199+
{jsonData.authenticationMethod === 'oauth2_client_credentials' && (
200+
<InlineField label="OAuth2 Token Endpoint" labelWidth={30} tooltip="HTTP URL to token endpoint">
201+
<Input
202+
value={jsonData.externalCredentialsUrl || ''}
203+
placeholder="http://localhost:2020"
204+
width={40}
205+
onChange={this.onExternalCredentialsUrlChange}
206+
/>
207+
</InlineField>
208+
)}
209+
{(jsonData.authenticationMethod === 'm2m' || jsonData.authenticationMethod === 'oauth2_client_credentials') ? (
185210
<>
186211
<InlineField label="Client ID" labelWidth={30} tooltip="Databricks Service Principal Client ID">
187212
<Input

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface MyDataSourceOptions extends DataSourceJsonData {
2828
autoCompletion?: boolean;
2929
authenticationMethod?: string;
3030
clientId?: string;
31+
externalCredentialsUrl?: string;
3132
}
3233

3334
/**

0 commit comments

Comments
 (0)