Merge pull request #1776 from influxdata/feature/authorization-influx

Update influx Authorization Headers for write and query path
pull/10616/head
Nathan Haugo 2017-07-25 12:14:28 -05:00 committed by GitHub
commit 040e72b78d
7 changed files with 137 additions and 81 deletions

View File

@ -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. [#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. [#1776](https://github.com/influxdata/chronograf/pull/1776): Fix JWT for the write path
### Features
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts

93
influx/authorization.go Normal file
View File

@ -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))
}

View File

@ -28,7 +28,7 @@ var (
// Client is a device for retrieving time series data from an InfluxDB instance
type Client struct {
URL *url.URL
Bearer Bearer
Authorizer Authorizer
InsecureSkipVerify bool
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
req.URL.RawQuery = params.Encode()
if c.Bearer != nil && u.User != nil {
token, err := c.Bearer.Token(u.User.Username())
if err != nil {
logs.Error("Error creating token", err)
return nil, fmt.Errorf("Unable to create token")
if c.Authorizer != nil {
if err := c.Authorizer.Set(req); err != nil {
logs.Error("Error setting authorization header ", err)
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
}
hc := &http.Client{}
@ -156,22 +154,13 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
if err != nil {
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.
if u.Scheme == "https" && src.InsecureSkipVerify {
c.InsecureSkipVerify = src.InsecureSkipVerify
}
c.URL = u
// Optionally, add the shared secret JWT token creation
if src.Username != "" && src.SharedSecret != "" {
c.Bearer = &BearerJWT{
src.SharedSecret,
}
} else {
// Clear out the bearer if not needed
c.Bearer = nil
}
c.URL = u
return nil
}

View File

@ -62,38 +62,52 @@ func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
}
}
type MockBearer struct {
type MockAuthorization struct {
Bearer string
Error error
}
func (m *MockBearer) Token(username string) (string, error) {
return m.Bearer, m.Error
func (m *MockAuthorization) Set(req *http.Request) error {
return m.Error
}
func Test_Influx_AuthorizationBearer(t *testing.T) {
t.Parallel()
want := "Bearer ********"
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{}`))
got := r.Header.Get("Authorization")
if got != want {
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
auth := r.Header.Get("Authorization")
tokenString := strings.Split(auth, " ")[1]
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()
bearer := &MockBearer{
Bearer: "********",
src := &chronograf.Source{
Username: "AzureDiamond",
URL: ts.URL,
SharedSecret: "42",
}
u, _ := url.Parse(ts.URL)
u.User = url.UserPassword("AzureDiamond", "hunter2")
series := &influx.Client{
URL: u,
Bearer: bearer,
Logger: log.New(log.DebugLevel),
}
series.Connect(context.Background(), src)
query := chronograf.Query{
Command: "show databases",
@ -161,16 +175,16 @@ func Test_Influx_AuthorizationBearerCtx(t *testing.T) {
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
t.Parallel()
bearer := &MockBearer{
bearer := &MockAuthorization{
Error: fmt.Errorf("cracked1337"),
}
u, _ := url.Parse("http://haxored.net")
u.User = url.UserPassword("AzureDiamond", "hunter2")
series := &influx.Client{
URL: u,
Bearer: bearer,
Logger: log.New(log.DebugLevel),
URL: u,
Authorizer: bearer,
Logger: log.New(log.DebugLevel),
}
query := chronograf.Query{

View File

@ -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))
}

View File

@ -8,6 +8,7 @@ import (
"net/url"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
)
// 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.URL = u
// Because we are acting as a proxy, influxdb needs to have the
// basic auth information set as a header directly
if src.Username != "" && src.Password != "" {
req.SetBasicAuth(src.Username, src.Password)
}
// basic auth or bearer token information set as a header directly
auth := influx.DefaultAuthorization(&src)
auth.Set(req)
}
proxy := &httputil.ReverseProxy{
Director: director,