Skip to content
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

Introduce interface to ease forward/access HTTP headers #562

Merged
merged 8 commits into from Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 43 additions & 2 deletions backend/data.go
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"

"github.com/grafana/grafana-plugin-sdk-go/data"
Expand Down Expand Up @@ -37,9 +39,43 @@ func (fn QueryDataHandlerFunc) QueryData(ctx context.Context, req *QueryDataRequ
// QueryDataRequest contains a single request which contains multiple queries.
// It is the input type for a QueryData call.
type QueryDataRequest struct {
// PluginContext the contextual information for the request.
PluginContext PluginContext
Headers map[string]string
Queries []DataQuery

// Headers the environment/metadata information for the request.
//
// To access forwarded HTTP headers please use
// GetHTTPHeaders or GetHTTPHeader.
Headers map[string]string

// Queries the data queries for the request.
Queries []DataQuery
}

// SetHTTPHeader sets the header entries associated with key to the
// single element value. It replaces any existing values
// associated with key. The key is case insensitive; it is
// canonicalized by textproto.CanonicalMIMEHeaderKey.
func (req *QueryDataRequest) SetHTTPHeader(key, value string) {
if req.Headers == nil {
req.Headers = map[string]string{}
}

req.Headers[fmt.Sprintf("%s%s", httpHeaderPrefix, key)] = value
}

// GetHTTPHeader gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. Get assumes that all
// keys are stored in canonical form.
func (req *QueryDataRequest) GetHTTPHeader(key string) string {
return req.GetHTTPHeaders().Get(key)
}

// GetHTTPHeaders returns HTTP headers.
func (req *QueryDataRequest) GetHTTPHeaders() http.Header {
return getHTTPHeadersFromStringMap(req.Headers)
}

// DataQuery represents a single query as sent from the frontend.
Expand Down Expand Up @@ -99,12 +135,14 @@ func NewQueryDataResponse() *QueryDataResponse {
// Responses is a map of RefIDs (Unique Query ID) to DataResponses.
// The QueryData method the QueryDataHandler method will set the RefId
// property on the DataResponses' frames based on these RefIDs.
//
//swagger:model
type Responses map[string]DataResponse

// DataResponse contains the results from a DataQuery.
// A map of RefIDs (unique query identifiers) to this type makes up the Responses property of a QueryDataResponse.
// The Error property is used to allow for partial success responses from the containing QueryDataResponse.
//
//swagger:model
type DataResponse struct {
// The data returned from the Query. Each Frame repeats the RefID.
Expand All @@ -117,6 +155,7 @@ type DataResponse struct {
Status Status
}

// ErrDataResponse returns an error DataResponse given status and message.
func ErrDataResponse(status Status, message string) DataResponse {
return DataResponse{
Error: errors.New(message),
Expand Down Expand Up @@ -147,3 +186,5 @@ type TimeRange struct {
func (tr TimeRange) Duration() time.Duration {
return tr.To.Sub(tr.From)
}

var _ ForwardHTTPHeaders = (*QueryDataRequest)(nil)
81 changes: 81 additions & 0 deletions backend/data_test.go
@@ -0,0 +1,81 @@
package backend

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestQueryDataRequest(t *testing.T) {
req := &QueryDataRequest{}
const customHeaderName = "X-Custom"

t.Run("Legacy headers", func(t *testing.T) {
req.Headers = map[string]string{
"Authorization": "a",
"X-ID-Token": "b",
"Cookies": "c",
customHeaderName: "d",
}

t.Run("GetHTTPHeaders canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", headers.Get(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", headers.Get(CookiesHeaderName))
require.Empty(t, headers.Get(customHeaderName))
})

t.Run("GetHTTPHeader canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", req.GetHTTPHeader(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", req.GetHTTPHeader(CookiesHeaderName))
require.Empty(t, req.GetHTTPHeader(customHeaderName))
})
})

t.Run("SetHTTPHeader canonical form", func(t *testing.T) {
req.SetHTTPHeader(OAuthIdentityTokenHeaderName, "a")
req.SetHTTPHeader(OAuthIdentityIDTokenHeaderName, "b")
req.SetHTTPHeader(CookiesHeaderName, "c")
req.SetHTTPHeader(customHeaderName, "d")

t.Run("GetHTTPHeaders canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", headers.Get(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", headers.Get(CookiesHeaderName))
require.Equal(t, "d", headers.Get(customHeaderName))
})

t.Run("GetHTTPHeader canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", req.GetHTTPHeader(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", req.GetHTTPHeader(CookiesHeaderName))
require.Equal(t, "d", req.GetHTTPHeader(customHeaderName))
})
})

t.Run("SetHTTPHeader non-canonical form", func(t *testing.T) {
req.SetHTTPHeader(strings.ToLower(OAuthIdentityTokenHeaderName), "a")
req.SetHTTPHeader(strings.ToLower(OAuthIdentityIDTokenHeaderName), "b")
req.SetHTTPHeader(strings.ToLower(CookiesHeaderName), "c")
req.SetHTTPHeader(strings.ToLower(customHeaderName), "d")

t.Run("GetHTTPHeaders non-canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(strings.ToLower(OAuthIdentityTokenHeaderName)))
require.Equal(t, "b", headers.Get(strings.ToLower(OAuthIdentityIDTokenHeaderName)))
require.Equal(t, "c", headers.Get(strings.ToLower(CookiesHeaderName)))
require.Equal(t, "d", headers.Get(strings.ToLower(customHeaderName)))
})

t.Run("GetHTTPHeader non-canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(strings.ToLower(OAuthIdentityTokenHeaderName)))
require.Equal(t, "b", req.GetHTTPHeader(strings.ToLower(OAuthIdentityIDTokenHeaderName)))
require.Equal(t, "c", req.GetHTTPHeader(strings.ToLower(CookiesHeaderName)))
require.Equal(t, "d", req.GetHTTPHeader(strings.ToLower(customHeaderName)))
})
})
}
49 changes: 46 additions & 3 deletions backend/diagnostics.go
Expand Up @@ -2,6 +2,8 @@ package backend

import (
"context"
"fmt"
"net/http"
"strconv"
)

Expand Down Expand Up @@ -53,14 +55,51 @@ func (hs HealthStatus) String() string {

// CheckHealthRequest contains the healthcheck request
type CheckHealthRequest struct {
// PluginContext the contextual information for the request.
PluginContext PluginContext
Headers map[string]string

// Headers the environment/metadata information for the request.
//
// To access forwarded HTTP headers please use
// GetHTTPHeaders or GetHTTPHeader.
Headers map[string]string
}

// SetHTTPHeader sets the header entries associated with key to the
// single element value. It replaces any existing values
// associated with key. The key is case insensitive; it is
// canonicalized by textproto.CanonicalMIMEHeaderKey.
func (req *CheckHealthRequest) SetHTTPHeader(key, value string) {
if req.Headers == nil {
req.Headers = map[string]string{}
}

req.Headers[fmt.Sprintf("%s%s", httpHeaderPrefix, key)] = value
}

// GetHTTPHeader gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. Get assumes that all
// keys are stored in canonical form.
func (req *CheckHealthRequest) GetHTTPHeader(key string) string {
return req.GetHTTPHeaders().Get(key)
}

// GetHTTPHeaders returns HTTP headers.
func (req *CheckHealthRequest) GetHTTPHeaders() http.Header {
return getHTTPHeadersFromStringMap(req.Headers)
}

// CheckHealthResult contains the healthcheck response
type CheckHealthResult struct {
Status HealthStatus
Message string
// Status the HealthStatus of the healthcheck.
Status HealthStatus

// Message the message of the healthcheck, if any.
Message string

// JSONDetails the details of the healthcheck, if any, encoded as JSON bytes.
JSONDetails []byte
}

Expand All @@ -82,10 +121,14 @@ func (fn CollectMetricsHandlerFunc) CollectMetrics(ctx context.Context, req *Col

// CollectMetricsRequest contains the metrics request
type CollectMetricsRequest struct {
// PluginContext the contextual information for the request.
PluginContext PluginContext
}

// CollectMetricsResult collect metrics result.
type CollectMetricsResult struct {
// PrometheusMetrics the Prometheus metrics encoded as bytes.
PrometheusMetrics []byte
}

var _ ForwardHTTPHeaders = (*CheckHealthRequest)(nil)
81 changes: 81 additions & 0 deletions backend/diagnostics_test.go
@@ -0,0 +1,81 @@
package backend

import (
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestCheckHealthRequest(t *testing.T) {
req := &CheckHealthRequest{}
const customHeaderName = "X-Custom"

t.Run("Legacy headers", func(t *testing.T) {
req.Headers = map[string]string{
"Authorization": "a",
"X-ID-Token": "b",
"Cookies": "c",
customHeaderName: "d",
}

t.Run("GetHTTPHeaders canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", headers.Get(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", headers.Get(CookiesHeaderName))
require.Empty(t, headers.Get(customHeaderName))
})

t.Run("GetHTTPHeader canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", req.GetHTTPHeader(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", req.GetHTTPHeader(CookiesHeaderName))
require.Empty(t, req.GetHTTPHeader(customHeaderName))
})
})

t.Run("SetHTTPHeader canonical form", func(t *testing.T) {
req.SetHTTPHeader(OAuthIdentityTokenHeaderName, "a")
req.SetHTTPHeader(OAuthIdentityIDTokenHeaderName, "b")
req.SetHTTPHeader(CookiesHeaderName, "c")
req.SetHTTPHeader(customHeaderName, "d")

t.Run("GetHTTPHeaders canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", headers.Get(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", headers.Get(CookiesHeaderName))
require.Equal(t, "d", headers.Get(customHeaderName))
})

t.Run("GetHTTPHeader canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(OAuthIdentityTokenHeaderName))
require.Equal(t, "b", req.GetHTTPHeader(OAuthIdentityIDTokenHeaderName))
require.Equal(t, "c", req.GetHTTPHeader(CookiesHeaderName))
require.Equal(t, "d", req.GetHTTPHeader(customHeaderName))
})
})

t.Run("SetHTTPHeader non-canonical form", func(t *testing.T) {
req.SetHTTPHeader(strings.ToLower(OAuthIdentityTokenHeaderName), "a")
req.SetHTTPHeader(strings.ToLower(OAuthIdentityIDTokenHeaderName), "b")
req.SetHTTPHeader(strings.ToLower(CookiesHeaderName), "c")
req.SetHTTPHeader(strings.ToLower(customHeaderName), "d")

t.Run("GetHTTPHeaders non-canonical form", func(t *testing.T) {
headers := req.GetHTTPHeaders()
require.Equal(t, "a", headers.Get(strings.ToLower(OAuthIdentityTokenHeaderName)))
require.Equal(t, "b", headers.Get(strings.ToLower(OAuthIdentityIDTokenHeaderName)))
require.Equal(t, "c", headers.Get(strings.ToLower(CookiesHeaderName)))
require.Equal(t, "d", headers.Get(strings.ToLower(customHeaderName)))
})

t.Run("GetHTTPHeader non-canonical form", func(t *testing.T) {
require.Equal(t, "a", req.GetHTTPHeader(strings.ToLower(OAuthIdentityTokenHeaderName)))
require.Equal(t, "b", req.GetHTTPHeader(strings.ToLower(OAuthIdentityIDTokenHeaderName)))
require.Equal(t, "c", req.GetHTTPHeader(strings.ToLower(CookiesHeaderName)))
require.Equal(t, "d", req.GetHTTPHeader(strings.ToLower(customHeaderName)))
})
})
}
67 changes: 67 additions & 0 deletions backend/http_headers.go
@@ -0,0 +1,67 @@
package backend

import (
"net/http"
"net/textproto"
"strings"
)

const (
// OAuthIdentityTokenHeaderName the header name used for forwarding
// OAuth Identity access token.
OAuthIdentityTokenHeaderName = "Authorization"

// OAuthIdentityIDTokenHeaderName the header name used for forwarding
// OAuth Identity ID token.
OAuthIdentityIDTokenHeaderName = "X-Id-Token"

// CookiesHeaderName the header name used for forwarding
// cookies.
CookiesHeaderName = "Cookies"

httpHeaderPrefix = "http_"
)

// ForwardHTTPHeaders interface marking that forward of HTTP headers is supported.
type ForwardHTTPHeaders interface {
// SetHTTPHeader sets the header entries associated with key to the
// single element value. It replaces any existing values
// associated with key. The key is case insensitive; it is
// canonicalized by textproto.CanonicalMIMEHeaderKey.
SetHTTPHeader(key, value string)

// GetHTTPHeader gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. Get assumes that all
// keys are stored in canonical form.
GetHTTPHeader(key string) string

// GetHTTPHeaders returns HTTP headers.
GetHTTPHeaders() http.Header
}

func getHTTPHeadersFromStringMap(headers map[string]string) http.Header {
httpHeaders := http.Header{}

for k, v := range headers {
if textproto.CanonicalMIMEHeaderKey(k) == OAuthIdentityTokenHeaderName {
httpHeaders.Set(k, v)
}

if textproto.CanonicalMIMEHeaderKey(k) == OAuthIdentityIDTokenHeaderName {
httpHeaders.Set(k, v)
}

if textproto.CanonicalMIMEHeaderKey(k) == CookiesHeaderName {
httpHeaders.Set(k, v)
}

if strings.HasPrefix(k, httpHeaderPrefix) {
hKey := strings.TrimPrefix(k, httpHeaderPrefix)
httpHeaders.Set(hKey, v)
}
}

return httpHeaders
}