Merge branch 'master' into feature/template-variables
commit
5538cfa963
|
@ -1,8 +1,15 @@
|
|||
## v1.2.0 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
1. [#1257](https://github.com/influxdata/chronograf/issues/1257): Fix function selection in query builder
|
||||
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix env var name for Google client secret
|
||||
1. [#1269](https://github.com/influxdata/chronograf/issues/1269): Add more functionality to query config generation
|
||||
|
||||
### Features
|
||||
1. [#1292](https://github.com/influxdata/chronograf/pull/1292): Introduce Template Variable Manager
|
||||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
||||
|
||||
|
||||
### UI Improvements
|
||||
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add default display for empty dashboard
|
||||
|
@ -33,7 +40,6 @@
|
|||
1. [#1209](https://github.com/influxdata/chronograf/pull/1209): Ask for the HipChat subdomain instead of the entire HipChat URL in the HipChat Kapacitor configuration
|
||||
1. [#1223](https://github.com/influxdata/chronograf/pull/1223): Use vhost as Chronograf's proxy to Kapacitor
|
||||
1. [#1205](https://github.com/influxdata/chronograf/pull/1205): Allow initial source to be an InfluxEnterprise source
|
||||
1. [#1244](https://github.com/influxdata/chronograf/pull/1244): Fix env var name for Google client secret
|
||||
|
||||
### Features
|
||||
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
|
||||
|
@ -47,7 +53,6 @@
|
|||
1. [#1212](https://github.com/influxdata/chronograf/pull/1212): Add meta query templates and loading animation to the RawQueryEditor
|
||||
1. [#1221](https://github.com/influxdata/chronograf/pull/1221): Remove the default query from empty cells on dashboards
|
||||
1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip
|
||||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||
|
||||
### UI Improvements
|
||||
1. [#1132](https://github.com/influxdata/chronograf/pull/1132): Show blue strip next to active tab on the sidebar
|
||||
|
|
2
Makefile
2
Makefile
|
@ -29,7 +29,7 @@ define CHRONOGIRAFFE
|
|||
,"" _\_
|
||||
," ## | 0 0.
|
||||
," ## ,-\__ `.
|
||||
," / `--._;)
|
||||
," / `--._;) - "HAI, I'm Chronogiraffe. Will you be my friend?"
|
||||
," ## /
|
||||
," ## /
|
||||
endef
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package influx
|
||||
|
||||
import "strings"
|
||||
|
||||
// TemplateValue is a value use to replace a template in an InfluxQL query
|
||||
type TemplateValue struct {
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// TemplateVar is a named variable within an InfluxQL query to be replaced with Values
|
||||
type TemplateVar struct {
|
||||
Var string `json:"tempVar"`
|
||||
Values []TemplateValue `json:"values"`
|
||||
}
|
||||
|
||||
// String converts the template variable into a correct InfluxQL string based
|
||||
// on its type
|
||||
func (t TemplateVar) String() string {
|
||||
if len(t.Values) == 0 {
|
||||
return ""
|
||||
}
|
||||
switch t.Values[0].Type {
|
||||
case "tagKey", "fieldKey":
|
||||
return `"` + t.Values[0].Value + `"`
|
||||
case "tagValue":
|
||||
return `'` + t.Values[0].Value + `'`
|
||||
case "csv":
|
||||
return t.Values[0].Value
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateVars are template variables to replace within an InfluxQL query
|
||||
type TemplateVars struct {
|
||||
Vars []TemplateVar `json:"tempVars"`
|
||||
}
|
||||
|
||||
// TemplateReplace replaces templates with values within the query string
|
||||
func TemplateReplace(query string, templates TemplateVars) string {
|
||||
replacements := []string{}
|
||||
for _, v := range templates.Vars {
|
||||
newVal := v.String()
|
||||
if newVal != "" {
|
||||
replacements = append(replacements, v.Var, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
replacer := strings.NewReplacer(replacements...)
|
||||
replaced := replacer.Replace(query)
|
||||
return replaced
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package influx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTemplateReplace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
vars TemplateVars
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "select with parameters",
|
||||
query: "$METHOD field1, $field FROM $measurement WHERE temperature > $temperature",
|
||||
vars: TemplateVars{
|
||||
Vars: []TemplateVar{
|
||||
{
|
||||
Var: "$temperature",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "csv",
|
||||
Value: "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: "$field",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "fieldKey",
|
||||
Value: "field2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: "$METHOD",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "csv",
|
||||
Value: "SELECT",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: "$measurement",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "csv",
|
||||
Value: `"cpu"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT field1, "field2" FROM "cpu" WHERE temperature > 10`,
|
||||
},
|
||||
{
|
||||
name: "select with parameters and aggregates",
|
||||
query: `SELECT mean($field) FROM "cpu" WHERE $tag = $value GROUP BY $tag`,
|
||||
vars: TemplateVars{
|
||||
Vars: []TemplateVar{
|
||||
{
|
||||
Var: "$value",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "tagValue",
|
||||
Value: "howdy.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: "$tag",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "tagKey",
|
||||
Value: "host",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Var: "$field",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "fieldKey",
|
||||
Value: "field",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT mean("field") FROM "cpu" WHERE "host" = 'howdy.com' GROUP BY "host"`,
|
||||
},
|
||||
{
|
||||
name: "Non-existant parameters",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
},
|
||||
{
|
||||
name: "var without a value",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
vars: TemplateVars{
|
||||
Vars: []TemplateVar{
|
||||
{
|
||||
Var: "$field",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
},
|
||||
{
|
||||
name: "var with unknown type",
|
||||
query: `SELECT $field FROM "cpu"`,
|
||||
vars: TemplateVars{
|
||||
Vars: []TemplateVar{
|
||||
{
|
||||
Var: "$field",
|
||||
Values: []TemplateValue{
|
||||
{
|
||||
Type: "who knows?",
|
||||
Value: "field",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `SELECT $field FROM "cpu"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := TemplateReplace(tt.query, tt.vars)
|
||||
if got != tt.want {
|
||||
t.Errorf("TestParse %s =\n%s\nwant\n%s", tt.name, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -9,27 +9,31 @@ import (
|
|||
const (
|
||||
// DefaultCookieName is the name of the stored cookie
|
||||
DefaultCookieName = "session"
|
||||
// DefaultInactivityDuration is the duration a token is valid without any new activity
|
||||
DefaultInactivityDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
var _ Authenticator = &cookie{}
|
||||
|
||||
// cookie represents the location and expiration time of new cookies.
|
||||
type cookie struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
Name string // Name is the name of the cookie stored on the browser
|
||||
Lifespan time.Duration // Lifespan is the expiration date of the cookie. 0 means session cookie
|
||||
Inactivity time.Duration // Inactivity is the length of time a token is valid if there is no activity
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
}
|
||||
|
||||
// NewCookieJWT creates an Authenticator that uses cookies for auth
|
||||
func NewCookieJWT(secret string, duration time.Duration) Authenticator {
|
||||
func NewCookieJWT(secret string, lifespan time.Duration) Authenticator {
|
||||
return &cookie{
|
||||
Name: DefaultCookieName,
|
||||
Duration: duration,
|
||||
Now: time.Now,
|
||||
Name: DefaultCookieName,
|
||||
Lifespan: lifespan,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: DefaultNowTime,
|
||||
Tokens: &JWT{
|
||||
Secret: secret,
|
||||
Now: time.Now,
|
||||
Now: DefaultNowTime,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -40,42 +44,76 @@ func (c *cookie) Validate(ctx context.Context, r *http.Request) (Principal, erro
|
|||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Duration)
|
||||
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Lifespan)
|
||||
}
|
||||
|
||||
// Extend will extend the lifetime of the Token by the Inactivity time. Assumes
|
||||
// Principal is already valid.
|
||||
func (c *cookie) Extend(ctx context.Context, w http.ResponseWriter, p Principal) (Principal, error) {
|
||||
// Refresh the token by extending its life another Inactivity duration
|
||||
p, err := c.Tokens.ExtendedPrincipal(ctx, p, c.Inactivity)
|
||||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
|
||||
// Creating a new token with the extended principal
|
||||
token, err := c.Tokens.Create(ctx, p)
|
||||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
|
||||
// Cookie lifespan can be indirectly figured out by taking the token's
|
||||
// issued at time and adding the lifespan setting The token's issued at
|
||||
// time happens to correspond to the cookie's original issued at time.
|
||||
exp := p.IssuedAt.Add(c.Lifespan)
|
||||
// Once the token has been extended, write it out as a new cookie.
|
||||
c.setCookie(w, string(token), exp)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Authorize will create cookies containing token information. It'll create
|
||||
// a token with cookie.Duration of life to be stored as the cookie's value.
|
||||
func (c *cookie) Authorize(ctx context.Context, w http.ResponseWriter, p Principal) error {
|
||||
token, err := c.Tokens.Create(ctx, p, c.Duration)
|
||||
// Principal will be issued at Now() and will expire
|
||||
// c.Inactivity into the future
|
||||
now := c.Now()
|
||||
p.IssuedAt = now
|
||||
p.ExpiresAt = now.Add(c.Inactivity)
|
||||
|
||||
token, err := c.Tokens.Create(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The time when the cookie expires
|
||||
exp := now.Add(c.Lifespan)
|
||||
c.setCookie(w, string(token), exp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCookie creates a cookie with value expiring at exp and writes it as a cookie into the response
|
||||
func (c *cookie) setCookie(w http.ResponseWriter, value string, exp time.Time) {
|
||||
// Cookie has a Token baked into it
|
||||
cookie := http.Cookie{
|
||||
Name: DefaultCookieName,
|
||||
Value: string(token),
|
||||
Value: value,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
// Only set a cookie to be persistent (endure beyond the browser session)
|
||||
// if auth duration is greater than zero
|
||||
if c.Duration > 0 {
|
||||
cookie.Expires = c.Now().UTC().Add(c.Duration)
|
||||
if c.Lifespan > 0 || exp.Before(c.Now()) {
|
||||
cookie.Expires = exp
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expire returns a cookie that will expire an existing cookie
|
||||
func (c *cookie) Expire(w http.ResponseWriter) {
|
||||
cookie := http.Cookie{
|
||||
Name: DefaultCookieName,
|
||||
Value: "none",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Expires: c.Now().UTC().Add(-1 * time.Hour), // to expire cookie set the time in the past
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
// to expire cookie set the time in the past
|
||||
c.setCookie(w, "none", c.Now().Add(-1*time.Hour))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -16,16 +17,21 @@ type MockTokenizer struct {
|
|||
ValidErr error
|
||||
Token Token
|
||||
CreateErr error
|
||||
ExtendErr error
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) ValidPrincipal(ctx context.Context, token Token, duration time.Duration) (Principal, error) {
|
||||
return m.Principal, m.ValidErr
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) Create(ctx context.Context, p Principal, t time.Duration) (Token, error) {
|
||||
func (m *MockTokenizer) Create(ctx context.Context, p Principal) (Token, error) {
|
||||
return m.Token, m.CreateErr
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error) {
|
||||
return principal, m.ExtendErr
|
||||
}
|
||||
|
||||
func TestCookieAuthorize(t *testing.T) {
|
||||
var test = []struct {
|
||||
Desc string
|
||||
|
@ -48,7 +54,7 @@ func TestCookieAuthorize(t *testing.T) {
|
|||
}
|
||||
for _, test := range test {
|
||||
cook := cookie{
|
||||
Duration: 1 * time.Second,
|
||||
Lifespan: 1 * time.Second,
|
||||
Now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
|
@ -121,8 +127,9 @@ func TestCookieValidate(t *testing.T) {
|
|||
})
|
||||
|
||||
cook := cookie{
|
||||
Name: test.Lookup,
|
||||
Duration: 1 * time.Second,
|
||||
Name: test.Lookup,
|
||||
Lifespan: 1 * time.Second,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
|
@ -150,3 +157,118 @@ func TestNewCookieJWT(t *testing.T) {
|
|||
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieExtend(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
type fields struct {
|
||||
Name string
|
||||
Lifespan time.Duration
|
||||
Inactivity time.Duration
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
w *httptest.ResponseRecorder
|
||||
p Principal
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want Principal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful extention",
|
||||
want: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
fields: fields{
|
||||
Name: "session",
|
||||
Lifespan: time.Second,
|
||||
Inactivity: time.Second,
|
||||
Now: func() time.Time {
|
||||
return history
|
||||
},
|
||||
Tokens: &MockTokenizer{
|
||||
Principal: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
Token: "token",
|
||||
ExtendErr: nil,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unable to extend",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
Tokens: &MockTokenizer{
|
||||
ExtendErr: fmt.Errorf("bad extend"),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unable to create",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
Tokens: &MockTokenizer{
|
||||
CreateErr: fmt.Errorf("bad extend"),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &cookie{
|
||||
Name: tt.fields.Name,
|
||||
Lifespan: tt.fields.Lifespan,
|
||||
Inactivity: tt.fields.Inactivity,
|
||||
Now: tt.fields.Now,
|
||||
Tokens: tt.fields.Tokens,
|
||||
}
|
||||
got, err := c.Extend(tt.args.ctx, tt.args.w, tt.args.p)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("cookie.Extend() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr == false {
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("cookie.Extend() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
cookies := tt.args.w.HeaderMap["Set-Cookie"]
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("Expected some cookies but got zero")
|
||||
}
|
||||
log.Printf("%s", cookies)
|
||||
want := fmt.Sprintf("%s=%s", DefaultCookieName, "token")
|
||||
if !strings.Contains(cookies[0], want) {
|
||||
t.Errorf("cookie.Extend() = %v, want %v", cookies[0], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
120
oauth2/jwt.go
120
oauth2/jwt.go
|
@ -21,7 +21,7 @@ type JWT struct {
|
|||
func NewJWT(secret string) *JWT {
|
||||
return &JWT{
|
||||
Secret: secret,
|
||||
Now: time.Now,
|
||||
Now: DefaultNowTime,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,47 +44,9 @@ func (c *Claims) Valid() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// EverlastingClaims extends jwt.StandardClaims' Valid to make sure claims has
|
||||
// a subject, and it ignores the expiration
|
||||
type EverlastingClaims struct {
|
||||
gojwt.StandardClaims
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Valid time based claims "iat, nbf".
|
||||
// There is no accounting for clock skew.
|
||||
// As well, if any of the above claims are not in the token, it will still
|
||||
// be considered a valid claim.
|
||||
func (c *EverlastingClaims) Valid() error {
|
||||
vErr := new(gojwt.ValidationError)
|
||||
now := c.Now().Unix()
|
||||
|
||||
// The claims below are optional, by default, so if they are set to the
|
||||
// default value in Go, let's not fail the verification for them.
|
||||
|
||||
if c.VerifyIssuedAt(now, false) == false {
|
||||
vErr.Inner = fmt.Errorf("Token used before issued")
|
||||
vErr.Errors |= gojwt.ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
if c.VerifyNotBefore(now, false) == false {
|
||||
vErr.Inner = fmt.Errorf("token is not valid yet")
|
||||
vErr.Errors |= gojwt.ValidationErrorNotValidYet
|
||||
}
|
||||
|
||||
if c.Subject == "" {
|
||||
return fmt.Errorf("claim has no subject")
|
||||
}
|
||||
|
||||
if vErr.Errors > 0 {
|
||||
return vErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims.
|
||||
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, duration time.Duration) (Principal, error) {
|
||||
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims. lifespan is the
|
||||
// maximum valid lifetime of a token.
|
||||
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error) {
|
||||
gojwt.TimeFunc = j.Now
|
||||
|
||||
// Check for expected signing method.
|
||||
|
@ -95,20 +57,16 @@ func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, duration time.
|
|||
return []byte(j.Secret), nil
|
||||
}
|
||||
|
||||
if duration == 0 {
|
||||
return j.ValidEverlastingClaims(jwtToken, alg)
|
||||
}
|
||||
|
||||
return j.ValidClaims(jwtToken, duration, alg)
|
||||
return j.ValidClaims(jwtToken, lifespan, alg)
|
||||
}
|
||||
|
||||
// ValidClaims validates a token with StandardClaims
|
||||
func (j *JWT) ValidClaims(jwtToken Token, duration time.Duration, alg gojwt.Keyfunc) (Principal, error) {
|
||||
func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyfunc) (Principal, error) {
|
||||
// 1. Checks for expired tokens
|
||||
// 2. Checks if time is after the issued at
|
||||
// 3. Check if time is after not before (nbf)
|
||||
// 4. Check if subject is not empty
|
||||
// 5. Check if duration matches auth duration
|
||||
// 5. Check if duration less than auth lifespan
|
||||
token, err := gojwt.ParseWithClaims(string(jwtToken), &Claims{}, alg)
|
||||
if err != nil {
|
||||
return Principal{}, err
|
||||
|
@ -123,56 +81,35 @@ func (j *JWT) ValidClaims(jwtToken Token, duration time.Duration, alg gojwt.Keyf
|
|||
return Principal{}, fmt.Errorf("unable to convert claims to standard claims")
|
||||
}
|
||||
|
||||
if time.Duration(claims.ExpiresAt-claims.IssuedAt)*time.Second != duration {
|
||||
return Principal{}, fmt.Errorf("claims duration is different from auth duration")
|
||||
exp := time.Unix(claims.ExpiresAt, 0)
|
||||
iat := time.Unix(claims.IssuedAt, 0)
|
||||
|
||||
// If the duration of the claim is longer than the auth lifespan then this is
|
||||
// an invalid claim because server assumes that lifespan is the maximum possible
|
||||
// duration
|
||||
if exp.Sub(iat) > lifespan {
|
||||
return Principal{}, fmt.Errorf("claims duration is different from auth lifespan")
|
||||
}
|
||||
|
||||
return Principal{
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
ExpiresAt: exp,
|
||||
IssuedAt: iat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidEverlastingClaims validates a token with EverlastingClaims
|
||||
func (j *JWT) ValidEverlastingClaims(jwtToken Token, alg gojwt.Keyfunc) (Principal, error) {
|
||||
// 1. Checks if time is after the issued at
|
||||
// 2. Check if time is after not before (nbf)
|
||||
// 3. Check if subject is not empty
|
||||
token, err := gojwt.ParseWithClaims(string(jwtToken), &EverlastingClaims{
|
||||
Now: j.Now,
|
||||
}, alg)
|
||||
if err != nil {
|
||||
return Principal{}, err
|
||||
// at time of this writing and researching the docs, token.Valid seems to be always true
|
||||
} else if !token.Valid {
|
||||
return Principal{}, err
|
||||
}
|
||||
|
||||
// at time of this writing and researching the docs, there will always be claims
|
||||
claims, ok := token.Claims.(*EverlastingClaims)
|
||||
if !ok {
|
||||
return Principal{}, fmt.Errorf("unable to convert claims to everlasting claims")
|
||||
}
|
||||
|
||||
return Principal{
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create creates a signed JWT token from user that expires at Now + duration
|
||||
func (j *JWT) Create(ctx context.Context, user Principal, duration time.Duration) (Token, error) {
|
||||
gojwt.TimeFunc = j.Now
|
||||
// Create creates a signed JWT token from user that expires at Principal's ExpireAt time.
|
||||
func (j *JWT) Create(ctx context.Context, user Principal) (Token, error) {
|
||||
// Create a new token object, specifying signing method and the claims
|
||||
// you would like it to contain.
|
||||
now := j.Now().UTC()
|
||||
claims := &Claims{
|
||||
gojwt.StandardClaims{
|
||||
Subject: user.Subject,
|
||||
Issuer: user.Issuer,
|
||||
ExpiresAt: now.Add(duration).Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
ExpiresAt: user.ExpiresAt.Unix(),
|
||||
IssuedAt: user.IssuedAt.Unix(),
|
||||
NotBefore: user.IssuedAt.Unix(),
|
||||
},
|
||||
}
|
||||
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
|
||||
|
@ -184,3 +121,12 @@ func (j *JWT) Create(ctx context.Context, user Principal, duration time.Duration
|
|||
}
|
||||
return Token(t), nil
|
||||
}
|
||||
|
||||
// ExtendedPrincipal sets the expires at to be the current time plus the extention into the future
|
||||
func (j *JWT) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error) {
|
||||
// Extend the time of expiration. Do not change IssuedAt as the
|
||||
// lifetime of the token is extended, but, NOT the original time
|
||||
// of issue. This is used to enforce a maximum lifetime of a token
|
||||
principal.ExpiresAt = j.Now().Add(extension)
|
||||
return principal, nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package oauth2_test
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
var tests = []struct {
|
||||
Desc string
|
||||
Secret string
|
||||
|
@ -33,7 +35,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0Mzk5LCJuYmYiOi00NDY3NzQ0MDB9.Ga0zGXWTT2CBVnnIhIO5tUAuBEVk4bKPaT4t4MU1ngo",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -42,7 +46,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAxLCJuYmYiOi00NDY3NzQ0MDB9.vWXdm0-XQ_pW62yBpSISFFJN_yz0vqT9_INcUKTp5Q8",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("token is expired by 1s"),
|
||||
},
|
||||
|
@ -52,7 +58,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQzOTl9.TMGAhv57u1aosjc4ywKC7cElP1tKyQH7GmRF2ToAxlE",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("token is not valid yet"),
|
||||
},
|
||||
|
@ -62,7 +70,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOi00NDY3NzQ0MDAsImV4cCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwfQ.gxsA6_Ei3s0f2I1TAtrrb8FmGiO25OqVlktlF_ylhX4",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("claim has no subject"),
|
||||
},
|
||||
|
@ -72,18 +82,12 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI",
|
||||
Duration: 500 * time.Hour,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history,
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("claims duration is different from auth duration"),
|
||||
},
|
||||
{
|
||||
Desc: "Test valid EverlastingClaim",
|
||||
Secret: "secret",
|
||||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0Mzk5LCJuYmYiOi00NDY3NzQ0MDB9.Ga0zGXWTT2CBVnnIhIO5tUAuBEVk4bKPaT4t4MU1ngo",
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
j := oauth2.JWT{
|
||||
|
@ -107,18 +111,20 @@ func TestAuthenticate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
duration := time.Second
|
||||
expected := oauth2.Token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzOTksImlhdCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwLCJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIn0.ofQM6yTmrmve5JeEE0RcK4_euLXuZ_rdh6bLAbtbC9M")
|
||||
history := time.Unix(-446774400, 0)
|
||||
j := oauth2.JWT{
|
||||
Secret: "secret",
|
||||
Now: func() time.Time {
|
||||
return time.Unix(-446774400, 0)
|
||||
return history
|
||||
},
|
||||
}
|
||||
p := oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
}
|
||||
if token, err := j.Create(context.Background(), p, duration); err != nil {
|
||||
if token, err := j.Create(context.Background(), p); err != nil {
|
||||
t.Errorf("Error creating token for principal: %v", err)
|
||||
} else if token != expected {
|
||||
t.Errorf("Error creating token; expected: %s actual: %s", expected, token)
|
||||
|
@ -134,3 +140,56 @@ func TestSigningMethod(t *testing.T) {
|
|||
t.Errorf("Error wanted 'unexpected signing method', got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWT_ExtendedPrincipal(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
type fields struct {
|
||||
Now func() time.Time
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
principal oauth2.Principal
|
||||
extension time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want oauth2.Principal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Extend principal by one hour",
|
||||
fields: fields{
|
||||
Now: func() time.Time {
|
||||
return history
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
principal: oauth2.Principal{
|
||||
ExpiresAt: history,
|
||||
},
|
||||
extension: time.Hour,
|
||||
},
|
||||
want: oauth2.Principal{
|
||||
ExpiresAt: history.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &oauth2.JWT{
|
||||
Now: tt.fields.Now,
|
||||
}
|
||||
got, err := j.ExtendedPrincipal(tt.args.ctx, tt.args.principal, tt.args.extension)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWT.ExtendedPrincipal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("JWT.ExtendedPrincipal() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ func NewAuthMux(p Provider, a Authenticator, t Tokenizer, l chronograf.Logger) *
|
|||
Logger: l,
|
||||
SuccessURL: "/",
|
||||
FailureURL: "/login",
|
||||
Now: DefaultNowTime,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,6 +39,7 @@ type AuthMux struct {
|
|||
Logger chronograf.Logger // Logger is used to give some more information about the OAuth2 process
|
||||
SuccessURL string // SuccessURL is redirect location after successful authorization
|
||||
FailureURL string // FailureURL is redirect location after authorization failure
|
||||
Now func() time.Time // Now returns the current time (for testing)
|
||||
}
|
||||
|
||||
// Login uses a Cookie with a random string as the state validation method. JWTs are
|
||||
|
@ -52,12 +54,17 @@ func (j *AuthMux) Login() http.Handler {
|
|||
// oauth2 provider's password.
|
||||
// If the callback is not received within 10 minutes, then authorization will fail.
|
||||
csrf := randomString(32) // 32 is not important... just long
|
||||
p := Principal{
|
||||
Subject: csrf,
|
||||
}
|
||||
now := j.Now()
|
||||
|
||||
// This token will be valid for 10 minutes. Any chronograf server will
|
||||
// be able to validate this token.
|
||||
token, err := j.Tokens.Create(r.Context(), p, TenMinutes)
|
||||
p := Principal{
|
||||
Subject: csrf,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: now.Add(TenMinutes),
|
||||
}
|
||||
token, err := j.Tokens.Create(r.Context(), p)
|
||||
|
||||
// This is likely an internal server error
|
||||
if err != nil {
|
||||
j.Logger.
|
||||
|
|
|
@ -30,10 +30,11 @@ func setupMuxTest(selector func(*AuthMux) http.Handler) (*http.Client, *httptest
|
|||
mp := &MockProvider{"biff@example.com", provider.URL}
|
||||
mt := &YesManTokenizer{}
|
||||
auth := &cookie{
|
||||
Name: DefaultCookieName,
|
||||
Duration: 1 * time.Hour,
|
||||
Now: now,
|
||||
Tokens: mt,
|
||||
Name: DefaultCookieName,
|
||||
Lifespan: 1 * time.Hour,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: now,
|
||||
Tokens: mt,
|
||||
}
|
||||
|
||||
jm := NewAuthMux(mp, auth, mt, clog.New(clog.ParseLevel("debug")))
|
||||
|
|
|
@ -30,8 +30,10 @@ var (
|
|||
|
||||
// Principal is any entity that can be authenticated
|
||||
type Principal struct {
|
||||
Subject string
|
||||
Issuer string
|
||||
Subject string
|
||||
Issuer string
|
||||
ExpiresAt time.Time
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
||||
/* Interfaces */
|
||||
|
@ -66,6 +68,8 @@ type Authenticator interface {
|
|||
Validate(context.Context, *http.Request) (Principal, error)
|
||||
// Authorize will grant privileges to a Principal
|
||||
Authorize(context.Context, http.ResponseWriter, Principal) error
|
||||
// Extend will extend the lifetime of a already validated Principal
|
||||
Extend(context.Context, http.ResponseWriter, Principal) (Principal, error)
|
||||
// Expire revokes privileges from a Principal
|
||||
Expire(http.ResponseWriter)
|
||||
}
|
||||
|
@ -78,9 +82,11 @@ type Token string
|
|||
// non-sensitive equivalent, referred to as a token, that has no extrinsic
|
||||
// or exploitable meaning or value.
|
||||
type Tokenizer interface {
|
||||
// Create uses a token lasting duration with Principal data
|
||||
Create(context.Context, Principal, time.Duration) (Token, error)
|
||||
// Create issues a token at Principal's IssuedAt that lasts until Principal's ExpireAt
|
||||
Create(context.Context, Principal) (Token, error)
|
||||
// ValidPrincipal checks if the token has a valid Principal and requires
|
||||
// a duration to ensure it complies with possible server runtime arguments.
|
||||
ValidPrincipal(context.Context, Token, time.Duration) (Principal, error)
|
||||
// a lifespan duration to ensure it complies with possible server runtime arguments.
|
||||
ValidPrincipal(ctx context.Context, token Token, lifespan time.Duration) (Principal, error)
|
||||
// ExtendedPrincipal adds the extention to the principal's lifespan.
|
||||
ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error)
|
||||
}
|
||||
|
|
|
@ -63,10 +63,14 @@ func (y *YesManTokenizer) ValidPrincipal(ctx context.Context, token Token, durat
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (y *YesManTokenizer) Create(ctx context.Context, p Principal, t time.Duration) (Token, error) {
|
||||
func (y *YesManTokenizer) Create(ctx context.Context, p Principal) (Token, error) {
|
||||
return Token("HELLO?!MCFLY?!ANYONEINTHERE?!"), nil
|
||||
}
|
||||
|
||||
func (y *YesManTokenizer) ExtendedPrincipal(ctx context.Context, p Principal, ext time.Duration) (Principal, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewTestTripper(log chronograf.Logger, ts *httptest.Server, rt http.RoundTripper) (*TestTripper, error) {
|
||||
url, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package oauth2
|
||||
|
||||
import "time"
|
||||
|
||||
// DefaultNowTime returns UTC time at the present moment
|
||||
var DefaultNowTime = func() time.Time { return time.Now().UTC() }
|
|
@ -21,7 +21,7 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
|||
WithField("url", r.URL)
|
||||
|
||||
ctx := r.Context()
|
||||
// We do not check the validity of the principal. Those
|
||||
// We do not check the authorization of the principal. Those
|
||||
// served further down the chain should do so.
|
||||
principal, err := auth.Validate(ctx, r)
|
||||
if err != nil {
|
||||
|
@ -30,6 +30,15 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
|||
return
|
||||
}
|
||||
|
||||
// If the principal is valid we will extend its lifespan
|
||||
// into the future
|
||||
principal, err = auth.Extend(ctx, w, principal)
|
||||
if err != nil {
|
||||
log.Error("Unable to extend principal")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the principal to the next handler
|
||||
ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
|
|
@ -15,19 +15,30 @@ import (
|
|||
type MockAuthenticator struct {
|
||||
Principal oauth2.Principal
|
||||
ValidateErr error
|
||||
ExtendErr error
|
||||
Serialized string
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Validate(context.Context, *http.Request) (oauth2.Principal, error) {
|
||||
return m.Principal, m.ValidateErr
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Extend(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) (oauth2.Principal, error) {
|
||||
cookie := http.Cookie{}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return m.Principal, m.ExtendErr
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Authorize(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) error {
|
||||
cookie := http.Cookie{}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Expire(http.ResponseWriter) {}
|
||||
|
||||
func (m *MockAuthenticator) ValidAuthorization(ctx context.Context, serializedAuthorization string) (oauth2.Principal, error) {
|
||||
return oauth2.Principal{}, nil
|
||||
}
|
||||
|
|
|
@ -2,8 +2,11 @@ package server
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/influx/queries"
|
||||
)
|
||||
|
@ -37,7 +40,8 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if _, err = s.SourcesStore.Get(ctx, srcID); err != nil {
|
||||
src, err := s.SourcesStore.Get(ctx, srcID)
|
||||
if err != nil {
|
||||
notFound(w, srcID, s.Logger)
|
||||
return
|
||||
}
|
||||
|
@ -54,16 +58,57 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
for i, q := range req.Queries {
|
||||
qr := QueryResponse{
|
||||
ID: q.ID,
|
||||
Query: q.Query,
|
||||
QueryConfig: ToQueryConfig(q.Query),
|
||||
ID: q.ID,
|
||||
Query: q.Query,
|
||||
}
|
||||
|
||||
qc := ToQueryConfig(q.Query)
|
||||
if err := s.DefaultRP(ctx, &qc, &src); err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
qr.QueryConfig = qc
|
||||
|
||||
if stmt, err := queries.ParseSelect(q.Query); err == nil {
|
||||
qr.QueryAST = stmt
|
||||
}
|
||||
|
||||
qr.QueryConfig.ID = q.ID
|
||||
res.Queries[i] = qr
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// DefaultRP will add the default retention policy to the QC if one has not been specified
|
||||
func (s *Service) DefaultRP(ctx context.Context, qc *chronograf.QueryConfig, src *chronograf.Source) error {
|
||||
// Only need to find the default RP IFF the qc's rp is empty
|
||||
if qc.RetentionPolicy != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For queries without databases, measurements, or fields we will not
|
||||
// be able to find an RP
|
||||
if qc.Database == "" || qc.Measurement == "" || len(qc.Fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
db := s.Databases
|
||||
if err := db.Connect(ctx, src); err != nil {
|
||||
return fmt.Errorf("Unable to connect to source: %v", err)
|
||||
}
|
||||
|
||||
rps, err := db.AllRP(ctx, qc.Database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to load RPs from DB %s: %v", qc.Database, err)
|
||||
}
|
||||
|
||||
for _, rp := range rps {
|
||||
if rp.Default {
|
||||
qc.RetentionPolicy = rp.Name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -30,14 +30,6 @@ class MultiSelectDropdown extends Component {
|
|||
this.onApplyFunctions = ::this.onApplyFunctions
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!_.isEqual(this.state.localSelectedItems, nextProps.selectedItems)) {
|
||||
this.setState({
|
||||
localSelectedItems: nextProps.selectedItems,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
this.setState({isOpen: false})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue