Skip to content

Commit d8ed43d

Browse files
committed
Support X-Access-Token for Grafana Cloud
In Grafana Cloud it is possible to have a plugin submit requests on behalf of the calling user by setting the X-Access-Token header instead of the standard Authorization header. This PR will preferentially look for the presence of an access token and use that before falling back to an API key like normal.
1 parent 0beb805 commit d8ed43d

File tree

6 files changed

+70
-20
lines changed

6 files changed

+70
-20
lines changed

mcpgrafana.go

+16
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func urlAndAPIKeyFromHeaders(req *http.Request) (string, string) {
4040

4141
type grafanaURLKey struct{}
4242
type grafanaAPIKeyKey struct{}
43+
type grafanaAccessTokenKey struct{}
4344

4445
// grafanaDebugKey is the context key for the Grafana transport's debug flag.
4546
type grafanaDebugKey struct{}
@@ -103,6 +104,12 @@ func WithGrafanaAPIKey(ctx context.Context, apiKey string) context.Context {
103104
return context.WithValue(ctx, grafanaAPIKeyKey{}, apiKey)
104105
}
105106

107+
// WithGrafanaAccessToken adds the Grafana access token to the context. An
108+
// access token is used for on-behalf-of auth in Grafana Cloud.
109+
func WithGrafanaAccessToken(ctx context.Context, accessToken string) context.Context {
110+
return context.WithValue(ctx, grafanaAccessTokenKey{}, accessToken)
111+
}
112+
106113
// GrafanaURLFromContext extracts the Grafana URL from the context.
107114
func GrafanaURLFromContext(ctx context.Context) string {
108115
if u, ok := ctx.Value(grafanaURLKey{}).(string); ok {
@@ -119,6 +126,15 @@ func GrafanaAPIKeyFromContext(ctx context.Context) string {
119126
return ""
120127
}
121128

129+
// GrafanaAccessTokenFromContext extracts a Grafana access token from the context.
130+
// An access token is used for on-behalf-of auth in Grafana Cloud.
131+
func GrafanaAccessTokenFromContext(ctx context.Context) string {
132+
if k, ok := ctx.Value(grafanaAccessTokenKey{}).(string); ok {
133+
return k
134+
}
135+
return ""
136+
}
137+
122138
type grafanaClientKey struct{}
123139

124140
func makeBasePath(path string) string {

tools/alerting_client.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ const (
2121
)
2222

2323
type alertingClient struct {
24-
baseURL *url.URL
25-
apiKey string
26-
httpClient *http.Client
24+
baseURL *url.URL
25+
accessToken string
26+
apiKey string
27+
httpClient *http.Client
2728
}
2829

2930
func newAlertingClientFromContext(ctx context.Context) (*alertingClient, error) {
@@ -34,8 +35,9 @@ func newAlertingClientFromContext(ctx context.Context) (*alertingClient, error)
3435
}
3536

3637
return &alertingClient{
37-
baseURL: parsedBaseURL,
38-
apiKey: mcpgrafana.GrafanaAPIKeyFromContext(ctx),
38+
baseURL: parsedBaseURL,
39+
accessToken: mcpgrafana.GrafanaAccessTokenFromContext(ctx),
40+
apiKey: mcpgrafana.GrafanaAPIKeyFromContext(ctx),
3941
httpClient: &http.Client{
4042
Timeout: defaultTimeout,
4143
},
@@ -53,7 +55,10 @@ func (c *alertingClient) makeRequest(ctx context.Context, path string) (*http.Re
5355
req.Header.Set("Accept", "application/json")
5456
req.Header.Set("Content-Type", "application/json")
5557

56-
if c.apiKey != "" {
58+
// If accessToken is set we use that first and fall back to normal Authorization.
59+
if c.accessToken != "" {
60+
req.Header.Set("X-Access-Token", c.accessToken)
61+
} else if c.apiKey != "" {
5762
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
5863
}
5964

tools/loki.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,18 @@ func newLokiClient(ctx context.Context, uid string) (*Client, error) {
5050
return nil, err
5151
}
5252

53-
grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
53+
var (
54+
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
55+
apiKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
56+
accessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
57+
)
5458
url := fmt.Sprintf("%s/api/datasources/proxy/uid/%s", strings.TrimRight(grafanaURL, "/"), uid)
5559

5660
client := &http.Client{
5761
Transport: &authRoundTripper{
58-
apiKey: apiKey,
59-
underlying: http.DefaultTransport,
62+
accessToken: accessToken,
63+
apiKey: apiKey,
64+
underlying: http.DefaultTransport,
6065
},
6166
}
6267

@@ -163,12 +168,15 @@ func (c *Client) fetchData(ctx context.Context, urlPath string, startRFC3339, en
163168
}
164169

165170
type authRoundTripper struct {
166-
apiKey string
167-
underlying http.RoundTripper
171+
accessToken string
172+
apiKey string
173+
underlying http.RoundTripper
168174
}
169175

170176
func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
171-
if rt.apiKey != "" {
177+
if rt.accessToken != "" {
178+
req.Header.Set("X-Access-Token", rt.accessToken)
179+
} else if rt.apiKey != "" {
172180
req.Header.Set("Authorization", "Bearer "+rt.apiKey)
173181
}
174182

tools/oncall.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ func getOnCallURLFromSettings(ctx context.Context, grafanaURL, grafanaAPIKey str
5757

5858
func oncallClientFromContext(ctx context.Context) (*aapi.Client, error) {
5959
// Get the standard Grafana URL and API key
60-
grafanaURL, grafanaAPIKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
60+
var (
61+
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
62+
grafanaAPIKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
63+
)
6164

6265
// Try to get OnCall URL from settings endpoint
6366
grafanaOnCallURL, err := getOnCallURLFromSettings(ctx, grafanaURL, grafanaAPIKey)
@@ -67,6 +70,7 @@ func oncallClientFromContext(ctx context.Context) (*aapi.Client, error) {
6770

6871
grafanaOnCallURL = strings.TrimRight(grafanaOnCallURL, "/")
6972

73+
// TODO: Allow access to OnCall using an access token instead of an API key.
7074
client, err := aapi.NewWithGrafanaURL(grafanaOnCallURL, grafanaAPIKey, grafanaURL)
7175
if err != nil {
7276
return nil, fmt.Errorf("creating OnCall client: %w", err)

tools/prometheus.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,22 @@ func promClientFromContext(ctx context.Context, uid string) (promv1.API, error)
3333
return nil, err
3434
}
3535

36-
grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
36+
var (
37+
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
38+
apiKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
39+
accessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
40+
)
3741
url := fmt.Sprintf("%s/api/datasources/proxy/uid/%s", strings.TrimRight(grafanaURL, "/"), uid)
3842
rt := api.DefaultRoundTripper
39-
if apiKey != "" {
43+
if accessToken != "" {
44+
rt = config.NewHeadersRoundTripper(&config.Headers{
45+
Headers: map[string]config.Header{
46+
"X-Access-Token": config.Header{
47+
Secrets: []config.Secret{config.Secret(accessToken)},
48+
},
49+
},
50+
}, rt)
51+
} else if apiKey != "" {
4052
rt = config.NewAuthorizationCredentialsRoundTripper(
4153
"Bearer", config.NewInlineSecret(apiKey), rt,
4254
)

tools/sift.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,12 @@ type siftClient struct {
9797
url string
9898
}
9999

100-
func newSiftClient(url, apiKey string) *siftClient {
100+
func newSiftClient(url, accessToken, apiKey string) *siftClient {
101101
client := &http.Client{
102102
Transport: &authRoundTripper{
103-
apiKey: apiKey,
104-
underlying: http.DefaultTransport,
103+
accessToken: accessToken,
104+
apiKey: apiKey,
105+
underlying: http.DefaultTransport,
105106
},
106107
}
107108
return &siftClient{
@@ -112,9 +113,13 @@ func newSiftClient(url, apiKey string) *siftClient {
112113

113114
func siftClientFromContext(ctx context.Context) (*siftClient, error) {
114115
// Get the standard Grafana URL and API key
115-
grafanaURL, grafanaAPIKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
116+
var (
117+
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
118+
grafanaAPIKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
119+
grafanaAccessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
120+
)
116121

117-
client := newSiftClient(grafanaURL, grafanaAPIKey)
122+
client := newSiftClient(grafanaURL, grafanaAccessToken, grafanaAPIKey)
118123

119124
return client, nil
120125
}

0 commit comments

Comments
 (0)