Merge pull request #1776 from influxdata/feature/authorization-influx
Update influx Authorization Headers for write and query pathpull/10616/head
commit
040e72b78d
|
@ -7,6 +7,7 @@
|
||||||
1. [#1751](https://github.com/influxdata/chronograf/pull/1751): Fix typo that may have affected PagerDuty node creation in Kapacitor
|
1. [#1751](https://github.com/influxdata/chronograf/pull/1751): Fix typo that may have affected PagerDuty node creation in Kapacitor
|
||||||
1. [#1756](https://github.com/influxdata/chronograf/pull/1756): Prevent 'auto' GROUP BY as option in Kapacitor rule builder when applying a function to a field
|
1. [#1756](https://github.com/influxdata/chronograf/pull/1756): Prevent 'auto' GROUP BY as option in Kapacitor rule builder when applying a function to a field
|
||||||
1. [#1773](https://github.com/influxdata/chronograf/pull/1773): Prevent clipped buttons in Rule Builder, Data Explorer, and Configuration pages
|
1. [#1773](https://github.com/influxdata/chronograf/pull/1773): Prevent clipped buttons in Rule Builder, Data Explorer, and Configuration pages
|
||||||
|
1. [#1776](https://github.com/influxdata/chronograf/pull/1776): Fix JWT for the write path
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
|
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package influx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/influxdata/chronograf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorizer adds optional authorization header to request
|
||||||
|
type Authorizer interface {
|
||||||
|
// Set may manipulate the request by adding the Authorization header
|
||||||
|
Set(req *http.Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoAuthorization does not add any authorization headers
|
||||||
|
type NoAuthorization struct{}
|
||||||
|
|
||||||
|
// Set does not add authorization
|
||||||
|
func (n *NoAuthorization) Set(req *http.Request) error { return nil }
|
||||||
|
|
||||||
|
// DefaultAuthorization creates either a shared JWT builder, basic auth or Noop
|
||||||
|
func DefaultAuthorization(src *chronograf.Source) Authorizer {
|
||||||
|
// Optionally, add the shared secret JWT token creation
|
||||||
|
if src.Username != "" && src.SharedSecret != "" {
|
||||||
|
return &BearerJWT{
|
||||||
|
Username: src.Username,
|
||||||
|
SharedSecret: src.SharedSecret,
|
||||||
|
}
|
||||||
|
} else if src.Username != "" && src.Password != "" {
|
||||||
|
return &BasicAuth{
|
||||||
|
Username: src.Username,
|
||||||
|
Password: src.Password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &NoAuthorization{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicAuth adds Authorization: Basic to the request header
|
||||||
|
type BasicAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds the basic auth headers to the request
|
||||||
|
func (b *BasicAuth) Set(r *http.Request) error {
|
||||||
|
r.SetBasicAuth(b.Username, b.Password)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BearerJWT is the default Bearer for InfluxDB
|
||||||
|
type BearerJWT struct {
|
||||||
|
Username string
|
||||||
|
SharedSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds an Authorization Bearer to the request if has a shared secret
|
||||||
|
func (b *BearerJWT) Set(r *http.Request) error {
|
||||||
|
if b.SharedSecret != "" && b.Username != "" {
|
||||||
|
token, err := b.Token(b.Username)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Unable to create token")
|
||||||
|
}
|
||||||
|
r.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token returns the expected InfluxDB JWT signed with the sharedSecret
|
||||||
|
func (b *BearerJWT) Token(username string) (string, error) {
|
||||||
|
return JWT(username, b.SharedSecret, time.Now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now returns the current time
|
||||||
|
type Now func() time.Time
|
||||||
|
|
||||||
|
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
|
||||||
|
func JWT(username, sharedSecret string, now Now) (string, error) {
|
||||||
|
token := &jwt.Token{
|
||||||
|
Header: map[string]interface{}{
|
||||||
|
"typ": "JWT",
|
||||||
|
"alg": jwt.SigningMethodHS512.Alg(),
|
||||||
|
},
|
||||||
|
Claims: jwt.MapClaims{
|
||||||
|
"username": username,
|
||||||
|
"exp": now().Add(time.Minute).Unix(),
|
||||||
|
},
|
||||||
|
Method: jwt.SigningMethodHS512,
|
||||||
|
}
|
||||||
|
return token.SignedString([]byte(sharedSecret))
|
||||||
|
}
|
|
@ -28,7 +28,7 @@ var (
|
||||||
// Client is a device for retrieving time series data from an InfluxDB instance
|
// Client is a device for retrieving time series data from an InfluxDB instance
|
||||||
type Client struct {
|
type Client struct {
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
Bearer Bearer
|
Authorizer Authorizer
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
}
|
}
|
||||||
|
@ -72,13 +72,11 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err
|
||||||
params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis
|
params.Set("epoch", "ms") // TODO(timraymond): set this based on analysis
|
||||||
req.URL.RawQuery = params.Encode()
|
req.URL.RawQuery = params.Encode()
|
||||||
|
|
||||||
if c.Bearer != nil && u.User != nil {
|
if c.Authorizer != nil {
|
||||||
token, err := c.Bearer.Token(u.User.Username())
|
if err := c.Authorizer.Set(req); err != nil {
|
||||||
if err != nil {
|
logs.Error("Error setting authorization header ", err)
|
||||||
logs.Error("Error creating token", err)
|
return nil, err
|
||||||
return nil, fmt.Errorf("Unable to create token")
|
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hc := &http.Client{}
|
hc := &http.Client{}
|
||||||
|
@ -156,22 +154,13 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.User = url.UserPassword(src.Username, src.Password)
|
c.Authorizer = DefaultAuthorization(src)
|
||||||
// Only allow acceptance of all certs if the scheme is https AND the user opted into to the setting.
|
// Only allow acceptance of all certs if the scheme is https AND the user opted into to the setting.
|
||||||
if u.Scheme == "https" && src.InsecureSkipVerify {
|
if u.Scheme == "https" && src.InsecureSkipVerify {
|
||||||
c.InsecureSkipVerify = src.InsecureSkipVerify
|
c.InsecureSkipVerify = src.InsecureSkipVerify
|
||||||
}
|
}
|
||||||
c.URL = u
|
|
||||||
|
|
||||||
// Optionally, add the shared secret JWT token creation
|
c.URL = u
|
||||||
if src.Username != "" && src.SharedSecret != "" {
|
|
||||||
c.Bearer = &BearerJWT{
|
|
||||||
src.SharedSecret,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Clear out the bearer if not needed
|
|
||||||
c.Bearer = nil
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,38 +62,52 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockBearer struct {
|
type MockAuthorization struct {
|
||||||
Bearer string
|
Bearer string
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockBearer) Token(username string) (string, error) {
|
func (m *MockAuthorization) Set(req *http.Request) error {
|
||||||
return m.Bearer, m.Error
|
return m.Error
|
||||||
}
|
}
|
||||||
func Test_Influx_AuthorizationBearer(t *testing.T) {
|
func Test_Influx_AuthorizationBearer(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
want := "Bearer ********"
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
rw.Write([]byte(`{}`))
|
rw.Write([]byte(`{}`))
|
||||||
got := r.Header.Get("Authorization")
|
auth := r.Header.Get("Authorization")
|
||||||
if got != want {
|
tokenString := strings.Split(auth, " ")[1]
|
||||||
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
|
token, err := gojwt.Parse(tokenString, func(token *gojwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return []byte("42"), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Invalid token %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(gojwt.MapClaims); ok && token.Valid {
|
||||||
|
got := claims["username"]
|
||||||
|
want := "AzureDiamond"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Errorf("Invalid token %v", token)
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
bearer := &MockBearer{
|
src := &chronograf.Source{
|
||||||
Bearer: "********",
|
Username: "AzureDiamond",
|
||||||
|
URL: ts.URL,
|
||||||
|
SharedSecret: "42",
|
||||||
}
|
}
|
||||||
|
|
||||||
u, _ := url.Parse(ts.URL)
|
|
||||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
|
||||||
series := &influx.Client{
|
series := &influx.Client{
|
||||||
URL: u,
|
|
||||||
Bearer: bearer,
|
|
||||||
Logger: log.New(log.DebugLevel),
|
Logger: log.New(log.DebugLevel),
|
||||||
}
|
}
|
||||||
|
series.Connect(context.Background(), src)
|
||||||
|
|
||||||
query := chronograf.Query{
|
query := chronograf.Query{
|
||||||
Command: "show databases",
|
Command: "show databases",
|
||||||
|
@ -161,16 +175,16 @@ func Test_Influx_AuthorizationBearerCtx(t *testing.T) {
|
||||||
|
|
||||||
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
|
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
bearer := &MockBearer{
|
bearer := &MockAuthorization{
|
||||||
Error: fmt.Errorf("cracked1337"),
|
Error: fmt.Errorf("cracked1337"),
|
||||||
}
|
}
|
||||||
|
|
||||||
u, _ := url.Parse("http://haxored.net")
|
u, _ := url.Parse("http://haxored.net")
|
||||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
||||||
series := &influx.Client{
|
series := &influx.Client{
|
||||||
URL: u,
|
URL: u,
|
||||||
Bearer: bearer,
|
Authorizer: bearer,
|
||||||
Logger: log.New(log.DebugLevel),
|
Logger: log.New(log.DebugLevel),
|
||||||
}
|
}
|
||||||
|
|
||||||
query := chronograf.Query{
|
query := chronograf.Query{
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bearer generates tokens for Authorization: Bearer
|
|
||||||
type Bearer interface {
|
|
||||||
Token(username string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BearerJWT is the default Bearer for InfluxDB
|
|
||||||
type BearerJWT struct {
|
|
||||||
SharedSecret string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token returns the expected InfluxDB JWT signed with the sharedSecret
|
|
||||||
func (b *BearerJWT) Token(username string) (string, error) {
|
|
||||||
return JWT(username, b.SharedSecret, time.Now)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now returns the current time
|
|
||||||
type Now func() time.Time
|
|
||||||
|
|
||||||
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
|
|
||||||
func JWT(username, sharedSecret string, now Now) (string, error) {
|
|
||||||
token := &jwt.Token{
|
|
||||||
Header: map[string]interface{}{
|
|
||||||
"typ": "JWT",
|
|
||||||
"alg": jwt.SigningMethodHS512.Alg(),
|
|
||||||
},
|
|
||||||
Claims: jwt.MapClaims{
|
|
||||||
"username": username,
|
|
||||||
"exp": now().Add(time.Minute).Unix(),
|
|
||||||
},
|
|
||||||
Method: jwt.SigningMethodHS512,
|
|
||||||
}
|
|
||||||
return token.SignedString([]byte(sharedSecret))
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/influxdata/chronograf"
|
"github.com/influxdata/chronograf"
|
||||||
|
"github.com/influxdata/chronograf/influx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidInfluxRequest checks if queries specify a command.
|
// ValidInfluxRequest checks if queries specify a command.
|
||||||
|
@ -106,10 +107,9 @@ func (h *Service) Write(w http.ResponseWriter, r *http.Request) {
|
||||||
req.Host = u.Host
|
req.Host = u.Host
|
||||||
req.URL = u
|
req.URL = u
|
||||||
// Because we are acting as a proxy, influxdb needs to have the
|
// Because we are acting as a proxy, influxdb needs to have the
|
||||||
// basic auth information set as a header directly
|
// basic auth or bearer token information set as a header directly
|
||||||
if src.Username != "" && src.Password != "" {
|
auth := influx.DefaultAuthorization(&src)
|
||||||
req.SetBasicAuth(src.Username, src.Password)
|
auth.Set(req)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
proxy := &httputil.ReverseProxy{
|
proxy := &httputil.ReverseProxy{
|
||||||
Director: director,
|
Director: director,
|
||||||
|
|
Loading…
Reference in New Issue