Skip to content

Commit a16df3f

Browse files
authored
Update plugin to support access token auth (#97)
* Add new option in UI and Client * Fix instance factory func * go mod tidy * re-ignore coverage * add HTTP headers component to UI * properly re ignore coverage * Add UI option to configure access token and project
1 parent 3e2fbf1 commit a16df3f

File tree

8 files changed

+198
-331
lines changed

8 files changed

+198
-331
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
22
dist/
3+
coverage/

go.mod

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ require (
2424
cloud.google.com/go/iam v1.5.2 // indirect
2525
cloud.google.com/go/longrunning v0.6.7 // indirect
2626
github.com/BurntSushi/toml v1.3.2 // indirect
27-
github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect
2827
github.com/apache/arrow/go/v15 v15.0.2 // indirect
2928
github.com/beorn7/perks v1.0.1 // indirect
3029
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -37,7 +36,6 @@ require (
3736
github.com/fatih/color v1.15.0 // indirect
3837
github.com/felixge/httpsnoop v1.0.4 // indirect
3938
github.com/getkin/kin-openapi v0.124.0 // indirect
40-
github.com/ghodss/yaml v1.0.0 // indirect
4139
github.com/go-logr/logr v1.4.2 // indirect
4240
github.com/go-logr/stdr v1.2.2 // indirect
4341
github.com/go-openapi/jsonpointer v0.20.2 // indirect
@@ -54,10 +52,8 @@ require (
5452
github.com/gorilla/mux v1.8.1 // indirect
5553
github.com/grafana/otel-profiling-go v0.5.1 // indirect
5654
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
57-
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
5855
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
5956
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect
60-
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
6157
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
6258
github.com/hashicorp/go-hclog v1.6.3 // indirect
6359
github.com/hashicorp/go-plugin v1.6.1 // indirect
@@ -73,7 +69,6 @@ require (
7369
github.com/mattn/go-colorable v0.1.13 // indirect
7470
github.com/mattn/go-isatty v0.0.19 // indirect
7571
github.com/mattn/go-runewidth v0.0.14 // indirect
76-
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
7772
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
7873
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
7974
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -122,6 +117,5 @@ require (
122117
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
123118
google.golang.org/grpc v1.71.1 // indirect
124119
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
125-
gopkg.in/yaml.v2 v2.4.0 // indirect
126120
gopkg.in/yaml.v3 v3.0.1 // indirect
127121
)

go.sum

Lines changed: 16 additions & 258 deletions
Large diffs are not rendered by default.

pkg/plugin/cloudlogging/client.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,36 @@ func NewClientWithImpersonation(ctx context.Context, jsonCreds []byte, impersona
155155
}, nil
156156
}
157157

158+
// NewClientWithAccessToken creates a new Client using an access token for authentication.
159+
// Since the datasource is re-created whenever the token changes, we can treat this token as static.
160+
func NewClientWithAccessToken(ctx context.Context, accessToken string) (*Client, error) {
161+
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken})
162+
163+
client, err := logging.NewClient(ctx, option.WithTokenSource(ts),
164+
option.WithUserAgent("googlecloud-logging-datasource"))
165+
if err != nil {
166+
return nil, err
167+
}
168+
169+
rClient, err := resourcemanager.NewService(ctx, option.WithTokenSource(ts),
170+
option.WithUserAgent("googlecloud-logging-datasource"))
171+
if err != nil {
172+
return nil, err
173+
}
174+
175+
configClient, err := logging.NewConfigClient(ctx, option.WithTokenSource(ts),
176+
option.WithUserAgent("googlecloud-logging-datasource"))
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
return &Client{
182+
lClient: client,
183+
rClient: rClient.Projects,
184+
configClient: configClient,
185+
}, nil
186+
}
187+
158188
// Close closes the underlying connection to the GCP API
159189
func (c *Client) Close() error {
160190
c.configClient.Close()

pkg/plugin/plugin.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ var (
3838
_ backend.CheckHealthHandler = (*CloudLoggingDatasource)(nil)
3939
_ instancemgmt.InstanceDisposer = (*CloudLoggingDatasource)(nil)
4040
errMissingCredentials = errors.New("missing credentials")
41+
errMissingAccessToken = errors.New("missing access token")
4142
)
4243

4344
const (
44-
privateKeyKey = "privateKey"
45-
gceAuthentication = "gce"
46-
jwtAuthentication = "jwt"
45+
privateKeyKey = "privateKey"
46+
gceAuthentication = "gce"
47+
jwtAuthentication = "jwt"
48+
accessTokenAuthentication = "accessToken"
49+
accessTokenKey = "accessToken"
4750
)
4851

4952
// config is the fields parsed from the front end
@@ -88,10 +91,16 @@ func NewCloudLoggingDatasource(ctx context.Context, settings backend.DataSourceI
8891
conf.AuthType = jwtAuthentication
8992
}
9093

94+
// Check if access token is configured and switch auth type if present
95+
if accessToken, ok := settings.DecryptedSecureJSONData[accessTokenKey]; ok && accessToken != "" {
96+
conf.AuthType = accessTokenAuthentication
97+
}
98+
9199
var client_err error
92100
var client *cloudlogging.Client
93101

94-
if conf.AuthType == jwtAuthentication {
102+
switch conf.AuthType {
103+
case jwtAuthentication:
95104
privateKey, ok := settings.DecryptedSecureJSONData[privateKeyKey]
96105
if !ok || privateKey == "" {
97106
return nil, errMissingCredentials
@@ -107,15 +116,24 @@ func NewCloudLoggingDatasource(ctx context.Context, settings backend.DataSourceI
107116
} else {
108117
client, client_err = cloudlogging.NewClient(context.TODO(), serviceAccount)
109118
}
110-
} else {
119+
case gceAuthentication:
111120
if conf.UsingImpersonation {
112121
client, client_err = cloudlogging.NewClientWithImpersonation(context.TODO(), nil, conf.ServiceAccountToImpersonate)
113122
} else {
114123
client, client_err = cloudlogging.NewClientWithGCE(context.TODO())
115124
}
125+
case accessTokenAuthentication:
126+
accessToken, ok := settings.DecryptedSecureJSONData[accessTokenKey]
127+
if !ok || accessToken == "" {
128+
return nil, errMissingAccessToken
129+
}
130+
client, client_err = cloudlogging.NewClientWithAccessToken(context.TODO(), accessToken)
131+
default:
132+
return nil, fmt.Errorf("unknown authentication type: %s", conf.AuthType)
116133
}
134+
117135
if client_err != nil {
118-
return nil, client_err
136+
return nil, fmt.Errorf("create client: %w", client_err)
119137
}
120138

121139
return &CloudLoggingDatasource{

src/ConfigEditor.tsx

Lines changed: 91 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,99 @@
1616

1717
import React, { PureComponent } from 'react';
1818
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
19-
import { DataSourceSecureJsonData, ConnectionConfig } from '@grafana/google-sdk';
20-
import { Label } from '@grafana/ui';
21-
import { CloudLoggingOptions } from 'types';
19+
import { ConnectionConfig } from '@grafana/google-sdk';
20+
import { Label, SecretInput } from '@grafana/ui';
21+
import { CloudLoggingOptions, DataSourceSecureJsonData } from './types';
2222

2323
export type Props = DataSourcePluginOptionsEditorProps<CloudLoggingOptions, DataSourceSecureJsonData>;
2424

2525
export class ConfigEditor extends PureComponent<Props> {
26-
state = {
27-
isChecked: this.props.options.jsonData.usingImpersonation || false,
28-
sa: this.props.options.jsonData.serviceAccountToImpersonate || '',
29-
};
30-
handleClick = () => {
31-
this.props.options.jsonData.usingImpersonation = !this.state.isChecked;
32-
this.setState({
33-
isChecked: !this.state.isChecked,
34-
});
35-
}
36-
render() {
37-
return (
38-
<>
39-
<ConnectionConfig {...this.props}></ConnectionConfig>
40-
<div>
41-
<input type="checkbox" onChange={this.handleClick} checked={this.state.isChecked} /> To impersonate an existing Google Cloud service account.
42-
<div hidden={!this.state.isChecked}>
43-
<Label>Service Account:</Label>
44-
<input
45-
size={60}
46-
id="serviceAccount"
47-
value={this.state.sa}
48-
onChange={(e) => {
49-
this.setState({ sa: e.target.value },
50-
() => { this.props.options.jsonData.serviceAccountToImpersonate = this.state.sa; });
51-
}}
52-
/>
53-
</div>
54-
</div>
55-
</>
56-
);
57-
}
26+
state = {
27+
isChecked: this.props.options.jsonData.usingImpersonation || false,
28+
sa: this.props.options.jsonData.serviceAccountToImpersonate || '',
29+
};
30+
31+
handleClick = () => {
32+
this.props.options.jsonData.usingImpersonation = !this.state.isChecked;
33+
this.setState({
34+
isChecked: !this.state.isChecked,
35+
});
36+
};
37+
render() {
38+
const { options, onOptionsChange } = this.props;
39+
const secureJsonData = options.secureJsonData || {};
40+
41+
return (
42+
<>
43+
<ConnectionConfig {...this.props}></ConnectionConfig>
44+
<div>
45+
<input type="checkbox" onChange={this.handleClick} checked={this.state.isChecked} /> To impersonate an
46+
existing Google Cloud service account.
47+
<div hidden={!this.state.isChecked}>
48+
<Label>Service Account:</Label>
49+
<input
50+
size={60}
51+
id="serviceAccount"
52+
value={this.state.sa}
53+
onChange={(e) => {
54+
this.setState({ sa: e.target.value }, () => {
55+
this.props.options.jsonData.serviceAccountToImpersonate = this.state.sa;
56+
});
57+
}}
58+
/>
59+
</div>
60+
<div style={{ marginTop: '10px' }}>
61+
<div className="gf-form-label__desc">
62+
Alternatively, configure a temporary access token and a project ID. This will override other
63+
authentication methods.
64+
</div>
65+
<div style={{ marginTop: '10px' }}>
66+
<Label>Access Token</Label>
67+
<SecretInput
68+
value={secureJsonData.accessToken || ''}
69+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
70+
onOptionsChange({
71+
...options,
72+
secureJsonData: {
73+
...secureJsonData,
74+
accessToken: e.target.value,
75+
},
76+
});
77+
}}
78+
isConfigured={!!options.secureJsonFields?.accessToken}
79+
onReset={() => {
80+
onOptionsChange({
81+
...options,
82+
secureJsonData: {
83+
...secureJsonData,
84+
accessToken: '',
85+
},
86+
secureJsonFields: {
87+
...options.secureJsonFields,
88+
accessToken: false,
89+
},
90+
});
91+
}}
92+
/>
93+
</div>
94+
<div style={{ marginTop: '10px' }}>
95+
<Label>Project ID</Label>
96+
<input
97+
value={options.jsonData.defaultProject || ''}
98+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
99+
onOptionsChange({
100+
...options,
101+
jsonData: {
102+
...options.jsonData,
103+
defaultProject: e.target.value,
104+
},
105+
});
106+
}}
107+
/>
108+
</div>
109+
</div>
110+
</div>
111+
</>
112+
);
113+
}
58114
}

src/module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { DataSource } from './datasource';
1919
import { ConfigEditor } from './ConfigEditor';
2020
import { LoggingQueryEditor } from './QueryEditor';
2121
import { CloudLoggingOptions, Query } from './types';
22+
import { DataSourceSecureJsonData } from '@grafana/google-sdk';
2223

23-
export const plugin = new DataSourcePlugin<DataSource, Query, CloudLoggingOptions>(DataSource)
24+
export const plugin = new DataSourcePlugin<DataSource, Query, CloudLoggingOptions, DataSourceSecureJsonData>(DataSource)
2425
.setConfigEditor(ConfigEditor)
2526
.setQueryEditor(LoggingQueryEditor);

src/types.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,47 @@
1515
*/
1616

1717
import { DataQuery, SelectableValue } from '@grafana/data';
18-
import { DataSourceOptions, GoogleAuthType } from '@grafana/google-sdk';
18+
import {
19+
DataSourceOptions,
20+
GoogleAuthType,
21+
DataSourceSecureJsonData as BaseDataSourceSecureJsonData,
22+
} from '@grafana/google-sdk';
23+
24+
export interface DataSourceSecureJsonData extends BaseDataSourceSecureJsonData {
25+
accessToken?: string;
26+
}
1927

2028
export const authTypes: Array<SelectableValue<string>> = [
21-
{ label: 'Google JWT File', value: GoogleAuthType.JWT },
22-
{ label: 'GCE Default Service Account', value: GoogleAuthType.GCE },
29+
{ label: 'Google JWT File', value: GoogleAuthType.JWT },
30+
{ label: 'GCE Default Service Account', value: GoogleAuthType.GCE },
31+
{ label: 'Access Token', value: 'accessToken' },
2332
];
2433

2534
/**
2635
* DataSourceOptionsExt adds two more fields to DataSourceOptions
2736
*/
2837
export interface DataSourceOptionsExt extends DataSourceOptions {
29-
gceDefaultProject?: string;
30-
serviceAccountToImpersonate?: string;
31-
usingImpersonation?: boolean;
38+
gceDefaultProject?: string;
39+
serviceAccountToImpersonate?: string;
40+
usingImpersonation?: boolean;
3241
}
3342

3443
/**
3544
* Query from Grafana
3645
*/
3746
export interface Query extends DataQuery {
38-
queryText?: string;
39-
query?: string;
40-
projectId: string;
41-
bucketId?: string;
42-
viewId?: string;
47+
queryText?: string;
48+
query?: string;
49+
projectId: string;
50+
bucketId?: string;
51+
viewId?: string;
4352
}
4453

4554
/**
4655
* Query that basically gets all logs
4756
*/
4857
export const defaultQuery: Partial<Query> = {
49-
queryText: `severity >= DEFAULT`,
58+
queryText: `severity >= DEFAULT`,
5059
};
5160

5261
/**
@@ -58,29 +67,29 @@ export type CloudLoggingOptions = DataSourceOptionsExt;
5867
* Supported types for template variables
5968
*/
6069
export interface CloudLoggingVariableQuery extends DataQuery {
61-
selectedQueryType: string;
62-
projectId: string;
63-
bucketId?: string;
70+
selectedQueryType: string;
71+
projectId: string;
72+
bucketId?: string;
6473
}
6574

6675
/**
6776
* Enum for logging scopes
6877
*/
6978
export enum LogFindQueryScopes {
70-
Projects = 'projects',
71-
Buckets = 'buckets',
72-
Views = 'views',
79+
Projects = 'projects',
80+
Buckets = 'buckets',
81+
Views = 'views',
7382
}
7483

7584
/**
7685
* Scope data for template variables
7786
*/
7887
export interface VariableScopeData {
79-
selectedQueryType: string;
80-
projects: SelectableValue[];
81-
buckets: SelectableValue[];
82-
bucketId: string;
83-
viewId: string;
84-
projectId: string;
85-
loading: boolean;
88+
selectedQueryType: string;
89+
projects: SelectableValue[];
90+
buckets: SelectableValue[];
91+
bucketId: string;
92+
viewId: string;
93+
projectId: string;
94+
loading: boolean;
8695
}

0 commit comments

Comments
 (0)