Skip to content

Support X-Access-Token for Grafana Cloud #107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
16 changes: 16 additions & 0 deletions mcpgrafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func urlAndAPIKeyFromHeaders(req *http.Request) (string, string) {

type grafanaURLKey struct{}
type grafanaAPIKeyKey struct{}
type grafanaAccessTokenKey struct{}

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

// WithGrafanaAccessToken adds the Grafana access token to the context. An
// access token is used for on-behalf-of auth in Grafana Cloud.
func WithGrafanaAccessToken(ctx context.Context, accessToken string) context.Context {
return context.WithValue(ctx, grafanaAccessTokenKey{}, accessToken)
}

// GrafanaURLFromContext extracts the Grafana URL from the context.
func GrafanaURLFromContext(ctx context.Context) string {
if u, ok := ctx.Value(grafanaURLKey{}).(string); ok {
Expand All @@ -119,6 +126,15 @@ func GrafanaAPIKeyFromContext(ctx context.Context) string {
return ""
}

// GrafanaAccessTokenFromContext extracts a Grafana access token from the context.
// An access token is used for on-behalf-of auth in Grafana Cloud.
func GrafanaAccessTokenFromContext(ctx context.Context) string {
if k, ok := ctx.Value(grafanaAccessTokenKey{}).(string); ok {
return k
}
return ""
}

type grafanaClientKey struct{}

func makeBasePath(path string) string {
Expand Down
17 changes: 11 additions & 6 deletions tools/alerting_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ const (
)

type alertingClient struct {
baseURL *url.URL
apiKey string
httpClient *http.Client
baseURL *url.URL
accessToken string
apiKey string
httpClient *http.Client
}

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

return &alertingClient{
baseURL: parsedBaseURL,
apiKey: mcpgrafana.GrafanaAPIKeyFromContext(ctx),
baseURL: parsedBaseURL,
accessToken: mcpgrafana.GrafanaAccessTokenFromContext(ctx),
apiKey: mcpgrafana.GrafanaAPIKeyFromContext(ctx),
httpClient: &http.Client{
Timeout: defaultTimeout,
},
Expand All @@ -53,7 +55,10 @@ func (c *alertingClient) makeRequest(ctx context.Context, path string) (*http.Re
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")

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

Expand Down
20 changes: 14 additions & 6 deletions tools/loki.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,18 @@ func newLokiClient(ctx context.Context, uid string) (*Client, error) {
return nil, err
}

grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
var (
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
apiKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
accessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
)
url := fmt.Sprintf("%s/api/datasources/proxy/uid/%s", strings.TrimRight(grafanaURL, "/"), uid)

client := &http.Client{
Transport: &authRoundTripper{
apiKey: apiKey,
underlying: http.DefaultTransport,
accessToken: accessToken,
apiKey: apiKey,
underlying: http.DefaultTransport,
},
}

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

type authRoundTripper struct {
apiKey string
underlying http.RoundTripper
accessToken string
apiKey string
underlying http.RoundTripper
}

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

Expand Down
6 changes: 5 additions & 1 deletion tools/oncall.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ func getOnCallURLFromSettings(ctx context.Context, grafanaURL, grafanaAPIKey str

func oncallClientFromContext(ctx context.Context) (*aapi.Client, error) {
// Get the standard Grafana URL and API key
grafanaURL, grafanaAPIKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
var (
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
grafanaAPIKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
)

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

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

// TODO: Allow access to OnCall using an access token instead of an API key.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This TODO will wait for another PR as the OnCall client needs to be upgraded to take a transport. In the meantime we may just need to disable these tools when we use an access token.

Copy link
Contributor

Choose a reason for hiding this comment

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

@sd2k mentioned there are additional complications apart from just the OnCall client upgrade (something about the way routing is set up in cloud IIRC) but yeah that's non-blocking for this PR.

client, err := aapi.NewWithGrafanaURL(grafanaOnCallURL, grafanaAPIKey, grafanaURL)
if err != nil {
return nil, fmt.Errorf("creating OnCall client: %w", err)
Expand Down
16 changes: 14 additions & 2 deletions tools/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,22 @@ func promClientFromContext(ctx context.Context, uid string) (promv1.API, error)
return nil, err
}

grafanaURL, apiKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
var (
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
apiKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
accessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
)
url := fmt.Sprintf("%s/api/datasources/proxy/uid/%s", strings.TrimRight(grafanaURL, "/"), uid)
rt := api.DefaultRoundTripper
if apiKey != "" {
if accessToken != "" {
rt = config.NewHeadersRoundTripper(&config.Headers{
Headers: map[string]config.Header{
"X-Access-Token": config.Header{
Secrets: []config.Secret{config.Secret(accessToken)},
},
},
}, rt)
} else if apiKey != "" {
rt = config.NewAuthorizationCredentialsRoundTripper(
"Bearer", config.NewInlineSecret(apiKey), rt,
)
Expand Down
15 changes: 10 additions & 5 deletions tools/sift.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ type siftClient struct {
url string
}

func newSiftClient(url, apiKey string) *siftClient {
func newSiftClient(url, accessToken, apiKey string) *siftClient {
client := &http.Client{
Transport: &authRoundTripper{
Copy link
Contributor

Choose a reason for hiding this comment

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

we should move this authRoundTripper out into a common location in the future

apiKey: apiKey,
underlying: http.DefaultTransport,
accessToken: accessToken,
apiKey: apiKey,
underlying: http.DefaultTransport,
},
}
return &siftClient{
Expand All @@ -112,9 +113,13 @@ func newSiftClient(url, apiKey string) *siftClient {

func siftClientFromContext(ctx context.Context) (*siftClient, error) {
// Get the standard Grafana URL and API key
grafanaURL, grafanaAPIKey := mcpgrafana.GrafanaURLFromContext(ctx), mcpgrafana.GrafanaAPIKeyFromContext(ctx)
var (
grafanaURL = mcpgrafana.GrafanaURLFromContext(ctx)
grafanaAPIKey = mcpgrafana.GrafanaAPIKeyFromContext(ctx)
grafanaAccessToken = mcpgrafana.GrafanaAccessTokenFromContext(ctx)
)

client := newSiftClient(grafanaURL, grafanaAPIKey)
client := newSiftClient(grafanaURL, grafanaAccessToken, grafanaAPIKey)

return client, nil
}
Expand Down