diff --git a/chronograf/cmd/chronoctl/add.go b/chronograf/cmd/chronoctl/add.go deleted file mode 100644 index ccb5add711..0000000000 --- a/chronograf/cmd/chronoctl/add.go +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - "context" - "strings" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -type AddCommand struct { - BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` - ID *uint64 `short:"i" long:"id" description:"Users ID. Must be id for existing user"` - Username string `short:"n" long:"name" description:"Users name. Must be Oauth-able email address or username"` - Provider string `short:"p" long:"provider" description:"Name of the Auth provider (e.g. google, github, auth0, or generic)"` - Scheme string `short:"s" long:"scheme" description:"Authentication scheme that matches auth provider (e.g. oauth2)" default:"oauth2"` - Organizations string `short:"o" long:"orgs" description:"A comma separated list of organizations that the user should be added to" default:"default"` -} - -var addCommand AddCommand - -func (l *AddCommand) Execute(args []string) error { - c, err := NewBoltClient(l.BoltPath) - if err != nil { - return err - } - defer c.Close() - - q := chronograf.UserQuery{ - Name: &l.Username, - Provider: &l.Provider, - Scheme: &l.Scheme, - } - - if l.ID != nil { - q.ID = l.ID - } - - ctx := context.Background() - - user, err := c.UsersStore.Get(ctx, q) - if err != nil && err != chronograf.ErrUserNotFound { - return err - } else if err == chronograf.ErrUserNotFound { - user = &chronograf.User{ - Name: l.Username, - Provider: l.Provider, - Scheme: l.Scheme, - Roles: []chronograf.Role{ - { - Name: "member", - Organization: "default", - }, - }, - SuperAdmin: true, - } - - user, err = c.UsersStore.Add(ctx, user) - if err != nil { - return err - } - } else { - user.SuperAdmin = true - if len(user.Roles) == 0 { - user.Roles = []chronograf.Role{ - { - Name: "member", - Organization: "default", - }, - } - } - if err = c.UsersStore.Update(ctx, user); err != nil { - return err - } - } - - // TODO(desa): Apply mapping to user and update their roles - roles := []chronograf.Role{} -OrgLoop: - for _, org := range strings.Split(l.Organizations, ",") { - // Check to see is user is already a part of the organization - for _, r := range user.Roles { - if r.Organization == org { - continue OrgLoop - } - } - - orgQuery := chronograf.OrganizationQuery{ - ID: &org, - } - o, err := c.OrganizationsStore.Get(ctx, orgQuery) - if err != nil { - return err - } - - role := chronograf.Role{ - Organization: org, - Name: o.DefaultRole, - } - roles = append(roles, role) - } - - user.Roles = append(user.Roles, roles...) - if err = c.UsersStore.Update(ctx, user); err != nil { - return err - } - - w := NewTabWriter() - WriteHeaders(w) - WriteUser(w, user) - w.Flush() - - return nil -} - -func init() { - parser.AddCommand("add-superadmin", - "Creates a new superadmin user", - "The add-user command will create a new user with superadmin status", - &addCommand) -} diff --git a/chronograf/cmd/chronoctl/list.go b/chronograf/cmd/chronoctl/list.go deleted file mode 100644 index 6396359adf..0000000000 --- a/chronograf/cmd/chronoctl/list.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "context" -) - -type ListCommand struct { - BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (e.g. './chronograf-v1.db')" env:"BOLT_PATH" default:"chronograf-v1.db"` -} - -var listCommand ListCommand - -func (l *ListCommand) Execute(args []string) error { - c, err := NewBoltClient(l.BoltPath) - if err != nil { - return err - } - defer c.Close() - - ctx := context.Background() - users, err := c.UsersStore.All(ctx) - if err != nil { - return err - } - - w := NewTabWriter() - WriteHeaders(w) - for _, user := range users { - WriteUser(w, &user) - } - w.Flush() - - return nil -} - -func init() { - parser.AddCommand("list-users", - "Lists users", - "The list-users command will list all users in the chronograf boltdb instance", - &listCommand) -} diff --git a/chronograf/cmd/chronoctl/main.go b/chronograf/cmd/chronoctl/main.go deleted file mode 100644 index 3e8f180b96..0000000000 --- a/chronograf/cmd/chronoctl/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/jessevdk/go-flags" -) - -type Options struct { -} - -var options Options - -var parser = flags.NewParser(&options, flags.Default) - -func main() { - if _, err := parser.Parse(); err != nil { - if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { - os.Exit(0) - } else { - fmt.Fprintln(os.Stdout) - parser.WriteHelp(os.Stdout) - os.Exit(1) - } - } -} diff --git a/chronograf/cmd/chronoctl/util.go b/chronograf/cmd/chronoctl/util.go deleted file mode 100644 index fdf691681c..0000000000 --- a/chronograf/cmd/chronoctl/util.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/bolt" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func NewBoltClient(path string) (*bolt.Client, error) { - c := bolt.NewClient() - c.Path = path - - ctx := context.Background() - logger := mocks.NewLogger() - var bi chronograf.BuildInfo - if err := c.Open(ctx, logger, bi); err != nil { - return nil, err - } - - return c, nil -} - -func NewTabWriter() *tabwriter.Writer { - return tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) -} - -func WriteHeaders(w io.Writer) { - fmt.Fprintln(w, "ID\tName\tProvider\tScheme\tSuperAdmin\tOrganization(s)") -} - -func WriteUser(w io.Writer, user *chronograf.User) { - orgs := []string{} - for _, role := range user.Roles { - orgs = append(orgs, role.Organization) - } - fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%t\t%s\n", user.ID, user.Name, user.Provider, user.Scheme, user.SuperAdmin, strings.Join(orgs, ",")) -} diff --git a/chronograf/cmd/chronograf/main.go b/chronograf/cmd/chronograf/main.go deleted file mode 100644 index 516dc2f492..0000000000 --- a/chronograf/cmd/chronograf/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "context" - "log" - "os" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/server" - flags "github.com/jessevdk/go-flags" -) - -// Build flags -var ( - version = "" - commit = "" -) - -func main() { - srv := server.Server{ - BuildInfo: chronograf.BuildInfo{ - Version: version, - Commit: commit, - }, - } - - parser := flags.NewParser(&srv, flags.Default) - parser.ShortDescription = `Chronograf` - parser.LongDescription = `Options for Chronograf` - - if _, err := parser.Parse(); err != nil { - code := 1 - if fe, ok := err.(*flags.Error); ok { - if fe.Type == flags.ErrHelp { - code = 0 - } - } - os.Exit(code) - } - - if srv.ShowVersion { - log.Printf("Chronograf %s (git: %s)\n", version, commit) - os.Exit(0) - } - - ctx := context.Background() - if err := srv.Serve(ctx); err != nil { - log.Fatalln(err) - } -} diff --git a/chronograf/dist/Makefile b/chronograf/dist/Makefile deleted file mode 100644 index 0f7175c065..0000000000 --- a/chronograf/dist/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# List any generated files here -TARGETS = dist_gen.go -# List any source files used to generate the targets here -SOURCES = dist.go $(shell find ../../ui/build -type f) -# List any directories that have their own Makefile here -SUBDIRS = - -# Default target -all: $(SUBDIRS) $(TARGETS) - -# Recurse into subdirs for same make goal -$(SUBDIRS): - $(MAKE) -C $@ $(MAKECMDGOALS) - -# Clean all targets recursively -clean: $(SUBDIRS) - rm -f $(TARGETS) - -# Define go generate if not already defined -GO_GENERATE := go generate - -# Run go generate for the targets -$(TARGETS): $(SOURCES) - $(GO_GENERATE) -x - -.PHONY: all clean $(SUBDIRS) diff --git a/chronograf/dist/TODO.go b/chronograf/dist/TODO.go deleted file mode 100644 index ab6e1b47ee..0000000000 --- a/chronograf/dist/TODO.go +++ /dev/null @@ -1,26 +0,0 @@ -package dist - -import ( - "errors" - "os" -) - -// The functions defined in this file are placeholders -// when the binary is compiled without assets. - -var errNoAssets = errors.New("no assets included in binary") - -// Asset returns an error stating no assets were included in the binary. -func Asset(string) ([]byte, error) { - return nil, errNoAssets -} - -// AssetInfo returns an error stating no assets were included in the binary. -func AssetInfo(name string) (os.FileInfo, error) { - return nil, errNoAssets -} - -// AssetDir returns nil because there are no assets included in the binary. -func AssetDir(name string) ([]string, error) { - return nil, errNoAssets -} diff --git a/chronograf/dist/dir.go b/chronograf/dist/dir.go deleted file mode 100644 index 1f4ac90b93..0000000000 --- a/chronograf/dist/dir.go +++ /dev/null @@ -1,33 +0,0 @@ -package dist - -import ( - "net/http" - "os" -) - -// Dir functions like http.Dir except returns the content of a default file if not found. -type Dir struct { - Default string - dir http.Dir -} - -// NewDir constructs a Dir with a default file -func NewDir(dir, def string) Dir { - return Dir{ - Default: def, - dir: http.Dir(dir), - } -} - -// Open will return the file in the dir if it exists, or, the Default file otherwise. -func (d Dir) Open(name string) (http.File, error) { - f, err := d.dir.Open(name) - if err != nil { - f, err = os.Open(d.Default) - if err != nil { - return nil, err - } - return f, nil - } - return f, err -} diff --git a/chronograf/dist/dist.go b/chronograf/dist/dist.go deleted file mode 100644 index bd70d9546c..0000000000 --- a/chronograf/dist/dist.go +++ /dev/null @@ -1,88 +0,0 @@ -package dist - -//go:generate env GO111MODULE=on go run github.com/kevinburke/go-bindata/go-bindata -o dist_gen.go -ignore 'map|go' -tags assets -pkg dist ../../ui/build/... - -import ( - "fmt" - "net/http" - - assetfs "github.com/elazarl/go-bindata-assetfs" -) - -// DebugAssets serves assets via a specified directory -type DebugAssets struct { - Dir string // Dir is a directory location of asset files - Default string // Default is the file to serve if file is not found. -} - -// Handler is an http.FileServer for the Dir -func (d *DebugAssets) Handler() http.Handler { - return http.FileServer(NewDir(d.Dir, d.Default)) -} - -// BindataAssets serves assets from go-bindata, but, also serves Default if assent doesn't exist -// This is to support single-page react-apps with its own router. -type BindataAssets struct { - Prefix string // Prefix is prepended to the http file request - Default string // Default is the file to serve if the file is not found - DefaultContentType string // DefaultContentType is the content type of the default file -} - -// Handler serves go-bindata using a go-bindata-assetfs façade -func (b *BindataAssets) Handler() http.Handler { - return b -} - -// addCacheHeaders requests an hour of Cache-Control and sets an ETag based on file size and modtime -func (b *BindataAssets) addCacheHeaders(filename string, w http.ResponseWriter) error { - w.Header().Add("Cache-Control", "public, max-age=3600") - fi, err := AssetInfo(filename) - if err != nil { - return err - } - - hour, minute, second := fi.ModTime().Clock() - etag := fmt.Sprintf(`"%d%d%d%d%d"`, fi.Size(), fi.ModTime().Day(), hour, minute, second) - - w.Header().Set("ETag", etag) - return nil -} - -// ServeHTTP wraps http.FileServer by returning a default asset if the asset -// doesn't exist. This supports single-page react-apps with its own -// built-in router. Additionally, we override the content-type if the -// Default file is used. -func (b *BindataAssets) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // def wraps the assets to return the default file if the file doesn't exist - def := func(name string) ([]byte, error) { - // If the named asset exists, then return it directly. - octets, err := Asset(name) - if err != nil { - // If this is at / then we just error out so we can return a Directory - // This directory will then be redirected by go to the /index.html - if name == b.Prefix { - return nil, err - } - // If this is anything other than slash, we just return the default - // asset. This default asset will handle the routing. - // Additionally, because we know we are returning the default asset, - // we need to set the default asset's content-type. - w.Header().Set("Content-Type", b.DefaultContentType) - if err := b.addCacheHeaders(b.Default, w); err != nil { - return nil, err - } - return Asset(b.Default) - } - if err := b.addCacheHeaders(name, w); err != nil { - return nil, err - } - return octets, nil - } - var dir http.FileSystem = &assetfs.AssetFS{ - Asset: def, - AssetDir: AssetDir, - AssetInfo: AssetInfo, - Prefix: b.Prefix, - } - http.FileServer(dir).ServeHTTP(w, r) -} diff --git a/chronograf/integrations/server_test.go b/chronograf/integrations/server_test.go deleted file mode 100644 index 0f6462cb74..0000000000 --- a/chronograf/integrations/server_test.go +++ /dev/null @@ -1,3730 +0,0 @@ -package integrations - -// This was intentionally added under the integrations package and not the integrations test package -// so that changes in other parts of the code base that may have an effect on these test will not -// compile until they are fixed. - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - - "net/http" - "testing" - "time" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/bolt" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/server" -) - -func TestServer(t *testing.T) { - type fields struct { - Organizations []chronograf.Organization - Mappings []chronograf.Mapping - Users []chronograf.User - Sources []chronograf.Source - Servers []chronograf.Server - Layouts []chronograf.Layout - Dashboards []chronograf.Dashboard - Config *chronograf.Config - } - type args struct { - server *server.Server - method string - path string - payload interface{} // Expects this to be a json serializable struct - principal oauth2.Principal - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - subName string - fields fields - args args - wants wants - }{ - // { - // name: "GET /sources/5000", - // subName: "Get specific source; including Canned source", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "viewer", - // Organization: "howdy", // from canned testdata - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/sources/5000", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "id": "5000", - // "name": "Influx 1", - // "type": "influx-enterprise", - // "username": "user1", - // "url": "http://localhost:8086", - // "metaUrl": "http://metaurl.com", - // "default": true, - // "telegraf": "telegraf", - // "organization": "howdy", - // "defaultRP": "", - // "authentication": "basic", - // "links": { - // "self": "/chronograf/v1/sources/5000", - // "kapacitors": "/chronograf/v1/sources/5000/kapacitors", - // "services": "/chronograf/v1/sources/5000/services", - // "proxy": "/chronograf/v1/sources/5000/proxy", - // "queries": "/chronograf/v1/sources/5000/queries", - // "write": "/chronograf/v1/sources/5000/write", - // "permissions": "/chronograf/v1/sources/5000/permissions", - // "users": "/chronograf/v1/sources/5000/users", - // "roles": "/chronograf/v1/sources/5000/roles", - // "databases": "/chronograf/v1/sources/5000/dbs", - // "annotations": "/chronograf/v1/sources/5000/annotations", - // "health": "/chronograf/v1/sources/5000/health" - // } - //} - //`, - // }, - // }, - // { - // name: "GET /sources/5000/kapacitors/5000", - // subName: "Get specific kapacitors; including Canned kapacitors", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "viewer", - // Organization: "howdy", // from canned testdata - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/sources/5000/kapacitors/5000", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "id": "5000", - // "name": "Kapa 1", - // "url": "http://localhost:9092", - // "active": true, - // "insecureSkipVerify": false, - // "links": { - // "proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy", - // "self": "/chronograf/v1/sources/5000/kapacitors/5000", - // "rules": "/chronograf/v1/sources/5000/kapacitors/5000/rules", - // "tasks": "/chronograf/v1/sources/5000/kapacitors/5000/proxy?path=/kapacitor/v1/tasks", - // "ping": "/chronograf/v1/sources/5000/kapacitors/5000/proxy?path=/kapacitor/v1/ping" - // } - //} - //`, - // }, - // }, - // { - // name: "GET /sources/5000/kapacitors", - // subName: "Get all kapacitors; including Canned kapacitors", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "viewer", - // Organization: "howdy", // from canned testdata - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/sources/5000/kapacitors", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "kapacitors": [ - // { - // "id": "5000", - // "name": "Kapa 1", - // "url": "http://localhost:9092", - // "active": true, - // "insecureSkipVerify": false, - // "links": { - // "proxy": "/chronograf/v1/sources/5000/kapacitors/5000/proxy", - // "self": "/chronograf/v1/sources/5000/kapacitors/5000", - // "rules": "/chronograf/v1/sources/5000/kapacitors/5000/rules", - // "tasks": "/chronograf/v1/sources/5000/kapacitors/5000/proxy?path=/kapacitor/v1/tasks", - // "ping": "/chronograf/v1/sources/5000/kapacitors/5000/proxy?path=/kapacitor/v1/ping" - // } - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "GET /sources", - // subName: "Get all sources; including Canned sources", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "viewer", - // Organization: "howdy", // from canned testdata - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/sources", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "sources": [ - // { - // "id": "5000", - // "name": "Influx 1", - // "type": "influx-enterprise", - // "username": "user1", - // "url": "http://localhost:8086", - // "metaUrl": "http://metaurl.com", - // "default": true, - // "telegraf": "telegraf", - // "organization": "howdy", - // "defaultRP": "", - // "authentication": "basic", - // "links": { - // "self": "/chronograf/v1/sources/5000", - // "kapacitors": "/chronograf/v1/sources/5000/kapacitors", - // "services": "/chronograf/v1/sources/5000/services", - // "proxy": "/chronograf/v1/sources/5000/proxy", - // "queries": "/chronograf/v1/sources/5000/queries", - // "write": "/chronograf/v1/sources/5000/write", - // "permissions": "/chronograf/v1/sources/5000/permissions", - // "users": "/chronograf/v1/sources/5000/users", - // "roles": "/chronograf/v1/sources/5000/roles", - // "databases": "/chronograf/v1/sources/5000/dbs", - // "annotations": "/chronograf/v1/sources/5000/annotations", - // "health": "/chronograf/v1/sources/5000/health" - // } - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "GET /organizations", - // subName: "Get all organizations; including Canned organization", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations" - // }, - // "organizations": [ - // { - // "links": { - // "self": "/chronograf/v1/organizations/default" - // }, - // "id": "default", - // "name": "Default", - // "defaultRole": "member" - // }, - // { - // "links": { - // "self": "/chronograf/v1/organizations/howdy" - // }, - // "id": "howdy", - // "name": "An Organization", - // "defaultRole": "viewer" - // } - // ] - //}`, - // }, - // }, - // { - // name: "GET /organizations/howdy", - // subName: "Get specific organizations; Canned organization", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations/howdy", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/howdy" - // }, - // "id": "howdy", - // "name": "An Organization", - // "defaultRole": "viewer" - //}`, - // }, - // }, - // { - // name: "GET /dashboards/1000", - // subName: "Get specific in the howdy organization; Using Canned testdata", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "howdy", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/dashboards/1000", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "id": 1000, - // "cells": [ - // { - // "i": "8f61c619-dd9b-4761-8aa8-577f27247093", - // "x": 0, - // "y": 0, - // "w": 11, - // "h": 5, - // "name": "Untitled Cell", - // "queries": [ - // { - // "query": "SELECT mean(\"value\") AS \"mean_value\" FROM \"telegraf\".\"autogen\".\"cpg\" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)", - // "queryConfig": { - // "database": "telegraf", - // "measurement": "cpg", - // "retentionPolicy": "autogen", - // "fields": [ - // { - // "value": "mean", - // "type": "func", - // "alias": "mean_value", - // "args": [ - // { - // "value": "value", - // "type": "field", - // "alias": "" - // } - // ] - // } - // ], - // "tags": {}, - // "groupBy": { - // "time": "auto", - // "tags": [] - // }, - // "areTagsAccepted": false, - // "fill": "null", - // "rawText": null, - // "range": null, - // "shifts": null - // }, - // "source": "/chronograf/v1/sources/2" - // } - // ], - // "axes": { - // "x": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // }, - // "y": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // }, - // "y2": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // } - // }, - // "type": "line", - // "colors": [ - // { - // "id": "0", - // "type": "min", - // "hex": "#00C9FF", - // "name": "laser", - // "value": "0" - // }, - // { - // "id": "1", - // "type": "max", - // "hex": "#9394FF", - // "name": "comet", - // "value": "100" - // } - // ], - // "legend":{ - // "type": "static", - // "orientation": "bottom" - // }, - // "tableOptions":{ - // "verticalTimeAxis": false, - // "sortBy":{ - // "internalName": "", - // "displayName": "", - // "visible": false - // }, - // "wrapping": "", - // "fixFirstColumn": false - // }, - // "fieldOptions": null, - // "timeFormat": "", - // "decimalPlaces":{ - // "isEnforced": false, - // "digits": 0 - // }, - // "links": { - // "self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093" - // } - // } - // ], - // "templates": [ - // { - // "tempVar": ":dbs:", - // "values": [ - // { - // "value": "_internal", - // "type": "database", - // "selected": true - // }, - // { - // "value": "telegraf", - // "type": "database", - // "selected": false - // }, - // { - // "value": "tensorflowdb", - // "type": "database", - // "selected": false - // }, - // { - // "value": "pushgateway", - // "type": "database", - // "selected": false - // }, - // { - // "value": "node_exporter", - // "type": "database", - // "selected": false - // }, - // { - // "value": "mydb", - // "type": "database", - // "selected": false - // }, - // { - // "value": "tiny", - // "type": "database", - // "selected": false - // }, - // { - // "value": "blah", - // "type": "database", - // "selected": false - // }, - // { - // "value": "test", - // "type": "database", - // "selected": false - // }, - // { - // "value": "chronograf", - // "type": "database", - // "selected": false - // }, - // { - // "value": "db_name", - // "type": "database", - // "selected": false - // }, - // { - // "value": "demo", - // "type": "database", - // "selected": false - // }, - // { - // "value": "eeg", - // "type": "database", - // "selected": false - // }, - // { - // "value": "solaredge", - // "type": "database", - // "selected": false - // }, - // { - // "value": "zipkin", - // "type": "database", - // "selected": false - // } - // ], - // "id": "e7e498bf-5869-4874-9071-24628a2cda63", - // "type": "databases", - // "label": "", - // "query": { - // "influxql": "SHOW DATABASES", - // "measurement": "", - // "tagKey": "", - // "fieldKey": "" - // }, - // "links": { - // "self": "/chronograf/v1/dashboards/1000/templates/e7e498bf-5869-4874-9071-24628a2cda63" - // } - // } - // ], - // "name": "Name This Dashboard", - // "organization": "howdy", - // "links": { - // "self": "/chronograf/v1/dashboards/1000", - // "cells": "/chronograf/v1/dashboards/1000/cells", - // "templates": "/chronograf/v1/dashboards/1000/templates" - // } - //}`, - // }, - // }, - // { - // name: "GET /dashboards", - // subName: "Get all dashboards in the howdy organization; Using Canned testdata", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "admin", - // Organization: "howdy", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/dashboards", - // principal: oauth2.Principal{ - // Organization: "howdy", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "dashboards": [ - // { - // "id": 1000, - // "cells": [ - // { - // "i": "8f61c619-dd9b-4761-8aa8-577f27247093", - // "x": 0, - // "y": 0, - // "w": 11, - // "h": 5, - // "name": "Untitled Cell", - // "queries": [ - // { - // "query": "SELECT mean(\"value\") AS \"mean_value\" FROM \"telegraf\".\"autogen\".\"cpg\" WHERE time > :dashboardTime: GROUP BY time(:interval:) FILL(null)", - // "queryConfig": { - // "database": "telegraf", - // "measurement": "cpg", - // "retentionPolicy": "autogen", - // "fields": [ - // { - // "value": "mean", - // "type": "func", - // "alias": "mean_value", - // "args": [ - // { - // "value": "value", - // "type": "field", - // "alias": "" - // } - // ] - // } - // ], - // "tags": {}, - // "groupBy": { - // "time": "auto", - // "tags": [] - // }, - // "areTagsAccepted": false, - // "fill": "null", - // "rawText": null, - // "range": null, - // "shifts": null - // }, - // "source": "/chronograf/v1/sources/2" - // } - // ], - // "axes": { - // "x": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // }, - // "y": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // }, - // "y2": { - // "bounds": [], - // "label": "", - // "prefix": "", - // "suffix": "", - // "base": "10", - // "scale": "linear" - // } - // }, - // "type": "line", - // "colors": [ - // { - // "id": "0", - // "type": "min", - // "hex": "#00C9FF", - // "name": "laser", - // "value": "0" - // }, - // { - // "id": "1", - // "type": "max", - // "hex": "#9394FF", - // "name": "comet", - // "value": "100" - // } - // ], - // "legend": { - // "type": "static", - // "orientation": "bottom" - // }, - // "tableOptions":{ - // "verticalTimeAxis": false, - // "sortBy":{ - // "internalName": "", - // "displayName": "", - // "visible": false - // }, - // "wrapping": "", - // "fixFirstColumn": false - // }, - // "fieldOptions": null, - // "timeFormat": "", - // "decimalPlaces":{ - // "isEnforced": false, - // "digits": 0 - // }, - // "links": { - // "self": "/chronograf/v1/dashboards/1000/cells/8f61c619-dd9b-4761-8aa8-577f27247093" - // } - // } - // ], - // "templates": [ - // { - // "tempVar": ":dbs:", - // "values": [ - // { - // "value": "_internal", - // "type": "database", - // "selected": true - // }, - // { - // "value": "telegraf", - // "type": "database", - // "selected": false - // }, - // { - // "value": "tensorflowdb", - // "type": "database", - // "selected": false - // }, - // { - // "value": "pushgateway", - // "type": "database", - // "selected": false - // }, - // { - // "value": "node_exporter", - // "type": "database", - // "selected": false - // }, - // { - // "value": "mydb", - // "type": "database", - // "selected": false - // }, - // { - // "value": "tiny", - // "type": "database", - // "selected": false - // }, - // { - // "value": "blah", - // "type": "database", - // "selected": false - // }, - // { - // "value": "test", - // "type": "database", - // "selected": false - // }, - // { - // "value": "chronograf", - // "type": "database", - // "selected": false - // }, - // { - // "value": "db_name", - // "type": "database", - // "selected": false - // }, - // { - // "value": "demo", - // "type": "database", - // "selected": false - // }, - // { - // "value": "eeg", - // "type": "database", - // "selected": false - // }, - // { - // "value": "solaredge", - // "type": "database", - // "selected": false - // }, - // { - // "value": "zipkin", - // "type": "database", - // "selected": false - // } - // ], - // "id": "e7e498bf-5869-4874-9071-24628a2cda63", - // "type": "databases", - // "label": "", - // "query": { - // "influxql": "SHOW DATABASES", - // "measurement": "", - // "tagKey": "", - // "fieldKey": "" - // }, - // "links": { - // "self": "/chronograf/v1/dashboards/1000/templates/e7e498bf-5869-4874-9071-24628a2cda63" - // } - // } - // ], - // "name": "Name This Dashboard", - // "organization": "howdy", - // "links": { - // "self": "/chronograf/v1/dashboards/1000", - // "cells": "/chronograf/v1/dashboards/1000/cells", - // "templates": "/chronograf/v1/dashboards/1000/templates" - // } - // } - // ] - //}`, - // }, - // }, - // { - // name: "GET /users", - // subName: "User Not Found in the Default Organization", - // fields: fields{ - // Users: []chronograf.User{}, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations/default/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 403, - // body: `{"code":403,"message":"User is not authorized"}`, - // }, - // }, - // { - // name: "GET /users", - // subName: "Single User in the Default Organization as SuperAdmin", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations/default/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users" - // }, - // "users": [ - // { - // "links": { - // "self": "/chronograf/v1/organizations/default/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // } - // ] - // } - // ] - //}`, - // }, - // }, - // { - // name: "GET /users", - // subName: "Two users in two organizations; user making request is as SuperAdmin with out raw query param", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // { - // ID: 2, // This is artificial, but should be reflective of the users actual ID - // Name: "billietta", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "cool", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations/default/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users" - // }, - // "users": [ - // { - // "links": { - // "self": "/chronograf/v1/organizations/default/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // } - // ] - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "POST /users", - // subName: "User making request is as SuperAdmin with raw query param; being created has wildcard role", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{ - // { - // Name: "*", - // Organization: "default", - // }, - // }, - // }, - // method: "POST", - // path: "/chronograf/v1/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": false, - // "roles": [ - // { - // "name": "member", - // "organization": "default" - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "POST /users", - // subName: "User making request is as SuperAdmin with raw query param; being created has no roles", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{}, - // }, - // method: "POST", - // path: "/chronograf/v1/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": false, - // "roles": [] - //} - //`, - // }, - // }, - // { - // name: "GET /users", - // subName: "Two users in two organizations; user making request is as SuperAdmin with raw query param", - // fields: fields{ - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // { - // ID: 2, // This is artificial, but should be reflective of the users actual ID - // Name: "billietta", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "1", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users" - // }, - // "users": [ - // { - // "links": { - // "self": "/chronograf/v1/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // } - // ] - // }, - // { - // "links": { - // "self": "/chronograf/v1/users/2" - // }, - // "id": "2", - // "name": "billietta", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "1" - // } - // ] - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "GET /users", - // subName: "Two users in two organizations; user making request is as not SuperAdmin with raw query param", - // fields: fields{ - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // { - // ID: 2, // This is artificial, but should be reflective of the users actual ID - // Name: "billietta", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "admin", - // Organization: "1", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/users", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billieta", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 403, - // body: ` - //{ - // "code": 403, - // "message": "User is not authorized" - //} - //`, - // }, - // }, - // { - // name: "POST /users", - // subName: "Create a New User with SuperAdmin status; SuperAdminNewUsers is true (the default case); User on Principal is a SuperAdmin", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/organizations/default/users", - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{ - // { - // Name: roles.EditorRoleName, - // Organization: "default", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "editor", - // "organization": "default" - // } - // ] - //}`, - // }, - // }, - // { - // name: "POST /users", - // subName: "Create a New User with SuperAdmin status; SuperAdminNewUsers is false; User on Principal is a SuperAdmin", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/organizations/default/users", - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{ - // { - // Name: roles.EditorRoleName, - // Organization: "default", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": false, - // "roles": [ - // { - // "name": "editor", - // "organization": "default" - // } - // ] - //}`, - // }, - // }, - // { - // name: "POST /users", - // subName: "Create a New User with SuperAdmin status; SuperAdminNewUsers is false; User on Principal is Admin, but not a SuperAdmin", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/organizations/default/users", - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{ - // { - // Name: roles.EditorRoleName, - // Organization: "default", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": false, - // "roles": [ - // { - // "name": "editor", - // "organization": "default" - // } - // ] - //}`, - // }, - // }, - // { - // name: "POST /users", - // subName: "Create a New User with SuperAdmin status; SuperAdminNewUsers is true; User on Principal is Admin, but not a SuperAdmin", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/organizations/default/users", - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: roles.EditorRoleName, - // Organization: "default", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 401, - // body: ` - //{ - // "code": 401, - // "message": "user does not have authorization required to set SuperAdmin status. See https://github.com/influxdata/influxdb/chronograf/issues/2601 for more information." - //}`, - // }, - // }, - // { - // name: "POST /users", - // subName: "Create a New User with in multiple organizations; User on Principal is a SuperAdmin with raw query param", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/users", - // payload: &chronograf.User{ - // Name: "user", - // Provider: "provider", - // Scheme: "oauth2", - // Roles: []chronograf.Role{ - // { - // Name: roles.EditorRoleName, - // Organization: "default", - // }, - // { - // Name: roles.EditorRoleName, - // Organization: "1", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users/2" - // }, - // "id": "2", - // "name": "user", - // "provider": "provider", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "editor", - // "organization": "default" - // }, - // { - // "name": "editor", - // "organization": "1" - // } - // ] - //}`, - // }, - // }, - // { - // name: "PATCH /users", - // subName: "Update user to have no roles", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PATCH", - // path: "/chronograf/v1/users/1", - // payload: map[string]interface{}{ - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": []chronograf.Role{}, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // ] - //}`, - // }, - // }, - // { - // name: "PATCH /users", - // subName: "Update user roles with wildcard", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PATCH", - // path: "/chronograf/v1/users/1", - // payload: &chronograf.User{ - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: roles.AdminRoleName, - // Organization: "default", - // }, - // { - // Name: roles.WildcardRoleName, - // Organization: "1", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // }, - // { - // "name": "viewer", - // "organization": "1" - // } - // ] - //}`, - // }, - // }, - // { - // name: "PATCH /users/1", - // subName: "SuperAdmin modifying their own status", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PATCH", - // path: "/chronograf/v1/organizations/default/users/1", - // payload: map[string]interface{}{ - // "id": "1", - // "superAdmin": false, - // "roles": []interface{}{ - // map[string]interface{}{ - // "name": "admin", - // "organization": "default", - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: http.StatusUnauthorized, - // body: ` - //{ - // "code": 401, - // "message": "user cannot modify their own SuperAdmin status" - //} - //`, - // }, - // }, - // { - // name: "GET /organization/default/users", - // subName: "Organization not set explicitly on principal", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Organizations: []chronograf.Organization{}, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/organizations/default/users", - // principal: oauth2.Principal{ - // Organization: "", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/organizations/default/users" - // }, - // "users": [ - // { - // "links": { - // "self": "/chronograf/v1/organizations/default/users/1" - // }, - // "id": "1", - // "name": "billibob", - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // } - // ] - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "PUT /me", - // subName: "Change SuperAdmins current organization to org they dont belong to", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PUT", - // path: "/chronograf/v1/me", - // payload: map[string]string{ - // "organization": "1", - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "id": "1", - // "name": "billibob", - // "roles": [ - // { - // "name": "admin", - // "organization": "default" - // }, - // { - // "name": "viewer", - // "organization": "1" - // } - // ], - // "provider": "github", - // "scheme": "oauth2", - // "superAdmin": true, - // "links": { - // "self": "/chronograf/v1/organizations/1/users/1" - // }, - // "organizations": [ - // { - // "id": "1", - // "name": "Sweet", - // "defaultRole": "viewer" - // }, - // { - // "id": "default", - // "name": "Default", - // "defaultRole": "member" - // } - // ], - // "currentOrganization": { - // "id": "1", - // "name": "Sweet", - // "defaultRole": "viewer" - // } - //}`, - // }, - // }, - // { - // name: "PUT /me", - // subName: "Change Admin current organization to org they dont belong to", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PUT", - // path: "/chronograf/v1/me", - // payload: map[string]string{ - // "organization": "1", - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 403, - // body: ` - // { - // "code": 403, - // "message": "user not found" - //}`, - // }, - // }, - // { - // name: "GET /me", - // subName: "New user hits me for the first time", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Mappings: []chronograf.Mapping{ - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "influxdata", - // }, - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "2", - // Organization: "2", - // Provider: "github", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "3", - // Organization: "3", - // Provider: "auth0", - // Scheme: "ldap", - // ProviderOrganization: "*", - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // { - // ID: "2", - // Name: "What", - // DefaultRole: roles.EditorRoleName, - // }, - // { - // ID: "3", - // Name: "Okay", - // DefaultRole: roles.AdminRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{}, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/me", - // principal: oauth2.Principal{ - // Subject: "billietta", - // Issuer: "github", - // Group: "influxdata,idk,mimi", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "id": "2", - // "name": "billietta", - // "roles": [ - // { - // "name": "viewer", - // "organization": "1" - // }, - // { - // "name": "editor", - // "organization": "2" - // }, - // { - // "name": "member", - // "organization": "default" - // } - // ], - // "provider": "github", - // "scheme": "oauth2", - // "links": { - // "self": "/chronograf/v1/organizations/default/users/2" - // }, - // "organizations": [ - // { - // "id": "1", - // "name": "Sweet", - // "defaultRole": "viewer" - // }, - // { - // "id": "2", - // "name": "What", - // "defaultRole": "editor" - // }, - // { - // "id": "default", - // "name": "Default", - // "defaultRole": "member" - // } - // ], - // "currentOrganization": { - // "id": "default", - // "name": "Default", - // "defaultRole": "member" - // } - //} - //`, - // }, - // }, - // { - // name: "GET /mappings", - // subName: "get all mappings", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Mappings: []chronograf.Mapping{ - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "influxdata", - // }, - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "2", - // Organization: "2", - // Provider: "github", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "3", - // Organization: "3", - // Provider: "auth0", - // Scheme: "ldap", - // ProviderOrganization: "*", - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // { - // ID: "2", - // Name: "What", - // DefaultRole: roles.EditorRoleName, - // }, - // { - // ID: "3", - // Name: "Okay", - // DefaultRole: roles.AdminRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/mappings", - // principal: oauth2.Principal{ - // Subject: "billibob", - // Issuer: "github", - // Group: "influxdata,idk,mimi", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/mappings" - // }, - // "mappings": [ - // { - // "links": { - // "self": "/chronograf/v1/mappings/1" - // }, - // "id": "1", - // "organizationId": "1", - // "provider": "*", - // "scheme": "*", - // "providerOrganization": "influxdata" - // }, - // { - // "links": { - // "self": "/chronograf/v1/mappings/2" - // }, - // "id": "2", - // "organizationId": "1", - // "provider": "*", - // "scheme": "*", - // "providerOrganization": "*" - // }, - // { - // "links": { - // "self": "/chronograf/v1/mappings/3" - // }, - // "id": "3", - // "organizationId": "2", - // "provider": "github", - // "scheme": "*", - // "providerOrganization": "*" - // }, - // { - // "links": { - // "self": "/chronograf/v1/mappings/4" - // }, - // "id": "4", - // "organizationId": "3", - // "provider": "auth0", - // "scheme": "ldap", - // "providerOrganization": "*" - // }, - // { - // "links": { - // "self": "/chronograf/v1/mappings/default" - // }, - // "id": "default", - // "organizationId": "default", - // "provider": "*", - // "scheme": "*", - // "providerOrganization": "*" - // } - // ] - //} - //`, - // }, - // }, - // { - // name: "GET /mappings", - // subName: "get all mappings - user is not super admin", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Mappings: []chronograf.Mapping{ - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "influxdata", - // }, - // { - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "2", - // Organization: "2", - // Provider: "github", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // { - // ID: "3", - // Organization: "3", - // Provider: "auth0", - // Scheme: "ldap", - // ProviderOrganization: "*", - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // { - // ID: "2", - // Name: "What", - // DefaultRole: roles.EditorRoleName, - // }, - // { - // ID: "3", - // Name: "Okay", - // DefaultRole: roles.AdminRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/mappings", - // principal: oauth2.Principal{ - // Subject: "billibob", - // Issuer: "github", - // Group: "influxdata,idk,mimi", - // }, - // }, - // wants: wants{ - // statusCode: 403, - // body: ` - //{ - // "code": 403, - // "message": "User is not authorized" - //} - //`, - // }, - // }, - // { - // name: "POST /mappings", - // subName: "create new mapping", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Mappings: []chronograf.Mapping{}, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "POST", - // path: "/chronograf/v1/mappings", - // payload: &chronograf.Mapping{ - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "influxdata", - // }, - // principal: oauth2.Principal{ - // Subject: "billibob", - // Issuer: "github", - // Group: "influxdata,idk,mimi", - // }, - // }, - // wants: wants{ - // statusCode: 201, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/mappings/1" - // }, - // "id": "1", - // "organizationId": "1", - // "provider": "*", - // "scheme": "*", - // "providerOrganization": "influxdata" - //} - //`, - // }, - // }, - // { - // name: "PUT /mappings", - // subName: "update new mapping", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: false, - // }, - // }, - // Mappings: []chronograf.Mapping{ - // chronograf.Mapping{ - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "influxdata", - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "Sweet", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PUT", - // path: "/chronograf/v1/mappings/1", - // payload: &chronograf.Mapping{ - // ID: "1", - // Organization: "1", - // Provider: "*", - // Scheme: "*", - // ProviderOrganization: "*", - // }, - // principal: oauth2.Principal{ - // Subject: "billibob", - // Issuer: "github", - // Group: "influxdata,idk,mimi", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "links": { - // "self": "/chronograf/v1/mappings/1" - // }, - // "id": "1", - // "organizationId": "1", - // "provider": "*", - // "scheme": "*", - // "providerOrganization": "*" - //} - //`, - // }, - // }, - // { - // name: "GET /org_config", - // subName: "default org", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/org_config", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - // { - // "links": { - // "self": "\/chronograf\/v1\/org_config", - // "logViewer": "\/chronograf\/v1\/org_config\/logviewer" - // }, - // "organization": "default", - // "logViewer": { - // "columns": [ - // { - // "name": "time", - // "position": 0, - // "encodings": [ - // { - // "type": "visibility", - // "value": "hidden" - // } - // ] - // }, - // { - // "name": "severity", - // "position": 1, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "label", - // "value": "icon" - // }, - // { - // "type": "label", - // "value": "text" - // }, - // { - // "type": "color", - // "value": "ruby", - // "name": "emerg" - // }, - // { - // "type": "color", - // "value": "fire", - // "name": "alert" - // }, - // { - // "type": "color", - // "value": "curacao", - // "name": "crit" - // }, - // { - // "type": "color", - // "value": "tiger", - // "name": "err" - // }, - // { - // "type": "color", - // "value": "pineapple", - // "name": "warning" - // }, - // { - // "type": "color", - // "value": "rainforest", - // "name": "notice" - // }, - // { - // "type": "color", - // "value": "star", - // "name": "info" - // }, - // { - // "type": "color", - // "value": "wolf", - // "name": "debug" - // } - // ] - // }, - // { - // "name": "timestamp", - // "position": 2, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "message", - // "position": 3, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "facility", - // "position": 4, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "procid", - // "position": 5, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "displayName", - // "value": "Proc ID" - // } - // ] - // }, - // { - // "name": "appname", - // "position": 6, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "displayName", - // "value": "Application" - // } - // ] - // }, - // { - // "name": "host", - // "position": 7, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // } - // ] - // } - // } - // `, - // }, - // }, - // { - // name: "GET /org_config/logviewer", - // subName: "default org", - // fields: fields{ - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/org_config/logviewer", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - // { - // "links": { - // "self": "\/chronograf\/v1\/org_config/logviewer" - // }, - // "columns": [ - // { - // "name": "time", - // "position": 0, - // "encodings": [ - // { - // "type": "visibility", - // "value": "hidden" - // } - // ] - // }, - // { - // "name": "severity", - // "position": 1, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "label", - // "value": "icon" - // }, - // { - // "type": "label", - // "value": "text" - // }, - // { - // "type": "color", - // "value": "ruby", - // "name": "emerg" - // }, - // { - // "type": "color", - // "value": "fire", - // "name": "alert" - // }, - // { - // "type": "color", - // "value": "curacao", - // "name": "crit" - // }, - // { - // "type": "color", - // "value": "tiger", - // "name": "err" - // }, - // { - // "type": "color", - // "value": "pineapple", - // "name": "warning" - // }, - // { - // "type": "color", - // "value": "rainforest", - // "name": "notice" - // }, - // { - // "type": "color", - // "value": "star", - // "name": "info" - // }, - // { - // "type": "color", - // "value": "wolf", - // "name": "debug" - // } - // ] - // }, - // { - // "name": "timestamp", - // "position": 2, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "message", - // "position": 3, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "facility", - // "position": 4, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "procid", - // "position": 5, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "displayName", - // "value": "Proc ID" - // } - // ] - // }, - // { - // "name": "appname", - // "position": 6, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "displayName", - // "value": "Application" - // } - // ] - // }, - // { - // "name": "host", - // "position": 7, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // } - // ] - // } - // `, - // }, - // }, - // { - // name: "PUT /org_config/logviewer", - // subName: "default org", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "PUT", - // path: "/chronograf/v1/org_config/logviewer", - // payload: &chronograf.LogViewerConfig{ - // Columns: []chronograf.LogViewerColumn{ - // { - // Name: "time", - // Position: 0, - // Encodings: []chronograf.ColumnEncoding{ - // { - // Type: "visibility", - // Value: "hidden", - // }, - // }, - // }, - // { - // Name: "severity", - // Position: 1, - // Encodings: []chronograf.ColumnEncoding{ - // - // { - // Type: "visibility", - // Value: "visible", - // }, - // { - // Type: "label", - // Value: "icon", - // }, - // { - // Type: "color", - // Name: "emerg", - // Value: "ruby", - // }, - // { - // Type: "color", - // Name: "alert", - // Value: "fire", - // }, - // { - // Type: "color", - // Name: "crit", - // Value: "curacao", - // }, - // { - // Type: "color", - // Name: "err", - // Value: "tiger", - // }, - // { - // Type: "color", - // Name: "warning", - // Value: "pineapple", - // }, - // { - // Type: "color", - // Name: "notice", - // Value: "wolf", - // }, - // { - // Type: "color", - // Name: "info", - // Value: "wolf", - // }, - // { - // Type: "color", - // Name: "debug", - // Value: "wolf", - // }, - // }, - // }, - // { - // Name: "timestamp", - // Position: 3, - // Encodings: []chronograf.ColumnEncoding{ - // - // { - // Type: "visibility", - // Value: "visible", - // }, - // }, - // }, - // { - // Name: "message", - // Position: 2, - // Encodings: []chronograf.ColumnEncoding{ - // - // { - // Type: "visibility", - // Value: "visible", - // }, - // }, - // }, - // { - // Name: "facility", - // Position: 4, - // Encodings: []chronograf.ColumnEncoding{ - // - // { - // Type: "visibility", - // Value: "visible", - // }, - // }, - // }, - // { - // Name: "procid", - // Position: 5, - // Encodings: []chronograf.ColumnEncoding{ - // - // { - // Type: "visibility", - // Value: "hidden", - // }, - // { - // Type: "displayName", - // Value: "ProcID!", - // }, - // }, - // }, - // { - // Name: "appname", - // Position: 6, - // Encodings: []chronograf.ColumnEncoding{ - // { - // Type: "visibility", - // Value: "visible", - // }, - // { - // Type: "displayName", - // Value: "Application", - // }, - // }, - // }, - // { - // Name: "host", - // Position: 7, - // Encodings: []chronograf.ColumnEncoding{ - // { - // Type: "visibility", - // Value: "visible", - // }, - // }, - // }, - // }, - // }, - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - // { - // "links": { - // "self": "\/chronograf\/v1\/org_config\/logviewer" - // }, - // "columns": [ - // { - // "name": "time", - // "position": 0, - // "encodings": [ - // { - // "type": "visibility", - // "value": "hidden" - // } - // ] - // }, - // { - // "name": "severity", - // "position": 1, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "label", - // "value": "icon" - // }, - // { - // "type": "color", - // "value": "ruby", - // "name": "emerg" - // }, - // { - // "type": "color", - // "value": "fire", - // "name": "alert" - // }, - // { - // "type": "color", - // "value": "curacao", - // "name": "crit" - // }, - // { - // "type": "color", - // "value": "tiger", - // "name": "err" - // }, - // { - // "type": "color", - // "value": "pineapple", - // "name": "warning" - // }, - // { - // "type": "color", - // "value": "wolf", - // "name": "notice" - // }, - // { - // "type": "color", - // "value": "wolf", - // "name": "info" - // }, - // { - // "type": "color", - // "value": "wolf", - // "name": "debug" - // } - // ] - // }, - // { - // "name": "timestamp", - // "position": 3, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "message", - // "position": 2, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "facility", - // "position": 4, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // }, - // { - // "name": "procid", - // "position": 5, - // "encodings": [ - // { - // "type": "visibility", - // "value": "hidden" - // }, - // { - // "type": "displayName", - // "value": "ProcID!" - // } - // ] - // }, - // { - // "name": "appname", - // "position": 6, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // }, - // { - // "type": "displayName", - // "value": "Application" - // } - // ] - // }, - // { - // "name": "host", - // "position": 7, - // "encodings": [ - // { - // "type": "visibility", - // "value": "visible" - // } - // ] - // } - // ] - // } - // `, - // }, - // }, - // { - // name: "GET /", - // subName: "signed into default org", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: true, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/", - // principal: oauth2.Principal{ - // Organization: "default", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "layouts": "/chronograf/v1/layouts", - // "cells": "/chronograf/v2/cells", - // "users": "/chronograf/v1/organizations/default/users", - // "allUsers": "/chronograf/v1/users", - // "organizations": "/chronograf/v1/organizations", - // "mappings": "/chronograf/v1/mappings", - // "sources": "/chronograf/v1/sources", - // "me": "/chronograf/v1/me", - // "environment": "/chronograf/v1/env", - // "dashboards": "/chronograf/v1/dashboards", - // "dashboardsv2":"/chronograf/v2/dashboards", - // "config": { - // "self": "/chronograf/v1/config", - // "auth": "/chronograf/v1/config/auth" - // }, - // "auth": [ - // { - // "name": "github", - // "label": "Github", - // "login": "/oauth/github/login", - // "logout": "/oauth/github/logout", - // "callback": "/oauth/github/callback" - // } - // ], - // "logout": "/oauth/logout", - // "external": { - // "statusFeed": "" - // }, - // "orgConfig": { - // "logViewer": "/chronograf/v1/org_config/logviewer", - // "self": "/chronograf/v1/org_config" - // }, - // "flux": { - // "ast": "/chronograf/v1/flux/ast", - // "self": "/chronograf/v1/flux", - // "suggestions": "/chronograf/v1/flux/suggestions" - // } - //} - //`, - // }, - // }, - // { - // name: "GET /", - // subName: "signed into org 1", - // fields: fields{ - // Config: &chronograf.Config{ - // Auth: chronograf.AuthConfig{ - // SuperAdminNewUsers: true, - // }, - // }, - // Organizations: []chronograf.Organization{ - // { - // ID: "1", - // Name: "cool", - // DefaultRole: roles.ViewerRoleName, - // }, - // }, - // Users: []chronograf.User{ - // { - // ID: 1, // This is artificial, but should be reflective of the users actual ID - // Name: "billibob", - // Provider: "github", - // Scheme: "oauth2", - // SuperAdmin: false, - // Roles: []chronograf.Role{ - // { - // Name: "admin", - // Organization: "default", - // }, - // { - // Name: "member", - // Organization: "1", - // }, - // }, - // }, - // }, - // }, - // args: args{ - // server: &server.Server{ - // GithubClientID: "not empty", - // GithubClientSecret: "not empty", - // }, - // method: "GET", - // path: "/chronograf/v1/", - // principal: oauth2.Principal{ - // Organization: "1", - // Subject: "billibob", - // Issuer: "github", - // }, - // }, - // wants: wants{ - // statusCode: 200, - // body: ` - //{ - // "layouts": "/chronograf/v1/layouts", - // "cells": "/chronograf/v2/cells", - // "users": "/chronograf/v1/organizations/1/users", - // "allUsers": "/chronograf/v1/users", - // "organizations": "/chronograf/v1/organizations", - // "mappings": "/chronograf/v1/mappings", - // "sources": "/chronograf/v1/sources", - // "me": "/chronograf/v1/me", - // "environment": "/chronograf/v1/env", - // "dashboards": "/chronograf/v1/dashboards", - // "dashboardsv2":"/chronograf/v2/dashboards", - // "config": { - // "self": "/chronograf/v1/config", - // "auth": "/chronograf/v1/config/auth" - // }, - // "orgConfig": { - // "logViewer": "/chronograf/v1/org_config/logviewer", - // "self": "/chronograf/v1/org_config" - // }, - // "auth": [ - // { - // "name": "github", - // "label": "Github", - // "login": "/oauth/github/login", - // "logout": "/oauth/github/logout", - // "callback": "/oauth/github/callback" - // } - // ], - // "logout": "/oauth/logout", - // "external": { - // "statusFeed": "" - // }, - // "flux": { - // "ast": "/chronograf/v1/flux/ast", - // "self": "/chronograf/v1/flux", - // "suggestions": "/chronograf/v1/flux/suggestions" - // } - //} - //`, - // }, - // }, - } - - for _, tt := range tests { - testName := fmt.Sprintf("%s: %s", tt.name, tt.subName) - t.Run(testName, func(t *testing.T) { - ctx := context.TODO() - // Create Test Server - host, port := hostAndPort() - tt.args.server.Host = host - tt.args.server.Port = port - - // Use testdata directory for the canned data - tt.args.server.CannedPath = "testdata" - tt.args.server.ResourcesPath = "testdata" - - // This is so that we can use staticly generate jwts - tt.args.server.TokenSecret = "secret" - - // Endpoint for validating RSA256 signatures when using id_token parsing for ADFS - tt.args.server.JwksURL = "" - - boltFile := newBoltFile() - tt.args.server.BoltPath = boltFile - - // Prepopulate BoltDB Database for Server - boltdb := bolt.NewClient() - boltdb.Path = boltFile - - logger := &chronograf.NoopLogger{} - build := chronograf.BuildInfo{ - Version: "pre-1.4.0.0", - Commit: "", - } - _ = boltdb.Open(ctx, logger, build) - - if tt.fields.Config != nil { - if err := boltdb.ConfigStore.Update(ctx, tt.fields.Config); err != nil { - t.Fatalf("failed to update global application config %v", err) - return - } - } - - // Populate Organizations - for i, mapping := range tt.fields.Mappings { - o, err := boltdb.MappingsStore.Add(ctx, &mapping) - if err != nil { - t.Fatalf("failed to add mapping: %v", err) - return - } - tt.fields.Mappings[i] = *o - } - - // Populate Organizations - for i, organization := range tt.fields.Organizations { - o, err := boltdb.OrganizationsStore.Add(ctx, &organization) - if err != nil { - t.Fatalf("failed to add organization: %v", err) - return - } - tt.fields.Organizations[i] = *o - } - - // Populate Users - for i, user := range tt.fields.Users { - u, err := boltdb.UsersStore.Add(ctx, &user) - if err != nil { - t.Fatalf("failed to add user: %v", err) - return - } - tt.fields.Users[i] = *u - } - - // Populate Sources - for i, source := range tt.fields.Sources { - s, err := boltdb.SourcesStore.Add(ctx, source) - if err != nil { - t.Fatalf("failed to add source: %v", err) - return - } - tt.fields.Sources[i] = s - } - - // Populate Servers - for i, server := range tt.fields.Servers { - s, err := boltdb.ServersStore.Add(ctx, server) - if err != nil { - t.Fatalf("failed to add server: %v", err) - return - } - tt.fields.Servers[i] = s - } - - // Populate Layouts - for i, layout := range tt.fields.Layouts { - l, err := boltdb.LayoutsStore.Add(ctx, layout) - if err != nil { - t.Fatalf("failed to add layout: %v", err) - return - } - tt.fields.Layouts[i] = l - } - - // Populate Dashboards - for i, dashboard := range tt.fields.Dashboards { - d, err := boltdb.DashboardsStore.Add(ctx, dashboard) - if err != nil { - t.Fatalf("failed to add dashboard: %v", err) - return - } - tt.fields.Dashboards[i] = d - } - - _ = boltdb.Close() - - go tt.args.server.Serve(ctx) - serverURL := fmt.Sprintf("http://%v:%v%v", host, port, tt.args.path) - - // Wait for the server to come online - timeout := time.Now().Add(30 * time.Second) - for { - _, err := http.Get(serverURL + "/swagger.json") - if err == nil { - break - } - if time.Now().After(timeout) { - t.Fatalf("failed to start server") - return - } - } - - // Set the Expiry time on the principal - tt.args.principal.IssuedAt = time.Now() - tt.args.principal.ExpiresAt = time.Now().Add(10 * time.Second) - - // Construct HTTP Request - buf, _ := json.Marshal(tt.args.payload) - reqBody := ioutil.NopCloser(bytes.NewReader(buf)) - req, _ := http.NewRequest(tt.args.method, serverURL, reqBody) - token, _ := oauth2.NewJWT(tt.args.server.TokenSecret, tt.args.server.JwksURL).Create(ctx, tt.args.principal) - req.AddCookie(&http.Cookie{ - Name: "session", - Value: string(token), - HttpOnly: true, - Path: "/", - }) - - // Make actual http request - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatalf("failed to make httprequest: %v", err) - return - } - - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf( - "%s %s Status Code = %v, want %v", - tt.args.method, - tt.args.path, - resp.StatusCode, - tt.wants.statusCode, - ) - } - - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf( - "%s %s Content Type = %v, want %v", - tt.args.method, - tt.args.path, - content, - tt.wants.contentType, - ) - } - - if eq, err := jsonEqual(tt.wants.body, string(body)); err != nil || !eq { - t.Errorf( - "%s %s Body = %v, want %v", - tt.args.method, - tt.args.path, - string(body), - tt.wants.body, - ) - } - - tt.args.server.Listener.Close() - }) - } -} diff --git a/chronograf/integrations/testdata/example.kap b/chronograf/integrations/testdata/example.kap deleted file mode 100644 index 611216d081..0000000000 --- a/chronograf/integrations/testdata/example.kap +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "5000", - "srcID": "5000", - "name": "Kapa 1", - "url": "http://localhost:9092", - "active": true, - "organization": "howdy" -} diff --git a/chronograf/integrations/testdata/example.org b/chronograf/integrations/testdata/example.org deleted file mode 100644 index 21031e50b1..0000000000 --- a/chronograf/integrations/testdata/example.org +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "howdy", - "name": "An Organization", - "defaultRole": "viewer" -} diff --git a/chronograf/integrations/testdata/example.src b/chronograf/integrations/testdata/example.src deleted file mode 100644 index 2e92c7fc65..0000000000 --- a/chronograf/integrations/testdata/example.src +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "5000", - "name": "Influx 1", - "username": "user1", - "password": "pass1", - "url": "http://localhost:8086", - "metaUrl": "http://metaurl.com", - "type": "influx-enterprise", - "insecureSkipVerify": false, - "default": true, - "telegraf": "telegraf", - "sharedSecret": "cubeapples", - "organization": "howdy" -} diff --git a/chronograf/integrations/testdata/mydash.dashboard b/chronograf/integrations/testdata/mydash.dashboard deleted file mode 100644 index 3e81b46dce..0000000000 --- a/chronograf/integrations/testdata/mydash.dashboard +++ /dev/null @@ -1,189 +0,0 @@ -{ - "id": 1000, - "cells": [ - { - "i": "8f61c619-dd9b-4761-8aa8-577f27247093", - "x": 0, - "y": 0, - "w": 11, - "h": 5, - "name": "Untitled Cell", - "queries": [ - { - "query": "SELECT mean(\"value\") AS \"mean_value\" FROM \"telegraf\".\"autogen\".\"cpg\" WHERE time \u003e :dashboardTime: GROUP BY time(:interval:) FILL(null)", - "queryConfig": { - "id": "b20baa61-bacb-4a17-b27d-b904a0d18114", - "database": "telegraf", - "measurement": "cpg", - "retentionPolicy": "autogen", - "fields": [ - { - "value": "mean", - "type": "func", - "alias": "mean_value", - "args": [ - { - "value": "value", - "type": "field", - "alias": "" - } - ] - } - ], - "tags": {}, - "groupBy": { - "time": "auto", - "tags": [] - }, - "areTagsAccepted": true, - "fill": "null", - "rawText": null, - "range": null, - "shifts": [] - }, - "source": "/chronograf/v1/sources/2" - } - ], - "axes": { - "x": { - "bounds": [], - "label": "", - "prefix": "", - "suffix": "", - "base": "10", - "scale": "linear" - }, - "y": { - "bounds": [], - "label": "", - "prefix": "", - "suffix": "", - "base": "10", - "scale": "linear" - }, - "y2": { - "bounds": [], - "label": "", - "prefix": "", - "suffix": "", - "base": "10", - "scale": "linear" - } - }, - "type": "line", - "colors": [ - { - "id": "0", - "type": "min", - "hex": "#00C9FF", - "name": "laser", - "value": "0" - }, - { - "id": "1", - "type": "max", - "hex": "#9394FF", - "name": "comet", - "value": "100" - } - ], - "legend": { - "type": "static", - "orientation": "bottom" - } - } - ], - "templates": [ - { - "tempVar": ":dbs:", - "values": [ - { - "value": "_internal", - "type": "database", - "selected": true - }, - { - "value": "telegraf", - "type": "database", - "selected": false - }, - { - "value": "tensorflowdb", - "type": "database", - "selected": false - }, - { - "value": "pushgateway", - "type": "database", - "selected": false - }, - { - "value": "node_exporter", - "type": "database", - "selected": false - }, - { - "value": "mydb", - "type": "database", - "selected": false - }, - { - "value": "tiny", - "type": "database", - "selected": false - }, - { - "value": "blah", - "type": "database", - "selected": false - }, - { - "value": "test", - "type": "database", - "selected": false - }, - { - "value": "chronograf", - "type": "database", - "selected": false - }, - { - "value": "db_name", - "type": "database", - "selected": false - }, - { - "value": "demo", - "type": "database", - "selected": false - }, - { - "value": "eeg", - "type": "database", - "selected": false - }, - { - "value": "solaredge", - "type": "database", - "selected": false - }, - { - "value": "zipkin", - "type": "database", - "selected": false - } - ], - "id": "e7e498bf-5869-4874-9071-24628a2cda63", - "type": "databases", - "label": "", - "query": { - "influxql": "SHOW DATABASES", - "measurement": "", - "tagKey": "", - "fieldKey": "" - } - } - ], - "name": "Name This Dashboard", - "organization": "howdy" - } diff --git a/chronograf/integrations/utils.go b/chronograf/integrations/utils.go deleted file mode 100644 index 2069c09595..0000000000 --- a/chronograf/integrations/utils.go +++ /dev/null @@ -1,54 +0,0 @@ -package integrations - -import ( - "encoding/json" - "io/ioutil" - "net/http/httptest" - "net/url" - "strconv" - "strings" - - "github.com/google/go-cmp/cmp" -) - -func hostAndPort() (string, int) { - s := httptest.NewServer(nil) - defer s.Close() - - u, err := url.Parse(s.URL) - if err != nil { - panic(err) - } - xs := strings.Split(u.Host, ":") - host := xs[0] - portStr := xs[1] - port, err := strconv.Atoi(portStr) - if err != nil { - panic(err) - } - return host, port - -} - -func newBoltFile() string { - f, err := ioutil.TempFile("", "chronograf-bolt-") - if err != nil { - panic(err) - } - f.Close() - - return f.Name() -} - -func jsonEqual(s1, s2 string) (eq bool, err error) { - var o1, o2 interface{} - - if err = json.Unmarshal([]byte(s1), &o1); err != nil { - return - } - if err = json.Unmarshal([]byte(s2), &o2); err != nil { - return - } - - return cmp.Equal(o1, o2), nil -} diff --git a/chronograf/server/Makefile b/chronograf/server/Makefile deleted file mode 100644 index 1ebe969917..0000000000 --- a/chronograf/server/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# List any generated files here -TARGETS = swagger_gen.go -# List any source files used to generate the targets here -SOURCES = swagger.json swagger.go -# List any directories that have their own Makefile here -SUBDIRS = - -# Default target -all: $(SUBDIRS) $(TARGETS) - -# Recurse into subdirs for same make goal -$(SUBDIRS): - $(MAKE) -C $@ $(MAKECMDGOALS) - -# Clean all targets recursively -clean: $(SUBDIRS) - rm -f $(TARGETS) - -# Define go generate if not already defined -GO_GENERATE := go generate - -# Run go generate for the targets -$(TARGETS): $(SOURCES) - $(GO_GENERATE) -x - -.PHONY: all clean $(SUBDIRS) diff --git a/chronograf/server/TODO.go b/chronograf/server/TODO.go deleted file mode 100644 index 36fba3417f..0000000000 --- a/chronograf/server/TODO.go +++ /dev/null @@ -1,13 +0,0 @@ -package server - -import ( - "errors" -) - -// The functions defined in this file are placeholders when the binary is compiled -// without assets. - -// Asset returns an error stating no assets were included in the binary. -func Asset(string) ([]byte, error) { - return nil, errors.New("no assets included in binary") -} diff --git a/chronograf/server/annotations.go b/chronograf/server/annotations.go deleted file mode 100644 index 262ff80fd1..0000000000 --- a/chronograf/server/annotations.go +++ /dev/null @@ -1,452 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/influx" -) - -const ( - since = "since" - until = "until" - timeMilliFormat = "2006-01-02T15:04:05.999Z07:00" -) - -type annotationLinks struct { - Self string `json:"self"` // Self link mapping to this resource -} - -type annotationResponse struct { - ID string `json:"id"` // ID is the unique annotation identifier - StartTime string `json:"startTime"` // StartTime in RFC3339 of the start of the annotation - EndTime string `json:"endTime"` // EndTime in RFC3339 of the end of the annotation - Text string `json:"text"` // Text is the associated user-facing text describing the annotation - Type string `json:"type"` // Type describes the kind of annotation - Links annotationLinks `json:"links"` -} - -func newAnnotationResponse(src chronograf.Source, a *chronograf.Annotation) annotationResponse { - base := "/chronograf/v1/sources" - res := annotationResponse{ - ID: a.ID, - StartTime: a.StartTime.UTC().Format(timeMilliFormat), - EndTime: a.EndTime.UTC().Format(timeMilliFormat), - Text: a.Text, - Type: a.Type, - Links: annotationLinks{ - Self: fmt.Sprintf("%s/%d/annotations/%s", base, src.ID, a.ID), - }, - } - - if a.EndTime.IsZero() { - res.EndTime = "" - } - - return res -} - -type annotationsResponse struct { - Annotations []annotationResponse `json:"annotations"` -} - -func newAnnotationsResponse(src chronograf.Source, as []chronograf.Annotation) annotationsResponse { - annotations := make([]annotationResponse, len(as)) - for i, a := range as { - annotations[i] = newAnnotationResponse(src, &a) - } - return annotationsResponse{ - Annotations: annotations, - } -} - -func validAnnotationQuery(query url.Values) (startTime, stopTime time.Time, err error) { - start := query.Get(since) - if start == "" { - return time.Time{}, time.Time{}, fmt.Errorf("since parameter is required") - } - - startTime, err = time.Parse(timeMilliFormat, start) - if err != nil { - return - } - - // if until isn't stated, the default time is now - stopTime = time.Now() - stop := query.Get(until) - if stop != "" { - stopTime, err = time.Parse(timeMilliFormat, stop) - if err != nil { - return time.Time{}, time.Time{}, err - } - } - if startTime.After(stopTime) { - startTime, stopTime = stopTime, startTime - } - return startTime, stopTime, nil -} - -// Annotations returns all annotations within the annotations store -func (s *Service) Annotations(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - start, stop, err := validAnnotationQuery(r.URL.Query()) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - store := influx.NewAnnotationStore(ts) - annotations, err := store.All(ctx, start, stop) - if err != nil { - msg := fmt.Errorf("error loading annotations: %v", err) - unknownErrorWithMessage(w, msg, s.Logger) - return - } - - res := newAnnotationsResponse(src, annotations) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// Annotation returns a specified annotation id within the annotations store -func (s *Service) Annotation(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - annoID, err := paramStr("aid", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - store := influx.NewAnnotationStore(ts) - anno, err := store.Get(ctx, annoID) - if err != nil { - if err != chronograf.ErrAnnotationNotFound { - msg := fmt.Errorf("error loading annotation: %v", err) - unknownErrorWithMessage(w, msg, s.Logger) - return - } - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newAnnotationResponse(src, anno) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -type newAnnotationRequest struct { - StartTime time.Time - EndTime time.Time - Text string `json:"text,omitempty"` // Text is the associated user-facing text describing the annotation - Type string `json:"type,omitempty"` // Type describes the kind of annotation -} - -func (ar *newAnnotationRequest) UnmarshalJSON(data []byte) error { - type Alias newAnnotationRequest - aux := &struct { - StartTime string `json:"startTime"` // StartTime is the time in rfc3339 milliseconds - EndTime string `json:"endTime"` // EndTime is the time in rfc3339 milliseconds - *Alias - }{ - Alias: (*Alias)(ar), - } - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - var err error - ar.StartTime, err = time.Parse(timeMilliFormat, aux.StartTime) - if err != nil { - return err - } - - ar.EndTime, err = time.Parse(timeMilliFormat, aux.EndTime) - if err != nil { - return err - } - - if ar.StartTime.After(ar.EndTime) { - ar.StartTime, ar.EndTime = ar.EndTime, ar.StartTime - } - - return nil -} - -func (ar *newAnnotationRequest) Annotation() *chronograf.Annotation { - return &chronograf.Annotation{ - StartTime: ar.StartTime, - EndTime: ar.EndTime, - Text: ar.Text, - Type: ar.Type, - } -} - -// NewAnnotation adds the annotation from a POST body to the annotations store -func (s *Service) NewAnnotation(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - var req newAnnotationRequest - if err = json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - store := influx.NewAnnotationStore(ts) - anno, err := store.Add(ctx, req.Annotation()) - if err != nil { - if err == chronograf.ErrUpstreamTimeout { - msg := "Timeout waiting for response" - Error(w, http.StatusRequestTimeout, msg, s.Logger) - return - } - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newAnnotationResponse(src, anno) - location(w, res.Links.Self) - encodeJSON(w, http.StatusCreated, res, s.Logger) -} - -// RemoveAnnotation removes the annotation from the time series source -func (s *Service) RemoveAnnotation(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - annoID, err := paramStr("aid", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - store := influx.NewAnnotationStore(ts) - if err = store.Delete(ctx, annoID); err != nil { - if err == chronograf.ErrUpstreamTimeout { - msg := "Timeout waiting for response" - Error(w, http.StatusRequestTimeout, msg, s.Logger) - return - } - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -type updateAnnotationRequest struct { - StartTime *time.Time `json:"startTime,omitempty"` // StartTime is the time in rfc3339 milliseconds - EndTime *time.Time `json:"endTime,omitempty"` // EndTime is the time in rfc3339 milliseconds - Text *string `json:"text,omitempty"` // Text is the associated user-facing text describing the annotation - Type *string `json:"type,omitempty"` // Type describes the kind of annotation -} - -// TODO: make sure that endtime is after starttime -func (u *updateAnnotationRequest) UnmarshalJSON(data []byte) error { - type Alias updateAnnotationRequest - aux := &struct { - StartTime *string `json:"startTime,omitempty"` - EndTime *string `json:"endTime,omitempty"` - *Alias - }{ - Alias: (*Alias)(u), - } - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - if aux.StartTime != nil { - tm, err := time.Parse(timeMilliFormat, *aux.StartTime) - if err != nil { - return err - } - u.StartTime = &tm - } - - if aux.EndTime != nil { - tm, err := time.Parse(timeMilliFormat, *aux.EndTime) - if err != nil { - return err - } - u.EndTime = &tm - } - - // Update must have at least one field set - if u.StartTime == nil && u.EndTime == nil && u.Text == nil && u.Type == nil { - return fmt.Errorf("update request must have at least one field") - } - - return nil -} - -// UpdateAnnotation overwrite an existing annotation -func (s *Service) UpdateAnnotation(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - annoID, err := paramStr("aid", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - store := influx.NewAnnotationStore(ts) - cur, err := store.Get(ctx, annoID) - if err != nil { - notFound(w, annoID, s.Logger) - return - } - - var req updateAnnotationRequest - if err = json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - if req.StartTime != nil { - cur.StartTime = *req.StartTime - } - if req.EndTime != nil { - cur.EndTime = *req.EndTime - } - if req.Text != nil { - cur.Text = *req.Text - } - if req.Type != nil { - cur.Type = *req.Type - } - - if err = store.Update(ctx, cur); err != nil { - if err == chronograf.ErrUpstreamTimeout { - msg := "Timeout waiting for response" - Error(w, http.StatusRequestTimeout, msg, s.Logger) - return - } - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newAnnotationResponse(src, cur) - location(w, res.Links.Self) - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/annotations_test.go b/chronograf/server/annotations_test.go deleted file mode 100644 index 23984b4139..0000000000 --- a/chronograf/server/annotations_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package server - -import ( - "bytes" - "context" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestService_Annotations(t *testing.T) { - type fields struct { - Store DataStore - TimeSeriesClient TimeSeriesClient - } - - tests := []struct { - name string - fields fields - w *httptest.ResponseRecorder - r *http.Request - ID string - want string - }{ - { - name: "error no id", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations", bytes.NewReader([]byte(`howdy`))), - want: `{"code":422,"message":"error converting ID "}`, - }, - { - name: "no since parameter", - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations", bytes.NewReader([]byte(`howdy`))), - want: `{"code":422,"message":"since parameter is required"}`, - }, - { - name: "invalid since parameter", - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=howdy", bytes.NewReader([]byte(`howdy`))), - want: `{"code":422,"message":"parsing time \"howdy\" as \"2006-01-02T15:04:05.999Z07:00\": cannot parse \"howdy\" as \"2006\""}`, - }, - { - name: "error is returned when get is an error", - fields: fields{ - Store: &mocks.Store{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{}, fmt.Errorf("error") - }, - }, - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))), - want: `{"code":404,"message":"ID 1 not found"}`, - }, - { - name: "error is returned connect is an error", - fields: fields{ - Store: &mocks.Store{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - }, - TimeSeriesClient: &mocks.TimeSeries{ - ConnectF: func(context.Context, *chronograf.Source) error { - return fmt.Errorf("error)") - }, - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))), - want: `{"code":400,"message":"unable to connect to source 1: error)"}`, - }, - { - name: "error returned when annotations are invalid", - fields: fields{ - Store: &mocks.Store{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - }, - TimeSeriesClient: &mocks.TimeSeries{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) { - return mocks.NewResponse(`{[]}`, nil), nil - }, - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))), - want: `{"code":500,"message":"unknown error: error loading annotations: invalid character '[' looking for beginning of object key string"}`, - }, - { - name: "error is returned connect is an error", - fields: fields{ - Store: &mocks.Store{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - }, - TimeSeriesClient: &mocks.TimeSeries{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) { - return mocks.NewResponse(`[ - { - "series": [ - { - "name": "annotations", - "columns": [ - "time", - "start_time", - "modified_time_ns", - "text", - "type", - "id" - ], - "values": [ - [ - 1516920177345000000, - 0, - 1516989242129417403, - "mytext", - "mytype", - "ea0aa94b-969a-4cd5-912a-5db61d502268" - ] - ] - } - ] - } - ]`, nil), nil - }, - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "/chronograf/v1/sources/1/annotations?since=1985-04-12T23:20:50.52Z", bytes.NewReader([]byte(`howdy`))), - want: `{"annotations":[{"id":"ea0aa94b-969a-4cd5-912a-5db61d502268","startTime":"1970-01-01T00:00:00Z","endTime":"2018-01-25T22:42:57.345Z","text":"mytext","type":"mytype","links":{"self":"/chronograf/v1/sources/1/annotations/ea0aa94b-969a-4cd5-912a-5db61d502268"}}]} -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.r = tt.r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: tt.ID, - }, - })) - s := &Service{ - Store: tt.fields.Store, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: mocks.NewLogger(), - } - s.Annotations(tt.w, tt.r) - got := tt.w.Body.String() - if got != tt.want { - t.Errorf("Annotations() got != want:\n%s\n%s", got, tt.want) - } - }) - } -} diff --git a/chronograf/server/assets.go b/chronograf/server/assets.go deleted file mode 100644 index 16f0ded8bd..0000000000 --- a/chronograf/server/assets.go +++ /dev/null @@ -1,58 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/dist" -) - -const ( - // Dir is prefix of the assets in the bindata - Dir = "../ui/build" - // Default is the default item to load if 404 - Default = "../ui/build/index.html" - // DebugDir is the prefix of the assets in development mode - DebugDir = "ui/build" - // DebugDefault is the default item to load if 404 - DebugDefault = "ui/build/index.html" - // DefaultContentType is the content-type to return for the Default file - DefaultContentType = "text/html; charset=utf-8" -) - -// AssetsOpts configures the asset middleware -type AssetsOpts struct { - // Develop when true serves assets from ui/build directory directly; false will use internal bindata. - Develop bool - // Logger will log the asset served - Logger chronograf.Logger -} - -// Assets creates a middleware that will serve a single page app. -func Assets(opts AssetsOpts) http.Handler { - var assets chronograf.Assets - if opts.Develop { - assets = &dist.DebugAssets{ - Dir: DebugDir, - Default: DebugDefault, - } - } else { - assets = &dist.BindataAssets{ - Prefix: Dir, - Default: Default, - DefaultContentType: DefaultContentType, - } - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if opts.Logger != nil { - opts.Logger. - WithField("component", "server"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL). - Info("Serving assets") - } - assets.Handler().ServeHTTP(w, r) - }) -} diff --git a/chronograf/server/auth.go b/chronograf/server/auth.go deleted file mode 100644 index fa38e74622..0000000000 --- a/chronograf/server/auth.go +++ /dev/null @@ -1,256 +0,0 @@ -package server - -import ( - "context" - "fmt" - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/organizations" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -// HasAuthorizedToken extracts the token from a request and validates it using the authenticator. -// It is used by routes that need access to the token to populate links request. -func HasAuthorizedToken(auth oauth2.Authenticator, r *http.Request) (oauth2.Principal, error) { - ctx := r.Context() - return auth.Validate(ctx, r) -} - -// AuthorizedToken extracts the token and validates; if valid the next handler -// will be run. The principal will be sent to the next handler via the request's -// Context. It is up to the next handler to determine if the principal has access. -// On failure, will return http.StatusForbidden. -func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next http.Handler) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := logger. - WithField("component", "token_auth"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL) - - ctx := r.Context() - // 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 { - log.Error("Invalid principal") - w.WriteHeader(http.StatusForbidden) - 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)) - }) -} - -// RawStoreAccess gives a super admin access to the data store without a facade. -func RawStoreAccess(logger chronograf.Logger, next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if isServer := hasServerContext(ctx); isServer { - next(w, r) - return - } - - log := logger. - WithField("component", "raw_store"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL) - - if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin { - r = r.WithContext(serverContext(ctx)) - } else { - log.Error("User making request is not a SuperAdmin") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - next(w, r) - } -} - -// AuthorizedUser extracts the user name and provider from context. If the -// user and provider can be found on the context, we look up the user by their -// name and provider. If the user is found, we verify that the user has at at -// least the role supplied. -func AuthorizedUser( - store DataStore, - useAuth bool, - role string, - logger chronograf.Logger, - next http.HandlerFunc, -) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - serverCtx := serverContext(ctx) - - log := logger. - WithField("component", "role_auth"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL) - - defaultOrg, err := store.Organizations(serverCtx).DefaultOrganization(serverCtx) - if err != nil { - log.Error(fmt.Sprintf("Failed to retrieve the default organization: %v", err)) - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - if !useAuth { - // If there is no auth, then set the organization id to be the default org id on context - // so that calls like hasOrganizationContext as used in Organization Config service - // method OrganizationConfig can successfully get the organization id - ctx = context.WithValue(ctx, organizations.ContextKey, defaultOrg.ID) - - // And if there is no auth, then give the user raw access to the DataStore - r = r.WithContext(serverContext(ctx)) - next(w, r) - return - } - - p, err := getValidPrincipal(ctx) - if err != nil { - log.Error("Failed to retrieve principal from context") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - scheme, err := getScheme(ctx) - if err != nil { - log.Error("Failed to retrieve scheme from context") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - // This is as if the user was logged into the default organization - if p.Organization == "" { - p.Organization = defaultOrg.ID - } - - // validate that the organization exists - _, err = store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization}) - if err != nil { - log.Error(fmt.Sprintf("Failed to retrieve organization %s from organizations store", p.Organization)) - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization) - // TODO: seems silly to look up a user twice - u, err := store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{ - Name: &p.Subject, - Provider: &p.Issuer, - Scheme: &scheme, - }) - - if err != nil { - log.Error("Failed to retrieve user") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - // In particular this is used by sever/users.go so that we know when and when not to - // allow users to make someone a super admin - ctx = context.WithValue(ctx, UserContextKey, u) - - if u.SuperAdmin { - // To access resources (servers, sources, databases, layouts) within a DataStore, - // an organization and a role are required even if you are a super admin or are - // not using auth. Every user's current organization is set on context to filter - // the resources accessed within a DataStore, including for super admin or when - // not using auth. In this way, a DataStore can treat all requests the same, - // including those from a super admin and when not using auth. - // - // As for roles, in the case of super admin or when not using auth, the user's - // role on context (though not on their JWT or user) is set to be admin. In order - // to access all resources belonging to their current organization. - ctx = context.WithValue(ctx, roles.ContextKey, roles.AdminRoleName) - r = r.WithContext(ctx) - next(w, r) - return - } - - u, err = store.Users(ctx).Get(ctx, chronograf.UserQuery{ - Name: &p.Subject, - Provider: &p.Issuer, - Scheme: &scheme, - }) - if err != nil { - log.Error("Failed to retrieve user") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - if hasAuthorizedRole(u, role) { - if len(u.Roles) != 1 { - msg := `User %d has too many role in organization. User: %#v.Please report this log at https://github.com/influxdata/influxdb/chronograf/issues/new"` - log.Error(fmt.Sprint(msg, u.ID, u)) - unknownErrorWithMessage(w, fmt.Errorf("please have administrator check logs and report error"), logger) - return - } - // use the first role, since there should only ever be one - // for any particular organization and hasAuthorizedRole - // should ensure that at least one role for the org exists - ctx = context.WithValue(ctx, roles.ContextKey, u.Roles[0].Name) - r = r.WithContext(ctx) - next(w, r) - return - } - - Error(w, http.StatusForbidden, "User is not authorized", logger) - }) -} - -func hasAuthorizedRole(u *chronograf.User, role string) bool { - if u == nil { - return false - } - - switch role { - case roles.MemberRoleName: - for _, r := range u.Roles { - switch r.Name { - case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName: - return true - } - } - case roles.ViewerRoleName: - for _, r := range u.Roles { - switch r.Name { - case roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName: - return true - } - } - case roles.EditorRoleName: - for _, r := range u.Roles { - switch r.Name { - case roles.EditorRoleName, roles.AdminRoleName: - return true - } - } - case roles.AdminRoleName: - for _, r := range u.Roles { - switch r.Name { - case roles.AdminRoleName: - return true - } - } - case roles.SuperAdminStatus: - // SuperAdmins should have been authorized before this. - // This is only meant to restrict access for non-superadmins. - return false - } - - return false -} diff --git a/chronograf/server/auth_test.go b/chronograf/server/auth_test.go deleted file mode 100644 index 307517d932..0000000000 --- a/chronograf/server/auth_test.go +++ /dev/null @@ -1,1950 +0,0 @@ -package server - -import ( - "context" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -func TestAuthorizedToken(t *testing.T) { - var tests = []struct { - Desc string - Code int - Principal oauth2.Principal - ValidateErr error - Expected string - }{ - { - Desc: "Error in validate", - Code: http.StatusForbidden, - ValidateErr: errors.New("error"), - }, - { - Desc: "Authorized ok", - Code: http.StatusOK, - Principal: oauth2.Principal{ - Subject: "Principal Strickland", - }, - Expected: "Principal Strickland", - }, - } - for _, test := range tests { - // next is a sentinel StatusOK and - // principal recorder. - var principal oauth2.Principal - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - principal = r.Context().Value(oauth2.PrincipalKey).(oauth2.Principal) - }) - req, _ := http.NewRequest("GET", "", nil) - w := httptest.NewRecorder() - - a := &mocks.Authenticator{ - Principal: test.Principal, - ValidateErr: test.ValidateErr, - } - - logger := &chronograf.NoopLogger{} - handler := AuthorizedToken(a, logger, next) - handler.ServeHTTP(w, req) - if w.Code != test.Code { - t.Errorf("Status code expected: %d actual %d", test.Code, w.Code) - } else if principal != test.Principal { - t.Errorf("Principal mismatch expected: %s actual %s", test.Principal, principal) - } - } -} -func TestAuthorizedUser(t *testing.T) { - type fields struct { - UsersStore chronograf.UsersStore - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - principal *oauth2.Principal - scheme string - useAuth bool - role string - } - tests := []struct { - name string - fields fields - args args - hasOrganizationContext bool - hasSuperAdminContext bool - hasRoleContext bool - hasServerContext bool - authorized bool - }{ - { - name: "Not using auth", - fields: fields{ - UsersStore: &mocks.UsersStore{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - useAuth: false, - }, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: false, - hasServerContext: true, - authorized: true, - }, - { - name: "User with member role is member authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "member", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with viewer role is member authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.ViewerRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "member", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with editor role is member authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.EditorRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "member", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with admin role is member authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "member", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with viewer role is viewer authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.ViewerRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "viewer", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with editor role is viewer authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.EditorRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "viewer", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with admin role is viewer authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "viewer", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with viewer role is editor unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.ViewerRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with editor role is editor authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.EditorRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with admin role is editor authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with viewer role is admin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.ViewerRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with editor role is admin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.EditorRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with admin role is admin authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: false, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "User with no role is viewer unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{}, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "view", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with no role is editor unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{}, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with no role is admin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{}, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with unknown role is viewer unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: "sweet_role", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "viewer", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with unknown role is editor unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: "sweet_role", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with unknown role is admin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: "sweet_role", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with viewer role is SuperAdmin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.ViewerRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "superadmin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with editor role is SuperAdmin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.EditorRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "superadmin", - useAuth: true, - }, - authorized: false, - }, - { - name: "User with admin role is SuperAdmin unauthorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "superadmin", - useAuth: true, - }, - authorized: false, - }, - { - name: "SuperAdmin is Viewer authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - SuperAdmin: true, - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "viewer", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: true, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "SuperAdmin is Editor authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - SuperAdmin: true, - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "editor", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: true, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "SuperAdmin is Admin authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - SuperAdmin: true, - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: true, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "SuperAdmin is SuperAdmin authorized", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - SuperAdmin: true, - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "superadmin", - useAuth: true, - }, - authorized: true, - hasOrganizationContext: true, - hasSuperAdminContext: true, - hasRoleContext: true, - hasServerContext: false, - }, - { - name: "Invalid principal – principal is nil", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: nil, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "Invalid principal - missing organization", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "Invalid principal - organization id not uint64", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1ee7", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "Failed to retrieve organization", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - switch *q.ID { - case "1338": - return &chronograf.Organization{ - ID: "1338", - Name: "The ShillBillThrilliettas", - }, nil - default: - return nil, chronograf.ErrOrganizationNotFound - } - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billysteve", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - { - name: "Failed to retrieve user", - fields: fields{ - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - switch *q.Name { - case "billysteve": - return &chronograf.User{ - ID: 1337, - Name: "billysteve", - Provider: "google", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - default: - return nil, chronograf.ErrUserNotFound - } - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - principal: &oauth2.Principal{ - Subject: "billietta", - Issuer: "google", - Organization: "1337", - }, - scheme: "oauth2", - role: "admin", - useAuth: true, - }, - authorized: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var authorized bool - var hasServerCtx bool - var hasSuperAdminCtx bool - var hasOrganizationCtx bool - var hasRoleCtx bool - next := func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - hasServerCtx = hasServerContext(ctx) - hasSuperAdminCtx = hasSuperAdminContext(ctx) - _, hasOrganizationCtx = hasOrganizationContext(ctx) - _, hasRoleCtx = hasRoleContext(ctx) - authorized = true - } - fn := AuthorizedUser( - &Store{ - UsersStore: tt.fields.UsersStore, - OrganizationsStore: tt.fields.OrganizationsStore, - }, - tt.args.useAuth, - tt.args.role, - tt.fields.Logger, - next, - ) - - w := httptest.NewRecorder() - r := httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ) - if tt.args.principal == nil { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil)) - } else { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal)) - } - fn(w, r) - - if authorized != tt.authorized { - t.Errorf("%q. AuthorizedUser() = %v, expected %v", tt.name, authorized, tt.authorized) - } - - if !authorized && w.Code != http.StatusForbidden { - t.Errorf("%q. AuthorizedUser() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden) - } - - if hasServerCtx != tt.hasServerContext { - t.Errorf("%q. AuthorizedUser().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.hasServerContext) - } - - if hasSuperAdminCtx != tt.hasSuperAdminContext { - t.Errorf("%q. AuthorizedUser().Context().SuperAdmin = %v, expected %v", tt.name, hasSuperAdminCtx, tt.hasSuperAdminContext) - } - - if hasOrganizationCtx != tt.hasOrganizationContext { - t.Errorf("%q. AuthorizedUser.Context().Organization = %v, expected %v", tt.name, hasOrganizationCtx, tt.hasOrganizationContext) - } - - if hasRoleCtx != tt.hasRoleContext { - t.Errorf("%q. AuthorizedUser().Context().Role = %v, expected %v", tt.name, hasRoleCtx, tt.hasRoleContext) - } - - }) - } -} - -func TestRawStoreAccess(t *testing.T) { - type fields struct { - Logger chronograf.Logger - } - type args struct { - principal *oauth2.Principal - serverContext bool - user *chronograf.User - } - type wants struct { - authorized bool - hasServerContext bool - } - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "middleware already has server context", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - serverContext: true, - }, - wants: wants{ - authorized: true, - hasServerContext: true, - }, - }, - { - name: "user on context is a SuperAdmin", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - user: &chronograf.User{ - SuperAdmin: true, - }, - }, - wants: wants{ - authorized: true, - hasServerContext: true, - }, - }, - { - name: "user on context is a not SuperAdmin", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - }, - args: args{ - user: &chronograf.User{ - SuperAdmin: false, - }, - }, - wants: wants{ - authorized: false, - hasServerContext: false, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var authorized bool - var hasServerCtx bool - next := func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - hasServerCtx = hasServerContext(ctx) - authorized = true - } - fn := RawStoreAccess( - tt.fields.Logger, - next, - ) - - w := httptest.NewRecorder() - url := "http://any.url" - r := httptest.NewRequest( - "GET", - url, - nil, - ) - if tt.args.principal == nil { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil)) - } else { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal)) - } - - if tt.args.serverContext { - r = r.WithContext(serverContext(r.Context())) - } - if tt.args.user != nil { - r = r.WithContext(context.WithValue(r.Context(), UserContextKey, tt.args.user)) - } - fn(w, r) - - if authorized != tt.wants.authorized { - t.Errorf("%q. RawStoreAccess() = %v, expected %v", tt.name, authorized, tt.wants.authorized) - } - - if !authorized && w.Code != http.StatusForbidden { - t.Errorf("%q. RawStoreAccess() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden) - } - - if hasServerCtx != tt.wants.hasServerContext { - t.Errorf("%q. RawStoreAccess().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext) - } - - }) - } -} diff --git a/chronograf/server/builders.go b/chronograf/server/builders.go deleted file mode 100644 index c3d9519cef..0000000000 --- a/chronograf/server/builders.go +++ /dev/null @@ -1,186 +0,0 @@ -package server - -import ( - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/canned" - "github.com/influxdata/influxdb/v2/chronograf/filestore" - "github.com/influxdata/influxdb/v2/chronograf/memdb" - "github.com/influxdata/influxdb/v2/chronograf/multistore" -) - -// LayoutBuilder is responsible for building Layouts -type LayoutBuilder interface { - Build(chronograf.LayoutsStore) (*multistore.Layouts, error) -} - -// MultiLayoutBuilder implements LayoutBuilder and will return a Layouts -type MultiLayoutBuilder struct { - Logger chronograf.Logger - UUID chronograf.ID - CannedPath string -} - -// Build will construct a Layouts of canned and db-backed personalized -// layouts -func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutsStore) (*multistore.Layouts, error) { - // These apps are those handled from a directory - apps := filestore.NewApps(builder.CannedPath, builder.UUID, builder.Logger) - // These apps are statically compiled into chronograf - binApps := &canned.BinLayoutsStore{ - Logger: builder.Logger, - } - // Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts. - // The idea here is that these stores form a hierarchy in which each is tried sequentially until - // the operation has success. So, the database is preferred over filesystem over binary data. - layouts := &multistore.Layouts{ - Stores: []chronograf.LayoutsStore{ - db, - apps, - binApps, - }, - } - - return layouts, nil -} - -// DashboardBuilder is responsible for building dashboards -type DashboardBuilder interface { - Build(chronograf.DashboardsStore) (*multistore.DashboardsStore, error) -} - -// MultiDashboardBuilder builds a DashboardsStore backed by bolt and the filesystem -type MultiDashboardBuilder struct { - Logger chronograf.Logger - ID chronograf.ID - Path string -} - -// Build will construct a Dashboard store of filesystem and db-backed dashboards -func (builder *MultiDashboardBuilder) Build(db chronograf.DashboardsStore) (*multistore.DashboardsStore, error) { - // These dashboards are those handled from a directory - files := filestore.NewDashboards(builder.Path, builder.ID, builder.Logger) - // Acts as a front-end to both the bolt dashboard and filesystem dashboards. - // The idea here is that these stores form a hierarchy in which each is tried sequentially until - // the operation has success. So, the database is preferred over filesystem - dashboards := &multistore.DashboardsStore{ - Stores: []chronograf.DashboardsStore{ - db, - files, - }, - } - - return dashboards, nil -} - -// SourcesBuilder builds a MultiSourceStore -type SourcesBuilder interface { - Build(chronograf.SourcesStore) (*multistore.SourcesStore, error) -} - -// MultiSourceBuilder implements SourcesBuilder -type MultiSourceBuilder struct { - InfluxDBURL string - InfluxDBUsername string - InfluxDBPassword string - - Logger chronograf.Logger - ID chronograf.ID - Path string -} - -// Build will return a MultiSourceStore -func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.SourcesStore, error) { - // These dashboards are those handled from a directory - files := filestore.NewSources(fs.Path, fs.ID, fs.Logger) - - stores := []chronograf.SourcesStore{db, files} - - if fs.InfluxDBURL != "" { - influxStore := &memdb.SourcesStore{ - Source: &chronograf.Source{ - ID: 0, - Name: fs.InfluxDBURL, - Type: chronograf.InfluxDB, - Username: fs.InfluxDBUsername, - Password: fs.InfluxDBPassword, - URL: fs.InfluxDBURL, - Default: true, - }} - stores = append([]chronograf.SourcesStore{influxStore}, stores...) - } - sources := &multistore.SourcesStore{ - Stores: stores, - } - - return sources, nil -} - -// KapacitorBuilder builds a KapacitorStore -type KapacitorBuilder interface { - Build(chronograf.ServersStore) (*multistore.KapacitorStore, error) -} - -// MultiKapacitorBuilder implements KapacitorBuilder -type MultiKapacitorBuilder struct { - KapacitorURL string - KapacitorUsername string - KapacitorPassword string - - Logger chronograf.Logger - ID chronograf.ID - Path string -} - -// Build will return a multistore facade KapacitorStore over memdb and bolt -func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*multistore.KapacitorStore, error) { - // These dashboards are those handled from a directory - files := filestore.NewKapacitors(builder.Path, builder.ID, builder.Logger) - - stores := []chronograf.ServersStore{db, files} - - if builder.KapacitorURL != "" { - memStore := &memdb.KapacitorStore{ - Kapacitor: &chronograf.Server{ - ID: 0, - SrcID: 0, - Name: builder.KapacitorURL, - URL: builder.KapacitorURL, - Username: builder.KapacitorUsername, - Password: builder.KapacitorPassword, - }, - } - stores = append([]chronograf.ServersStore{memStore}, stores...) - } - kapacitors := &multistore.KapacitorStore{ - Stores: stores, - } - return kapacitors, nil -} - -// OrganizationBuilder is responsible for building dashboards -type OrganizationBuilder interface { - Build(chronograf.OrganizationsStore) (*multistore.OrganizationsStore, error) -} - -// MultiOrganizationBuilder builds a OrganizationsStore backed by bolt and the filesystem -type MultiOrganizationBuilder struct { - Logger chronograf.Logger - Path string -} - -// Build will construct a Organization store of filesystem and db-backed dashboards -func (builder *MultiOrganizationBuilder) Build(db chronograf.OrganizationsStore) (*multistore.OrganizationsStore, error) { - // These organization are those handled from a directory - files := filestore.NewOrganizations(builder.Path, builder.Logger) - // Acts as a front-end to both the bolt org and filesystem orgs. - // The idea here is that these stores form a hierarchy in which each is tried sequentially until - // the operation has success. So, the database is preferred over filesystem - orgs := &multistore.OrganizationsStore{ - Stores: []chronograf.OrganizationsStore{ - db, - files, - }, - } - - return orgs, nil -} diff --git a/chronograf/server/builders_test.go b/chronograf/server/builders_test.go deleted file mode 100644 index ccd0b05577..0000000000 --- a/chronograf/server/builders_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package server_test - -import ( - "testing" - - "github.com/influxdata/influxdb/v2/chronograf/server" -) - -func TestLayoutBuilder(t *testing.T) { - var l server.LayoutBuilder = &server.MultiLayoutBuilder{} - layout, err := l.Build(nil) - if err != nil { - t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutsStore: %v", err) - } - - if layout == nil { - t.Fatal("LayoutBuilder should have built a layout") - } -} - -func TestSourcesStoresBuilder(t *testing.T) { - var b server.SourcesBuilder = &server.MultiSourceBuilder{} - sources, err := b.Build(nil) - if err != nil { - t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err) - } - if sources == nil { - t.Fatal("SourcesBuilder should have built a MultiSourceStore") - } -} diff --git a/chronograf/server/cells.go b/chronograf/server/cells.go deleted file mode 100644 index 78b834d7ef..0000000000 --- a/chronograf/server/cells.go +++ /dev/null @@ -1,358 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - idgen "github.com/influxdata/influxdb/v2/chronograf/id" -) - -const ( - // DefaultWidth is used if not specified - DefaultWidth = 4 - // DefaultHeight is used if not specified - DefaultHeight = 4 -) - -type dashboardCellLinks struct { - Self string `json:"self"` // Self link mapping to this resource -} - -type dashboardCellResponse struct { - chronograf.DashboardCell - Links dashboardCellLinks `json:"links"` -} - -func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse { - base := "/chronograf/v1/dashboards" - if cell.Queries == nil { - cell.Queries = []chronograf.DashboardQuery{} - } - if cell.CellColors == nil { - cell.CellColors = []chronograf.CellColor{} - } - - // Copy to handle race condition - newAxes := make(map[string]chronograf.Axis, len(cell.Axes)) - for k, v := range cell.Axes { - newAxes[k] = v - } - - // ensure x, y, and y2 axes always returned - for _, lbl := range []string{"x", "y", "y2"} { - if _, found := newAxes[lbl]; !found { - newAxes[lbl] = chronograf.Axis{ - Bounds: []string{"", ""}, - } - } - } - cell.Axes = newAxes - - return dashboardCellResponse{ - DashboardCell: cell, - Links: dashboardCellLinks{ - Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID), - }, - } -} - -func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse { - cells := make([]dashboardCellResponse, len(dcells)) - for i, cell := range dcells { - cells[i] = newCellResponse(dID, cell) - } - return cells -} - -// ValidDashboardCellRequest verifies that the dashboard cells have a query and -// have the correct axes specified -func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { - if c == nil { - return fmt.Errorf("chronograf dashboard cell was nil") - } - - CorrectWidthHeight(c) - for _, q := range c.Queries { - if err := ValidateQueryConfig(&q.QueryConfig); err != nil { - return err - } - } - MoveTimeShift(c) - err := HasCorrectAxes(c) - if err != nil { - return err - } - if err = HasCorrectColors(c); err != nil { - return err - } - return nil -} - -// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell -func HasCorrectAxes(c *chronograf.DashboardCell) error { - for label, axis := range c.Axes { - if !oneOf(label, "x", "y", "y2") { - return chronograf.ErrInvalidAxis - } - - if !oneOf(axis.Scale, "linear", "log", "") { - return chronograf.ErrInvalidAxis - } - - if !oneOf(axis.Base, "10", "2", "") { - return chronograf.ErrInvalidAxis - } - } - - return nil -} - -// HasCorrectColors verifies that the format of each color is correct -func HasCorrectColors(c *chronograf.DashboardCell) error { - for _, color := range c.CellColors { - if !oneOf(color.Type, "max", "min", "threshold", "text", "background", "scale") { - return chronograf.ErrInvalidColorType - } - if len(color.Hex) != 7 { - return chronograf.ErrInvalidColor - } - } - return nil -} - -// oneOf reports whether a provided string is a member of a variadic list of -// valid options -func oneOf(prop string, validOpts ...string) bool { - for _, valid := range validOpts { - if prop == valid { - return true - } - } - return false -} - -// CorrectWidthHeight changes the cell to have at least the -// minimum width and height -func CorrectWidthHeight(c *chronograf.DashboardCell) { - if c.W < 1 { - c.W = DefaultWidth - } - if c.H < 1 { - c.H = DefaultHeight - } -} - -// MoveTimeShift moves TimeShift from the QueryConfig to the DashboardQuery -func MoveTimeShift(c *chronograf.DashboardCell) { - for i, query := range c.Queries { - query.Shifts = query.QueryConfig.Shifts - c.Queries[i] = query - } -} - -// AddQueryConfig updates a cell by converting InfluxQL into queryconfigs -// If influxql cannot be represented by a full query config, then, the -// query config's raw text is set to the command. -func AddQueryConfig(c *chronograf.DashboardCell) { - for i, q := range c.Queries { - qc := ToQueryConfig(q.Command) - qc.Shifts = append([]chronograf.TimeShift(nil), q.Shifts...) - q.Shifts = nil - q.QueryConfig = qc - c.Queries[i] = q - } -} - -// DashboardCells returns all cells from a dashboard within the store -func (s *Service) DashboardCells(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - boards := newDashboardResponse(e) - cells := boards.Cells - encodeJSON(w, http.StatusOK, cells, s.Logger) -} - -// NewDashboardCell adds a cell to an existing dashboard -func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - var cell chronograf.DashboardCell - if err := json.NewDecoder(r.Body).Decode(&cell); err != nil { - invalidJSON(w, s.Logger) - return - } - - if err := ValidDashboardCellRequest(&cell); err != nil { - invalidData(w, err, s.Logger) - return - } - - ids := &idgen.UUID{} - cid, err := ids.Generate() - if err != nil { - msg := fmt.Sprintf("Error creating cell ID of dashboard %d: %v", id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - cell.ID = cid - - dash.Cells = append(dash.Cells, cell) - if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil { - msg := fmt.Sprintf("Error adding cell %s to dashboard %d: %v", cid, id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - - boards := newDashboardResponse(dash) - for _, cell := range boards.Cells { - if cell.ID == cid { - encodeJSON(w, http.StatusOK, cell, s.Logger) - return - } - } -} - -// DashboardCellID gets a specific cell from an existing dashboard -func (s *Service) DashboardCellID(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - boards := newDashboardResponse(dash) - cid := httprouter.ParamsFromContext(ctx).ByName("cid") - for _, cell := range boards.Cells { - if cell.ID == cid { - encodeJSON(w, http.StatusOK, cell, s.Logger) - return - } - } - notFound(w, id, s.Logger) -} - -// RemoveDashboardCell removes a specific cell from an existing dashboard -func (s *Service) RemoveDashboardCell(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - cid := httprouter.ParamsFromContext(ctx).ByName("cid") - cellid := -1 - for i, cell := range dash.Cells { - if cell.ID == cid { - cellid = i - break - } - } - if cellid == -1 { - notFound(w, id, s.Logger) - return - } - - dash.Cells = append(dash.Cells[:cellid], dash.Cells[cellid+1:]...) - if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil { - msg := fmt.Sprintf("Error removing cell %s from dashboard %d: %v", cid, id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - w.WriteHeader(http.StatusNoContent) -} - -// ReplaceDashboardCell replaces a cell entirely within an existing dashboard -func (s *Service) ReplaceDashboardCell(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - dash, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - cid := httprouter.ParamsFromContext(ctx).ByName("cid") - cellid := -1 - for i, cell := range dash.Cells { - if cell.ID == cid { - cellid = i - break - } - } - if cellid == -1 { - notFound(w, cid, s.Logger) - return - } - - var cell chronograf.DashboardCell - if err := json.NewDecoder(r.Body).Decode(&cell); err != nil { - invalidJSON(w, s.Logger) - return - } - - for i, a := range cell.Axes { - if len(a.Bounds) == 0 { - a.Bounds = []string{"", ""} - cell.Axes[i] = a - } - } - - if err := ValidDashboardCellRequest(&cell); err != nil { - invalidData(w, err, s.Logger) - return - } - cell.ID = cid - - dash.Cells[cellid] = cell - if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil { - msg := fmt.Sprintf("Error updating cell %s in dashboard %d: %v", cid, id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - - res := newCellResponse(dash.ID, cell) - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/cells_test.go b/chronograf/server/cells_test.go deleted file mode 100644 index c7e4bb482e..0000000000 --- a/chronograf/server/cells_test.go +++ /dev/null @@ -1,889 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "reflect" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func Test_Cells_CorrectAxis(t *testing.T) { - t.Parallel() - - axisTests := []struct { - name string - cell *chronograf.DashboardCell - shouldFail bool - }{ - { - name: "correct axes", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"0", "100"}, - }, - "y": { - Bounds: []string{"0", "100"}, - }, - "y2": { - Bounds: []string{"0", "100"}, - }, - }, - }, - }, - { - name: "invalid axes present", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "axis of evil": { - Bounds: []string{"666", "666"}, - }, - "axis of awesome": { - Bounds: []string{"1337", "31337"}, - }, - }, - }, - shouldFail: true, - }, - { - name: "linear scale value", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Scale: "linear", - Bounds: []string{"0", "100"}, - }, - }, - }, - }, - { - name: "log scale value", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Scale: "log", - Bounds: []string{"0", "100"}, - }, - }, - }, - }, - { - name: "invalid scale value", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Scale: "potatoes", - Bounds: []string{"0", "100"}, - }, - }, - }, - shouldFail: true, - }, - { - name: "base 10 axis", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Base: "10", - Bounds: []string{"0", "100"}, - }, - }, - }, - }, - { - name: "base 2 axis", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Base: "2", - Bounds: []string{"0", "100"}, - }, - }, - }, - }, - { - name: "invalid base", - cell: &chronograf.DashboardCell{ - Axes: map[string]chronograf.Axis{ - "x": { - Base: "all your base are belong to us", - Bounds: []string{"0", "100"}, - }, - }, - }, - shouldFail: true, - }, - } - - for _, test := range axisTests { - t.Run(test.name, func(tt *testing.T) { - if err := HasCorrectAxes(test.cell); err != nil && !test.shouldFail { - t.Errorf("%q: Unexpected error: err: %s", test.name, err) - } else if err == nil && test.shouldFail { - t.Errorf("%q: Expected error and received none", test.name) - } - }) - } -} - -func Test_Service_DashboardCells(t *testing.T) { - cellsTests := []struct { - name string - reqURL *url.URL - ctxParams map[string]string - mockResponse []chronograf.DashboardCell - expected []chronograf.DashboardCell - expectedCode int - }{ - { - name: "happy path", - reqURL: &url.URL{ - Path: "/chronograf/v1/dashboards/1/cells", - }, - ctxParams: map[string]string{ - "id": "1", - }, - mockResponse: []chronograf.DashboardCell{}, - expected: []chronograf.DashboardCell{}, - expectedCode: http.StatusOK, - }, - { - name: "cell axes should always be \"x\", \"y\", and \"y2\"", - reqURL: &url.URL{ - Path: "/chronograf/v1/dashboards/1/cells", - }, - ctxParams: map[string]string{ - "id": "1", - }, - mockResponse: []chronograf.DashboardCell{ - { - ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "CPU", - Type: "bar", - Queries: []chronograf.DashboardQuery{}, - Axes: map[string]chronograf.Axis{}, - }, - }, - expected: []chronograf.DashboardCell{ - { - ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "CPU", - Type: "bar", - Queries: []chronograf.DashboardQuery{}, - CellColors: []chronograf.CellColor{}, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"", ""}, - }, - "y": { - Bounds: []string{"", ""}, - }, - "y2": { - Bounds: []string{"", ""}, - }, - }, - }, - }, - expectedCode: http.StatusOK, - }, - } - - for _, test := range cellsTests { - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - // setup context with params - params := httprouter.Params{} - for k, v := range test.ctxParams { - params = append(params, httprouter.Param{ - Key: k, - Value: v, - }) - } - ctx := context.WithValue( - context.Background(), - httprouter.ParamsKey, - params, - ) - - // setup response recorder and request - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", test.reqURL.RequestURI(), strings.NewReader("")).WithContext(ctx) - - // setup mock DashboardCells store and logger - tlog := &mocks.TestLogger{} - svc := &Service{ - Store: &mocks.Store{ - DashboardsStore: &mocks.DashboardsStore{ - GetF: func(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{ - ID: chronograf.DashboardID(1), - Cells: test.mockResponse, - Templates: []chronograf.Template{}, - Name: "empty dashboard", - }, nil - }, - }, - }, - Logger: tlog, - } - - // invoke DashboardCell handler - svc.DashboardCells(rr, req) - - // setup frame to decode response into - respFrame := []struct { - chronograf.DashboardCell - Links json.RawMessage `json:"links"` // ignore links - }{} - - // decode response - resp := rr.Result() - - if resp.StatusCode != test.expectedCode { - tlog.Dump(t) - t.Fatalf("%q - Status codes do not match. Want %d (%s), Got %d (%s)", test.name, test.expectedCode, http.StatusText(test.expectedCode), resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - if err := json.NewDecoder(resp.Body).Decode(&respFrame); err != nil { - t.Fatalf("%q - Error unmarshalling response body: err: %s", test.name, err) - } - - // extract actual - actual := []chronograf.DashboardCell{} - for _, rsp := range respFrame { - actual = append(actual, rsp.DashboardCell) - } - - // compare actual and expected - if !cmp.Equal(actual, test.expected) { - t.Fatalf("%q - Dashboard Cells do not match: diff: %s", test.name, cmp.Diff(actual, test.expected)) - } - }) - } -} - -func TestHasCorrectColors(t *testing.T) { - tests := []struct { - name string - c *chronograf.DashboardCell - wantErr bool - }{ - { - name: "min type is valid", - c: &chronograf.DashboardCell{ - CellColors: []chronograf.CellColor{ - { - Type: "min", - Hex: "#FFFFFF", - }, - }, - }, - }, - { - name: "max type is valid", - c: &chronograf.DashboardCell{ - CellColors: []chronograf.CellColor{ - { - Type: "max", - Hex: "#FFFFFF", - }, - }, - }, - }, - { - name: "threshold type is valid", - c: &chronograf.DashboardCell{ - CellColors: []chronograf.CellColor{ - { - Type: "threshold", - Hex: "#FFFFFF", - }, - }, - }, - }, - { - name: "invalid color type", - c: &chronograf.DashboardCell{ - CellColors: []chronograf.CellColor{ - { - Type: "unknown", - Hex: "#FFFFFF", - }, - }, - }, - wantErr: true, - }, - { - name: "invalid color hex", - c: &chronograf.DashboardCell{ - CellColors: []chronograf.CellColor{ - { - Type: "min", - Hex: "bad", - }, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := HasCorrectColors(tt.c); (err != nil) != tt.wantErr { - t.Errorf("HasCorrectColors() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestService_ReplaceDashboardCell(t *testing.T) { - tests := []struct { - name string - DashboardsStore chronograf.DashboardsStore - ID string - CID string - w *httptest.ResponseRecorder - r *http.Request - want string - }{ - { - name: "update cell retains query config", - ID: "1", - CID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - DashboardsStore: &mocks.DashboardsStore{ - UpdateF: func(ctx context.Context, target chronograf.Dashboard) error { - return nil - }, - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{ - ID: ID, - Cells: []chronograf.DashboardCell{ - { - ID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - W: 4, - H: 4, - Name: "Untitled Cell", - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)", - QueryConfig: chronograf.QueryConfig{ - ID: "3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e", - Database: "telegraf", - Measurement: "cpu", - RetentionPolicy: "autogen", - Fields: []chronograf.Field{ - { - Value: "mean", - Type: "func", - Alias: "mean_usage_user", - Args: []chronograf.Field{ - { - Value: "usage_user", - Type: "field", - }, - }, - }, - }, - Tags: map[string][]string{ - "cpu": { - "ChristohersMBP2.lan", - }, - }, - GroupBy: chronograf.GroupBy{ - Time: "2s", - Tags: []string{}, - }, - AreTagsAccepted: true, - Fill: "null", - RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"), - Range: &chronograf.DurationRange{ - Lower: "now() - 15m"}, - Shifts: []chronograf.TimeShift{}, - }, - }, - }, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"", ""}, - }, - "y": { - Bounds: []string{"", ""}, - }, - "y2": { - Bounds: []string{"", ""}, - }, - }, - Type: "line", - CellColors: []chronograf.CellColor{ - { - ID: "0", - Type: "min", - Hex: "#00C9FF", - Name: "laser", - Value: "0", - }, - { - ID: "1", - Type: "max", - Hex: "#9394FF", - Name: "comet", - Value: "100", - }, - }, - }, - }, - }, nil - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(` - { - "i": "3c5c4102-fa40-4585-a8f9-917c77e37192", - "x": 0, - "y": 0, - "w": 4, - "h": 4, - "name": "Untitled Cell", - "queries": [ - { - "queryConfig": { - "id": "3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e", - "database": "telegraf", - "measurement": "cpu", - "retentionPolicy": "autogen", - "fields": [ - { - "value": "mean", - "type": "func", - "alias": "mean_usage_user", - "args": [{"value": "usage_user", "type": "field", "alias": ""}] - } - ], - "tags": {"cpu": ["ChristohersMBP2.lan"]}, - "groupBy": {"time": "2s", "tags": []}, - "areTagsAccepted": true, - "fill": "null", - "rawText": - "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)", - "range": {"upper": "", "lower": "now() - 15m"}, - "shifts": [] - }, - "query": - "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)", - "source": null - } - ], - "axes": { - "x": { - "bounds": ["",""], - "label": "", - "prefix": "", - "suffix": "", - "base": "", - "scale": "" - }, - "y": { - "bounds": ["",""], - "label": "", - "prefix": "", - "suffix": "", - "base": "", - "scale": "" - }, - "y2": { - "bounds": ["",""], - "label": "", - "prefix": "", - "suffix": "", - "base": "", - "scale": "" - } - }, - "type": "line", - "colors": [ - {"type": "min", "hex": "#00C9FF", "id": "0", "name": "laser", "value": "0"}, - { - "type": "max", - "hex": "#9394FF", - "id": "1", - "name": "comet", - "value": "100" - } - ], - "links": { - "self": - "/chronograf/v1/dashboards/6/cells/3c5c4102-fa40-4585-a8f9-917c77e37192" - } - } - `))), - want: `{"i":"3c5c4102-fa40-4585-a8f9-917c77e37192","x":0,"y":0,"w":4,"h":4,"name":"Untitled Cell","queries":[{"query":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","queryConfig":{"id":"3cd3eaa4-a4b8-44b3-b69e-0c7bf6b91d9e","database":"telegraf","measurement":"cpu","retentionPolicy":"autogen","fields":[{"value":"mean","type":"func","alias":"mean_usage_user","args":[{"value":"usage_user","type":"field","alias":""}]}],"tags":{"cpu":["ChristohersMBP2.lan"]},"groupBy":{"time":"2s","tags":[]},"areTagsAccepted":true,"fill":"null","rawText":"SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time \u003e :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)","range":{"upper":"","lower":"now() - 15m"},"shifts":[]},"source":""}],"axes":{"x":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""},"y2":{"bounds":["",""],"label":"","prefix":"","suffix":"","base":"","scale":""}},"type":"line","colors":[{"id":"0","type":"min","hex":"#00C9FF","name":"laser","value":"0"},{"id":"1","type":"max","hex":"#9394FF","name":"comet","value":"100"}],"tableOptions":{"verticalTimeAxis":false,"sortBy":{"internalName":"","displayName":"","visible":false},"wrapping":"","fixFirstColumn":false},"fieldOptions":null,"timeFormat":"","decimalPlaces":{"isEnforced":false,"digits":0},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}} -`, - }, - { - name: "dashboard doesn't exist", - ID: "1", - DashboardsStore: &mocks.DashboardsStore{ - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{}, fmt.Errorf("doesn't exist") - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil), - want: `{"code":404,"message":"ID 1 not found"}`, - }, - { - name: "cell doesn't exist", - ID: "1", - CID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - DashboardsStore: &mocks.DashboardsStore{ - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{}, nil - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil), - want: `{"code":404,"message":"ID 3c5c4102-fa40-4585-a8f9-917c77e37192 not found"}`, - }, - { - name: "invalid query config", - ID: "1", - CID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - DashboardsStore: &mocks.DashboardsStore{ - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{ - ID: ID, - Cells: []chronograf.DashboardCell{ - { - ID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - }, - }, - }, nil - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", bytes.NewReader([]byte(`{ - "i": "3c5c4102-fa40-4585-a8f9-917c77e37192", - "x": 0, - "y": 0, - "w": 4, - "h": 4, - "name": "Untitled Cell", - "queries": [ - { - "queryConfig": { - "fields": [ - { - "value": "invalid", - "type": "invalidType" - } - ] - } - } - ] - }`))), - want: `{"code":422,"message":"invalid field type \"invalidType\" ; expect func, field, integer, number, regex, wildcard"}`, - }, - { - name: "JSON is not parsable", - ID: "1", - CID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - DashboardsStore: &mocks.DashboardsStore{ - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{ - ID: ID, - Cells: []chronograf.DashboardCell{ - { - ID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - }, - }, - }, nil - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", nil), - want: `{"code":400,"message":"unparsable JSON"}`, - }, - { - name: "not able to update store returns error message", - ID: "1", - CID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - DashboardsStore: &mocks.DashboardsStore{ - UpdateF: func(ctx context.Context, target chronograf.Dashboard) error { - return fmt.Errorf("error") - }, - GetF: func(ctx context.Context, ID chronograf.DashboardID) (chronograf.Dashboard, error) { - return chronograf.Dashboard{ - ID: ID, - Cells: []chronograf.DashboardCell{ - { - ID: "3c5c4102-fa40-4585-a8f9-917c77e37192", - }, - }, - }, nil - }, - }, - w: httptest.NewRecorder(), - r: httptest.NewRequest("PUT", "/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192", bytes.NewReader([]byte(`{ - "i": "3c5c4102-fa40-4585-a8f9-917c77e37192", - "x": 0, - "y": 0, - "w": 4, - "h": 4, - "name": "Untitled Cell", - "queries": [ - { - "queryConfig": { - "fields": [ - { - "value": "usage_user", - "type": "field" - } - ] - } - } - ] - }`))), - want: `{"code":500,"message":"Error updating cell 3c5c4102-fa40-4585-a8f9-917c77e37192 in dashboard 1: error"}`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - DashboardsStore: tt.DashboardsStore, - }, - Logger: &mocks.TestLogger{}, - } - tt.r = WithContext(tt.r.Context(), tt.r, map[string]string{ - "id": tt.ID, - "cid": tt.CID, - }) - tt.r = tt.r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: tt.ID, - }, - { - Key: "cid", - Value: tt.CID, - }, - })) - s.ReplaceDashboardCell(tt.w, tt.r) - got := tt.w.Body.String() - if got != tt.want { - t.Errorf("ReplaceDashboardCell() = got/want\n%s\n%s\n", got, tt.want) - } - }) - } -} - -func strPtr(s string) *string { - return &s -} - -func Test_newCellResponses(t *testing.T) { - tests := []struct { - name string - dID chronograf.DashboardID - dcells []chronograf.DashboardCell - want []dashboardCellResponse - }{ - { - name: "all fields set", - dID: chronograf.DashboardID(1), - dcells: []chronograf.DashboardCell{ - { - ID: "445f8dc0-4d73-4168-8477-f628690d18a3", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "Untitled Cell", - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)", - Label: "", - QueryConfig: chronograf.QueryConfig{ - ID: "8d5ec6da-13a5-423e-9026-7bc45649766c", - Database: "telegraf", - Measurement: "cpu", - RetentionPolicy: "autogen", - Fields: []chronograf.Field{ - { - Value: "mean", - Type: "func", - Alias: "mean_usage_user", - Args: []chronograf.Field{ - { - Value: "usage_user", - Type: "field", - Alias: "", - }, - }, - }, - }, - Tags: map[string][]string{"cpu": {"ChristohersMBP2.lan"}}, - GroupBy: chronograf.GroupBy{ - Time: "2s", - }, - AreTagsAccepted: true, - Fill: "null", - RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"), - Range: &chronograf.DurationRange{ - Lower: "now() - 15m", - }, - }, - Source: "", - }, - }, - Axes: map[string]chronograf.Axis{ - "x": {}, - "y": {}, - "y2": {}, - }, - Type: "line", - CellColors: []chronograf.CellColor{ - {ID: "0", Type: "min", Hex: "#00C9FF", Name: "laser", Value: "0"}, - {ID: "1", Type: "max", Hex: "#9394FF", Name: "comet", Value: "100"}, - }, - }, - }, - want: []dashboardCellResponse{ - { - DashboardCell: chronograf.DashboardCell{ - ID: "445f8dc0-4d73-4168-8477-f628690d18a3", - W: 4, - H: 4, - Name: "Untitled Cell", - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)", - QueryConfig: chronograf.QueryConfig{ - ID: "8d5ec6da-13a5-423e-9026-7bc45649766c", - Database: "telegraf", - Measurement: "cpu", - RetentionPolicy: "autogen", - Fields: []chronograf.Field{ - { - Value: "mean", - Type: "func", - Alias: "mean_usage_user", - Args: []chronograf.Field{ - { - Value: "usage_user", - Type: "field", - }, - }, - }, - }, - Tags: map[string][]string{"cpu": {"ChristohersMBP2.lan"}}, - GroupBy: chronograf.GroupBy{ - Time: "2s", - }, - AreTagsAccepted: true, - Fill: "null", - RawText: strPtr("SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"autogen\".\"cpu\" WHERE time > :dashboardTime: AND \"cpu\"=:cpu: GROUP BY :interval: FILL(null)"), - Range: &chronograf.DurationRange{ - Lower: "now() - 15m", - }, - }, - }, - }, - Axes: map[string]chronograf.Axis{ - "x": {}, - "y": {}, - "y2": {}, - }, - Type: "line", - CellColors: []chronograf.CellColor{ - { - ID: "0", - Type: "min", - Hex: "#00C9FF", - Name: "laser", - Value: "0", - }, - { - ID: "1", - Type: "max", - Hex: "#9394FF", - Name: "comet", - Value: "100", - }, - }, - }, - Links: dashboardCellLinks{ - Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"}, - }, - }, - }, - { - name: "nothing set", - dID: chronograf.DashboardID(1), - dcells: []chronograf.DashboardCell{ - { - ID: "445f8dc0-4d73-4168-8477-f628690d18a3", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "Untitled Cell", - }, - }, - want: []dashboardCellResponse{ - { - DashboardCell: chronograf.DashboardCell{ - ID: "445f8dc0-4d73-4168-8477-f628690d18a3", - W: 4, - H: 4, - Name: "Untitled Cell", - Queries: []chronograf.DashboardQuery{}, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"", ""}, - }, - "y": { - Bounds: []string{"", ""}, - }, - "y2": { - Bounds: []string{"", ""}, - }, - }, - CellColors: []chronograf.CellColor{}, - }, - Links: dashboardCellLinks{ - Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := newCellResponses(tt.dID, tt.dcells); !reflect.DeepEqual(got, tt.want) { - t.Errorf("newCellResponses() = got-/want+ %s", cmp.Diff(got, tt.want)) - } - }) - } -} diff --git a/chronograf/server/config.go b/chronograf/server/config.go deleted file mode 100644 index cb5d07802c..0000000000 --- a/chronograf/server/config.go +++ /dev/null @@ -1,115 +0,0 @@ -package server - -import ( - "encoding/json" - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -type configLinks struct { - Self string `json:"self"` // Self link mapping to this resource - Auth string `json:"auth"` // Auth link to the auth config endpoint -} - -type selfLinks struct { - Self string `json:"self"` // Self link mapping to this resource -} - -type configResponse struct { - Links configLinks `json:"links"` - chronograf.Config -} - -func newConfigResponse(config chronograf.Config) *configResponse { - return &configResponse{ - Links: configLinks{ - Self: "/chronograf/v1/config", - Auth: "/chronograf/v1/config/auth", - }, - Config: config, - } -} - -type authConfigResponse struct { - Links selfLinks `json:"links"` - chronograf.AuthConfig -} - -func newAuthConfigResponse(config chronograf.Config) *authConfigResponse { - return &authConfigResponse{ - Links: selfLinks{ - Self: "/chronograf/v1/config/auth", - }, - AuthConfig: config.Auth, - } -} - -// Config retrieves the global application configuration -func (s *Service) Config(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - config, err := s.Store.Config(ctx).Get(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - if config == nil { - Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) - return - } - res := newConfigResponse(*config) - - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// AuthConfig retrieves the auth section of the global application configuration -func (s *Service) AuthConfig(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - config, err := s.Store.Config(ctx).Get(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - if config == nil { - Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) - return - } - - res := newAuthConfigResponse(*config) - - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// ReplaceAuthConfig replaces the auth section of the global application configuration -func (s *Service) ReplaceAuthConfig(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - var authConfig chronograf.AuthConfig - if err := json.NewDecoder(r.Body).Decode(&authConfig); err != nil { - invalidJSON(w, s.Logger) - return - } - - config, err := s.Store.Config(ctx).Get(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - if config == nil { - Error(w, http.StatusBadRequest, "Configuration object was nil", s.Logger) - return - } - config.Auth = authConfig - - res := newAuthConfigResponse(*config) - if err := s.Store.Config(ctx).Update(ctx, config); err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/config_test.go b/chronograf/server/config_test.go deleted file mode 100644 index d9633ebcae..0000000000 --- a/chronograf/server/config_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestConfig(t *testing.T) { - type fields struct { - ConfigStore chronograf.ConfigStore - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - wants wants - }{ - { - name: "Get global application configuration", - fields: fields{ - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":{"superAdminNewUsers":false}}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - ConfigStore: tt.fields.ConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - s.Config(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestAuthConfig(t *testing.T) { - type fields struct { - ConfigStore chronograf.ConfigStore - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - wants wants - }{ - { - name: "Get auth configuration", - fields: fields{ - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"superAdminNewUsers": false, "links": {"self": "/chronograf/v1/config/auth"}}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - ConfigStore: tt.fields.ConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - s.AuthConfig(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestReplaceAuthConfig(t *testing.T) { - type fields struct { - ConfigStore chronograf.ConfigStore - } - type args struct { - payload interface{} // expects JSON serializable struct - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "Set auth configuration", - fields: fields{ - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - }, - args: args{ - payload: chronograf.AuthConfig{ - SuperAdminNewUsers: true, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"superAdminNewUsers": true, "links": {"self": "/chronograf/v1/config/auth"}}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - ConfigStore: tt.fields.ConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - buf, _ := json.Marshal(tt.args.payload) - r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - - s.ReplaceAuthConfig(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} diff --git a/chronograf/server/context.go b/chronograf/server/context.go deleted file mode 100644 index 5cd4ea3797..0000000000 --- a/chronograf/server/context.go +++ /dev/null @@ -1,30 +0,0 @@ -package server - -import ( - "context" -) - -type serverContextKey string - -// ServerContextKey is the key used to specify that the -// server is making the requet via context -const ServerContextKey = serverContextKey("server") - -// hasServerContext specifies if the context contains -// the ServerContextKey and that the value stored there is true -func hasServerContext(ctx context.Context) bool { - // prevents panic in case of nil context - if ctx == nil { - return false - } - sa, ok := ctx.Value(ServerContextKey).(bool) - // should never happen - if !ok { - return false - } - return sa -} - -func serverContext(ctx context.Context) context.Context { - return context.WithValue(ctx, ServerContextKey, true) -} diff --git a/chronograf/server/dashboards.go b/chronograf/server/dashboards.go deleted file mode 100644 index 0e74c5b533..0000000000 --- a/chronograf/server/dashboards.go +++ /dev/null @@ -1,287 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -type dashboardLinks struct { - Self string `json:"self"` // Self link mapping to this resource - Cells string `json:"cells"` // Cells link to the cells endpoint - Templates string `json:"templates"` // Templates link to the templates endpoint -} - -type dashboardResponse struct { - ID chronograf.DashboardID `json:"id"` - Cells []dashboardCellResponse `json:"cells"` - Templates []templateResponse `json:"templates"` - Name string `json:"name"` - Organization string `json:"organization"` - Links dashboardLinks `json:"links"` -} - -type getDashboardsResponse struct { - Dashboards []*dashboardResponse `json:"dashboards"` -} - -func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse { - base := "/chronograf/v1/dashboards" - dd := AddQueryConfigs(DashboardDefaults(d)) - cells := newCellResponses(dd.ID, dd.Cells) - templates := newTemplateResponses(dd.ID, dd.Templates) - - return &dashboardResponse{ - ID: dd.ID, - Name: dd.Name, - Cells: cells, - Templates: templates, - Organization: d.Organization, - Links: dashboardLinks{ - Self: fmt.Sprintf("%s/%d", base, dd.ID), - Cells: fmt.Sprintf("%s/%d/cells", base, dd.ID), - Templates: fmt.Sprintf("%s/%d/templates", base, dd.ID), - }, - } -} - -// Dashboards returns all dashboards within the store -func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - dashboards, err := s.Store.Dashboards(ctx).All(ctx) - if err != nil { - Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger) - return - } - - res := getDashboardsResponse{ - Dashboards: []*dashboardResponse{}, - } - - for _, dashboard := range dashboards { - res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard)) - } - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// DashboardID returns a single specified dashboard -func (s *Service) DashboardID(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - res := newDashboardResponse(e) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// NewDashboard creates and returns a new dashboard object -func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) { - var dashboard chronograf.Dashboard - var err error - if err := json.NewDecoder(r.Body).Decode(&dashboard); err != nil { - invalidJSON(w, s.Logger) - return - } - - ctx := r.Context() - defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - if err := ValidDashboardRequest(&dashboard, defaultOrg.ID); err != nil { - invalidData(w, err, s.Logger) - return - } - - if dashboard, err = s.Store.Dashboards(ctx).Add(r.Context(), dashboard); err != nil { - msg := fmt.Errorf("error storing dashboard %v: %v", dashboard, err) - unknownErrorWithMessage(w, msg, s.Logger) - return - } - - res := newDashboardResponse(dashboard) - location(w, res.Links.Self) - encodeJSON(w, http.StatusCreated, res, s.Logger) -} - -// RemoveDashboard deletes a dashboard -func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - e, err := s.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id)) - if err != nil { - notFound(w, id, s.Logger) - return - } - - if err := s.Store.Dashboards(ctx).Delete(ctx, e); err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - w.WriteHeader(http.StatusNoContent) -} - -// ReplaceDashboard completely replaces a dashboard -func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - idParam, err := paramID("id", r) - if err != nil { - msg := fmt.Sprintf("Could not parse dashboard ID: %s", err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - } - id := chronograf.DashboardID(idParam) - - _, err = s.Store.Dashboards(ctx).Get(ctx, id) - if err != nil { - Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger) - return - } - - var req chronograf.Dashboard - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - req.ID = id - - defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - if err := ValidDashboardRequest(&req, defaultOrg.ID); err != nil { - invalidData(w, err, s.Logger) - return - } - - if err := s.Store.Dashboards(ctx).Update(ctx, req); err != nil { - msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - - res := newDashboardResponse(req) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// UpdateDashboard completely updates either the dashboard name or the cells -func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - idParam, err := paramID("id", r) - if err != nil { - msg := fmt.Sprintf("Could not parse dashboard ID: %s", err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - id := chronograf.DashboardID(idParam) - - orig, err := s.Store.Dashboards(ctx).Get(ctx, id) - if err != nil { - Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger) - return - } - - var req chronograf.Dashboard - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - req.ID = id - - if req.Name != "" { - orig.Name = req.Name - } else if len(req.Cells) > 0 { - defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - if err := ValidDashboardRequest(&req, defaultOrg.ID); err != nil { - invalidData(w, err, s.Logger) - return - } - orig.Cells = req.Cells - } else { - invalidData(w, fmt.Errorf("update must include either name or cells"), s.Logger) - return - } - - if err := s.Store.Dashboards(ctx).Update(ctx, orig); err != nil { - msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err) - Error(w, http.StatusInternalServerError, msg, s.Logger) - return - } - - res := newDashboardResponse(orig) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// ValidDashboardRequest verifies that the dashboard cells have a query -func ValidDashboardRequest(d *chronograf.Dashboard, defaultOrgID string) error { - if d.Organization == "" { - d.Organization = defaultOrgID - } - for i, c := range d.Cells { - if err := ValidDashboardCellRequest(&c); err != nil { - return err - } - d.Cells[i] = c - } - for _, t := range d.Templates { - if err := ValidTemplateRequest(&t); err != nil { - return err - } - } - (*d) = DashboardDefaults(*d) - return nil -} - -// DashboardDefaults updates the dashboard with the default values -// if none are specified -func DashboardDefaults(d chronograf.Dashboard) (newDash chronograf.Dashboard) { - newDash.ID = d.ID - newDash.Templates = d.Templates - newDash.Name = d.Name - newDash.Organization = d.Organization - newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells)) - - for i, c := range d.Cells { - CorrectWidthHeight(&c) - newDash.Cells[i] = c - } - return -} - -// AddQueryConfigs updates all the celsl in the dashboard to have query config -// objects corresponding to their influxql queries. -func AddQueryConfigs(d chronograf.Dashboard) (newDash chronograf.Dashboard) { - newDash.ID = d.ID - newDash.Templates = d.Templates - newDash.Name = d.Name - newDash.Cells = make([]chronograf.DashboardCell, len(d.Cells)) - - for i, c := range d.Cells { - AddQueryConfig(&c) - newDash.Cells[i] = c - } - return -} diff --git a/chronograf/server/dashboards_test.go b/chronograf/server/dashboards_test.go deleted file mode 100644 index 47aba6c05a..0000000000 --- a/chronograf/server/dashboards_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package server - -import ( - "reflect" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/influxdata/influxdb/v2/chronograf" -) - -func TestCorrectWidthHeight(t *testing.T) { - t.Parallel() - tests := []struct { - name string - cell chronograf.DashboardCell - want chronograf.DashboardCell - }{ - { - name: "updates width", - cell: chronograf.DashboardCell{ - W: 0, - H: 4, - }, - want: chronograf.DashboardCell{ - W: 4, - H: 4, - }, - }, - { - name: "updates height", - cell: chronograf.DashboardCell{ - W: 4, - H: 0, - }, - want: chronograf.DashboardCell{ - W: 4, - H: 4, - }, - }, - { - name: "updates both", - cell: chronograf.DashboardCell{ - W: 0, - H: 0, - }, - want: chronograf.DashboardCell{ - W: 4, - H: 4, - }, - }, - { - name: "updates neither", - cell: chronograf.DashboardCell{ - W: 4, - H: 4, - }, - want: chronograf.DashboardCell{ - W: 4, - H: 4, - }, - }, - } - for _, tt := range tests { - if CorrectWidthHeight(&tt.cell); !reflect.DeepEqual(tt.cell, tt.want) { - t.Errorf("%q. CorrectWidthHeight() = %v, want %v", tt.name, tt.cell, tt.want) - } - } -} - -func TestDashboardDefaults(t *testing.T) { - tests := []struct { - name string - d chronograf.Dashboard - want chronograf.Dashboard - }{ - { - name: "Updates all cell widths/heights", - d: chronograf.Dashboard{ - Cells: []chronograf.DashboardCell{ - { - W: 0, - H: 0, - }, - { - W: 2, - H: 2, - }, - }, - }, - want: chronograf.Dashboard{ - Cells: []chronograf.DashboardCell{ - { - W: 4, - H: 4, - }, - { - W: 2, - H: 2, - }, - }, - }, - }, - { - name: "Updates no cell", - d: chronograf.Dashboard{ - Cells: []chronograf.DashboardCell{ - { - W: 4, - H: 4, - }, { - W: 2, - H: 2, - }, - }, - }, - want: chronograf.Dashboard{ - Cells: []chronograf.DashboardCell{ - { - W: 4, - H: 4, - }, - { - W: 2, - H: 2, - }, - }, - }, - }, - } - for _, tt := range tests { - if actual := DashboardDefaults(tt.d); !reflect.DeepEqual(actual, tt.want) { - t.Errorf("%q. DashboardDefaults() = %v, want %v", tt.name, tt.d, tt.want) - } - } -} - -func TestValidDashboardRequest(t *testing.T) { - tests := []struct { - name string - d chronograf.Dashboard - want chronograf.Dashboard - wantErr bool - }{ - { - name: "Updates all cell widths/heights", - d: chronograf.Dashboard{ - Organization: "1337", - Cells: []chronograf.DashboardCell{ - { - W: 0, - H: 0, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00", - }, - }, - }, - { - W: 2, - H: 2, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00", - }, - }, - }, - }, - }, - want: chronograf.Dashboard{ - Organization: "1337", - Cells: []chronograf.DashboardCell{ - { - W: 4, - H: 4, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT donors from hill_valley_preservation_society where time > 1985-10-25T08:00:00", - }, - }, - }, - { - W: 2, - H: 2, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT winning_horses from grays_sports_alamanc where time > 1955-11-1T00:00:00", - }, - }, - }, - }, - }, - }, - } - for _, tt := range tests { - // TODO(desa): this Okay? - err := ValidDashboardRequest(&tt.d, "0") - if (err != nil) != tt.wantErr { - t.Errorf("%q. ValidDashboardRequest() error = %v, wantErr %v", tt.name, err, tt.wantErr) - continue - } - if diff := cmp.Diff(tt.d, tt.want); diff != "" { - t.Errorf("%q. ValidDashboardRequest(). got/want diff:\n%s", tt.name, diff) - } - } -} - -func Test_newDashboardResponse(t *testing.T) { - tests := []struct { - name string - d chronograf.Dashboard - want *dashboardResponse - }{ - { - name: "creates a dashboard response", - d: chronograf.Dashboard{ - Organization: "0", - Cells: []chronograf.DashboardCell{ - { - ID: "a", - W: 0, - H: 0, - Queries: []chronograf.DashboardQuery{ - { - Source: "/chronograf/v1/sources/1", - Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'", - Shifts: []chronograf.TimeShift{ - { - Label: "Best Week Evar", - Unit: "d", - Quantity: "7", - }, - }, - }, - }, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"0", "100"}, - }, - "y": { - Bounds: []string{"2", "95"}, - Label: "foo", - }, - }, - }, - { - ID: "b", - W: 0, - H: 0, - Queries: []chronograf.DashboardQuery{ - { - Source: "/chronograf/v1/sources/2", - Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", - }, - }, - }, - }, - }, - want: &dashboardResponse{ - Organization: "0", - Templates: []templateResponse{}, - Cells: []dashboardCellResponse{ - { - Links: dashboardCellLinks{ - Self: "/chronograf/v1/dashboards/0/cells/a", - }, - DashboardCell: chronograf.DashboardCell{ - ID: "a", - W: 4, - H: 4, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'", - Source: "/chronograf/v1/sources/1", - QueryConfig: chronograf.QueryConfig{ - RawText: &[]string{"SELECT donors from hill_valley_preservation_society where time > '1985-10-25 08:00:00'"}[0], - Fields: []chronograf.Field{}, - GroupBy: chronograf.GroupBy{ - Tags: []string{}, - }, - Tags: make(map[string][]string), - AreTagsAccepted: false, - Shifts: []chronograf.TimeShift{ - { - Label: "Best Week Evar", - Unit: "d", - Quantity: "7", - }, - }, - }, - }, - }, - CellColors: []chronograf.CellColor{}, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"0", "100"}, - }, - "y": { - Bounds: []string{"2", "95"}, - Label: "foo", - }, - "y2": { - Bounds: []string{"", ""}, - }, - }, - }, - }, - { - Links: dashboardCellLinks{ - Self: "/chronograf/v1/dashboards/0/cells/b", - }, - DashboardCell: chronograf.DashboardCell{ - ID: "b", - W: 4, - H: 4, - Axes: map[string]chronograf.Axis{ - "x": { - Bounds: []string{"", ""}, - }, - "y": { - Bounds: []string{"", ""}, - }, - "y2": { - Bounds: []string{"", ""}, - }, - }, - CellColors: []chronograf.CellColor{}, - Queries: []chronograf.DashboardQuery{ - { - Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", - Source: "/chronograf/v1/sources/2", - QueryConfig: chronograf.QueryConfig{ - Measurement: "grays_sports_alamanc", - Fields: []chronograf.Field{ - { - Type: "field", - Value: "winning_horses", - }, - }, - GroupBy: chronograf.GroupBy{ - Tags: []string{}, - }, - Tags: make(map[string][]string), - AreTagsAccepted: false, - Range: &chronograf.DurationRange{ - Lower: "now() - 15m", - }, - }, - }, - }, - }, - }, - }, - Links: dashboardLinks{ - Self: "/chronograf/v1/dashboards/0", - Cells: "/chronograf/v1/dashboards/0/cells", - Templates: "/chronograf/v1/dashboards/0/templates", - }, - }, - }, - } - for _, tt := range tests { - if got := newDashboardResponse(tt.d); !cmp.Equal(got, tt.want) { - t.Errorf("%q. newDashboardResponse() = diff:\n%s", tt.name, cmp.Diff(got, tt.want)) - } - } -} diff --git a/chronograf/server/databases.go b/chronograf/server/databases.go deleted file mode 100644 index d79ea29d1f..0000000000 --- a/chronograf/server/databases.go +++ /dev/null @@ -1,519 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" -) - -const ( - limitQuery = "limit" - offsetQuery = "offset" -) - -type dbLinks struct { - Self string `json:"self"` // Self link mapping to this resource - RPs string `json:"retentionPolicies"` // URL for retention policies for this database - Measurements string `json:"measurements"` // URL for measurements for this database -} - -type dbResponse struct { - Name string `json:"name"` // a unique string identifier for the database - Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy) - Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy) - ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy) - RPs []rpResponse `json:"retentionPolicies"` // RPs are the retention policies for a database - Links dbLinks `json:"links"` // Links are URI locations related to the database -} - -// newDBResponse creates the response for the /databases endpoint -func newDBResponse(srcID int, db string, rps []rpResponse) dbResponse { - base := "/chronograf/v1/sources" - return dbResponse{ - Name: db, - RPs: rps, - Links: dbLinks{ - Self: fmt.Sprintf("%s/%d/dbs/%s", base, srcID, db), - RPs: fmt.Sprintf("%s/%d/dbs/%s/rps", base, srcID, db), - Measurements: fmt.Sprintf("%s/%d/dbs/%s/measurements?limit=100&offset=0", base, srcID, db), - }, - } -} - -type dbsResponse struct { - Databases []dbResponse `json:"databases"` -} - -type rpLinks struct { - Self string `json:"self"` // Self link mapping to this resource -} - -type rpResponse struct { - Name string `json:"name"` // a unique string identifier for the retention policy - Duration string `json:"duration"` // the duration - Replication int32 `json:"replication"` // the replication factor - ShardDuration string `json:"shardDuration"` // the shard duration - Default bool `json:"isDefault"` // whether the RP should be the default - Links rpLinks `json:"links"` // Links are URI locations related to the database -} - -// WithLinks adds links to an rpResponse in place -func (r *rpResponse) WithLinks(srcID int, db string) { - base := "/chronograf/v1/sources" - r.Links = rpLinks{ - Self: fmt.Sprintf("%s/%d/dbs/%s/rps/%s", base, srcID, db, r.Name), - } -} - -type measurementLinks struct { - Self string `json:"self"` - First string `json:"first"` - Next string `json:"next,omitempty"` - Prev string `json:"prev,omitempty"` -} - -func newMeasurementLinks(src int, db string, limit, offset int) measurementLinks { - base := "/chronograf/v1/sources" - res := measurementLinks{ - Self: fmt.Sprintf("%s/%d/dbs/%s/measurements?limit=%d&offset=%d", base, src, db, limit, offset), - First: fmt.Sprintf("%s/%d/dbs/%s/measurements?limit=%d&offset=0", base, src, db, limit), - Next: fmt.Sprintf("%s/%d/dbs/%s/measurements?limit=%d&offset=%d", base, src, db, limit, offset+limit), - } - if offset-limit > 0 { - res.Prev = fmt.Sprintf("%s/%d/dbs/%s/measurements?limit=%d&offset=%d", base, src, db, limit, offset-limit) - } - - return res -} - -type measurementsResponse struct { - Measurements []chronograf.Measurement `json:"measurements"` // names of all measurements - Links measurementLinks `json:"links"` // Links are the URI locations for measurements pages -} - -// GetDatabases queries the list of all databases for a source -func (h *Service) GetDatabases(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - databases, err := dbsvc.AllDB(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - - dbs := make([]dbResponse, len(databases)) - for i, d := range databases { - rps, err := h.allRPs(ctx, dbsvc, srcID, d.Name) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - dbs[i] = newDBResponse(srcID, d.Name, rps) - } - - res := dbsResponse{ - Databases: dbs, - } - - encodeJSON(w, http.StatusOK, res, h.Logger) -} - -// NewDatabase creates a new database within the datastore -func (h *Service) NewDatabase(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - postedDB := &chronograf.Database{} - if err := json.NewDecoder(r.Body).Decode(postedDB); err != nil { - invalidJSON(w, h.Logger) - return - } - - if err := ValidDatabaseRequest(postedDB); err != nil { - invalidData(w, err, h.Logger) - return - } - - database, err := dbsvc.CreateDB(ctx, postedDB) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - - rps, err := h.allRPs(ctx, dbsvc, srcID, database.Name) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - res := newDBResponse(srcID, database.Name, rps) - encodeJSON(w, http.StatusCreated, res, h.Logger) -} - -// DropDatabase removes a database from a data source -func (h *Service) DropDatabase(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - db := httprouter.ParamsFromContext(ctx).ByName("db") - - dropErr := dbsvc.DropDB(ctx, db) - if dropErr != nil { - Error(w, http.StatusBadRequest, dropErr.Error(), h.Logger) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// RetentionPolicies lists retention policies within a database -func (h *Service) RetentionPolicies(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - db := httprouter.ParamsFromContext(ctx).ByName("db") - res, err := h.allRPs(ctx, dbsvc, srcID, db) - if err != nil { - msg := fmt.Sprintf("unable to connect get RPs %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - encodeJSON(w, http.StatusOK, res, h.Logger) -} - -func (h *Service) allRPs(ctx context.Context, dbsvc chronograf.Databases, srcID int, db string) ([]rpResponse, error) { - allRP, err := dbsvc.AllRP(ctx, db) - if err != nil { - return nil, err - } - - rps := make([]rpResponse, len(allRP)) - for i, rp := range allRP { - rp := rpResponse{ - Name: rp.Name, - Duration: rp.Duration, - Replication: rp.Replication, - ShardDuration: rp.ShardDuration, - Default: rp.Default, - } - rp.WithLinks(srcID, db) - rps[i] = rp - } - return rps, nil -} - -// NewRetentionPolicy creates a new retention policy for a database -func (h *Service) NewRetentionPolicy(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - postedRP := &chronograf.RetentionPolicy{} - if err := json.NewDecoder(r.Body).Decode(postedRP); err != nil { - invalidJSON(w, h.Logger) - return - } - if err := ValidRetentionPolicyRequest(postedRP); err != nil { - invalidData(w, err, h.Logger) - return - } - - db := httprouter.ParamsFromContext(ctx).ByName("db") - rp, err := dbsvc.CreateRP(ctx, db, postedRP) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - res := rpResponse{ - Name: rp.Name, - Duration: rp.Duration, - Replication: rp.Replication, - ShardDuration: rp.ShardDuration, - Default: rp.Default, - } - res.WithLinks(srcID, db) - encodeJSON(w, http.StatusCreated, res, h.Logger) -} - -// UpdateRetentionPolicy modifies an existing retention policy for a database -func (h *Service) UpdateRetentionPolicy(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - postedRP := &chronograf.RetentionPolicy{} - if err := json.NewDecoder(r.Body).Decode(postedRP); err != nil { - invalidJSON(w, h.Logger) - return - } - if err := ValidRetentionPolicyRequest(postedRP); err != nil { - invalidData(w, err, h.Logger) - return - } - - params := httprouter.ParamsFromContext(ctx) - db := params.ByName("db") - rp := params.ByName("rp") - p, err := dbsvc.UpdateRP(ctx, db, rp, postedRP) - - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), h.Logger) - return - } - - res := rpResponse{ - Name: p.Name, - Duration: p.Duration, - Replication: p.Replication, - ShardDuration: p.ShardDuration, - Default: p.Default, - } - res.WithLinks(srcID, db) - encodeJSON(w, http.StatusCreated, res, h.Logger) -} - -// DropRetentionPolicy removes a retention policy from a database -func (s *Service) DropRetentionPolicy(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - src, err := s.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, s.Logger) - return - } - - dbsvc := s.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - params := httprouter.ParamsFromContext(ctx) - db := params.ByName("db") - rp := params.ByName("rp") - dropErr := dbsvc.DropRP(ctx, db, rp) - if dropErr != nil { - Error(w, http.StatusBadRequest, dropErr.Error(), s.Logger) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -// Measurements lists measurements within a database -func (h *Service) Measurements(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - limit, offset, err := validMeasurementQuery(r.URL.Query()) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger) - return - } - - src, err := h.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, h.Logger) - return - } - - dbsvc := h.Databases - if err = dbsvc.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - db := httprouter.ParamsFromContext(ctx).ByName("db") - measurements, err := dbsvc.GetMeasurements(ctx, db, limit, offset) - if err != nil { - msg := fmt.Sprintf("Unable to get measurements %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, h.Logger) - return - } - - res := measurementsResponse{ - Measurements: measurements, - Links: newMeasurementLinks(srcID, db, limit, offset), - } - - encodeJSON(w, http.StatusOK, res, h.Logger) -} - -func validMeasurementQuery(query url.Values) (limit, offset int, err error) { - limitParam := query.Get(limitQuery) - if limitParam == "" { - limit = 100 - } else { - limit, err = strconv.Atoi(limitParam) - if err != nil { - return - } - if limit <= 0 { - limit = 100 - } - } - - offsetParam := query.Get(offsetQuery) - if offsetParam == "" { - offset = 0 - } else { - offset, err = strconv.Atoi(offsetParam) - if err != nil { - return - } - if offset < 0 { - offset = 0 - } - } - - return -} - -// ValidDatabaseRequest checks if the database posted is valid -func ValidDatabaseRequest(d *chronograf.Database) error { - if len(d.Name) == 0 { - return fmt.Errorf("name is required") - } - return nil -} - -// ValidRetentionPolicyRequest checks if a retention policy is valid on POST -func ValidRetentionPolicyRequest(rp *chronograf.RetentionPolicy) error { - if len(rp.Name) == 0 { - return fmt.Errorf("name is required") - } - if len(rp.Duration) == 0 { - return fmt.Errorf("duration is required") - } - if rp.Replication == 0 { - return fmt.Errorf("replication factor is invalid") - } - return nil -} diff --git a/chronograf/server/databases_test.go b/chronograf/server/databases_test.go deleted file mode 100644 index 5372da0806..0000000000 --- a/chronograf/server/databases_test.go +++ /dev/null @@ -1,648 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestService_GetDatabases(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.GetDatabases(tt.args.w, tt.args.r) - }) - } -} - -func TestService_NewDatabase(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.NewDatabase(tt.args.w, tt.args.r) - }) - } -} - -func TestService_DropDatabase(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.DropDatabase(tt.args.w, tt.args.r) - }) - } -} - -func TestService_RetentionPolicies(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.RetentionPolicies(tt.args.w, tt.args.r) - }) - } -} - -func TestService_NewRetentionPolicy(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.NewRetentionPolicy(tt.args.w, tt.args.r) - }) - } -} - -func TestService_UpdateRetentionPolicy(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.UpdateRetentionPolicy(tt.args.w, tt.args.r) - }) - } -} - -func TestService_DropRetentionPolicy(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - ServersStore chronograf.ServersStore - LayoutsStore chronograf.LayoutsStore - UsersStore chronograf.UsersStore - DashboardsStore chronograf.DashboardsStore - TimeSeriesClient TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - Databases chronograf.Databases - } - type args struct { - w http.ResponseWriter - r *http.Request - } - tests := []struct { - name string - fields fields - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := &Service{ - Store: &Store{ - SourcesStore: tt.fields.SourcesStore, - ServersStore: tt.fields.ServersStore, - LayoutsStore: tt.fields.LayoutsStore, - UsersStore: tt.fields.UsersStore, - DashboardsStore: tt.fields.DashboardsStore, - }, - TimeSeriesClient: tt.fields.TimeSeriesClient, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - Databases: tt.fields.Databases, - } - h.DropRetentionPolicy(tt.args.w, tt.args.r) - }) - } -} - -func TestService_Measurements(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - Logger chronograf.Logger - Databases chronograf.Databases - } - type args struct { - queryParams map[string]string - } - type wants struct { - statusCode int - body string - } - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "Gets 100 measurements when no limit or offset provided", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - Databases: &mocks.Databases{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - GetMeasurementsF: func(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) { - return []chronograf.Measurement{ - { - Name: "pineapple", - }, - { - Name: "cubeapple", - }, - { - Name: "pinecube", - }, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{}, - }, - wants: wants{ - statusCode: 200, - body: `{"measurements":[{"name":"pineapple"},{"name":"cubeapple"},{"name":"pinecube"}],"links":{"self":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","first":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","next":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=100"}} -`, - }, - }, - { - name: "Fails when invalid limit value provided", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{ - "limit": "joe", - }, - }, - wants: wants{ - statusCode: 422, - body: `{"code":422,"message":"strconv.Atoi: parsing \"joe\": invalid syntax"}`, - }, - }, - { - name: "Fails when invalid offset value provided", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{ - "offset": "bob", - }, - }, - wants: wants{ - statusCode: 422, - body: `{"code":422,"message":"strconv.Atoi: parsing \"bob\": invalid syntax"}`, - }, - }, - { - name: "Overrides limit less than or equal to 0 with limit 100", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - Databases: &mocks.Databases{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - GetMeasurementsF: func(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) { - return []chronograf.Measurement{ - { - Name: "pineapple", - }, - { - Name: "cubeapple", - }, - { - Name: "pinecube", - }, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{ - "limit": "0", - }, - }, - wants: wants{ - statusCode: 200, - body: `{"measurements":[{"name":"pineapple"},{"name":"cubeapple"},{"name":"pinecube"}],"links":{"self":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","first":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","next":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=100"}} -`, - }, - }, - { - name: "Overrides offset less than 0 with offset 0", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - Databases: &mocks.Databases{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - GetMeasurementsF: func(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) { - return []chronograf.Measurement{ - { - Name: "pineapple", - }, - { - Name: "cubeapple", - }, - { - Name: "pinecube", - }, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{ - "offset": "-1337", - }, - }, - wants: wants{ - statusCode: 200, - body: `{"measurements":[{"name":"pineapple"},{"name":"cubeapple"},{"name":"pinecube"}],"links":{"self":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","first":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=0","next":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=100\u0026offset=100"}} -`, - }, - }, - { - name: "Provides a prev link when offset exceeds limit", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, srcID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 0, - }, nil - }, - }, - Databases: &mocks.Databases{ - ConnectF: func(context.Context, *chronograf.Source) error { - return nil - }, - GetMeasurementsF: func(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) { - return []chronograf.Measurement{ - { - Name: "pineapple", - }, - { - Name: "cubeapple", - }, - { - Name: "pinecube", - }, - { - Name: "billietta", - }, - { - Name: "bobbetta", - }, - { - Name: "bobcube", - }, - }, nil - }, - }, - }, - args: args{ - queryParams: map[string]string{ - "limit": "2", - "offset": "4", - }, - }, - wants: wants{ - statusCode: 200, - body: `{"measurements":[{"name":"pineapple"},{"name":"cubeapple"},{"name":"pinecube"},{"name":"billietta"},{"name":"bobbetta"},{"name":"bobcube"}],"links":{"self":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=2\u0026offset=4","first":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=2\u0026offset=0","next":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=2\u0026offset=6","prev":"/chronograf/v1/sources/0/dbs/pineapples/measurements?limit=2\u0026offset=2"}} -`, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - logger := &chronograf.NoopLogger{} - h := &Service{ - Store: &mocks.Store{ - SourcesStore: tt.fields.SourcesStore, - }, - Logger: logger, - Databases: tt.fields.Databases, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest( - "GET", - "http://any.url", - nil, - ) - r = r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: "0", - }, - { - Key: "db", - Value: "pineapples", - }, - })) - - q := r.URL.Query() - for key, value := range tt.args.queryParams { - q.Add(key, value) - } - r.URL.RawQuery = q.Encode() - - h.Measurements(w, r) - - resp := w.Result() - body, err := ioutil.ReadAll(resp.Body) - defer resp.Body.Close() - - if err != nil { - t.Error("TestService_Measurements not able to retrieve body") - } - - var msmts measurementsResponse - if err := json.Unmarshal(body, &msmts); err != nil { - t.Error("TestService_Measurements not able to unmarshal JSON response") - } - - if tt.wants.statusCode != resp.StatusCode { - t.Errorf("%q. StatusCode:\nwant\n%v\ngot\n%v", tt.name, tt.wants.statusCode, resp.StatusCode) - } - - if tt.wants.body != string(body) { - t.Errorf("%q. Body:\nwant\n*%s*\ngot\n*%s*", tt.name, tt.wants.body, string(body)) - } - }) - } -} - -func TestValidDatabaseRequest(t *testing.T) { - type args struct { - d *chronograf.Database - } - tests := []struct { - name string - args args - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidDatabaseRequest(tt.args.d); (err != nil) != tt.wantErr { - t.Errorf("ValidDatabaseRequest() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestValidRetentionPolicyRequest(t *testing.T) { - type args struct { - rp *chronograf.RetentionPolicy - } - tests := []struct { - name string - args args - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidRetentionPolicyRequest(tt.args.rp); (err != nil) != tt.wantErr { - t.Errorf("ValidRetentionPolicyRequest() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/chronograf/server/env.go b/chronograf/server/env.go deleted file mode 100644 index 8573e77e0c..0000000000 --- a/chronograf/server/env.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -type envResponse struct { - Links selfLinks `json:"links"` - TelegrafSystemInterval string `json:"telegrafSystemInterval"` -} - -func newEnvResponse(env chronograf.Environment) *envResponse { - return &envResponse{ - Links: selfLinks{ - Self: "/chronograf/v1/env", - }, - TelegrafSystemInterval: env.TelegrafSystemInterval.String(), - } -} - -// Environment retrieves the global application configuration -func (s *Service) Environment(w http.ResponseWriter, r *http.Request) { - res := newEnvResponse(s.Env) - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/env_test.go b/chronograf/server/env_test.go deleted file mode 100644 index 07a8ee88f7..0000000000 --- a/chronograf/server/env_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package server - -import ( - "io/ioutil" - "net/http/httptest" - "testing" - "time" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -func TestEnvironment(t *testing.T) { - type fields struct { - Environment chronograf.Environment - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - wants wants - }{ - { - name: "Get environment", - fields: fields{ - Environment: chronograf.Environment{ - TelegrafSystemInterval: 1 * time.Minute, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"1m0s"}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Env: tt.fields.Environment, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - s.Environment(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} diff --git a/chronograf/server/helpers.go b/chronograf/server/helpers.go deleted file mode 100644 index 8952734982..0000000000 --- a/chronograf/server/helpers.go +++ /dev/null @@ -1,7 +0,0 @@ -package server - -import "net/http" - -func location(w http.ResponseWriter, self string) { - w.Header().Add("Location", self) -} diff --git a/chronograf/server/hsts.go b/chronograf/server/hsts.go deleted file mode 100644 index 1b6f54d71a..0000000000 --- a/chronograf/server/hsts.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -import "net/http" - -// HSTS add HTTP Strict Transport Security header with a max-age of two years -// Inspired from https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go -func HSTS(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") - next.ServeHTTP(w, r) - }) -} diff --git a/chronograf/server/influx.go b/chronograf/server/influx.go deleted file mode 100644 index a878687970..0000000000 --- a/chronograf/server/influx.go +++ /dev/null @@ -1,142 +0,0 @@ -package server - -import ( - "crypto/tls" - "encoding/json" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "time" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/influx" -) - -// ValidInfluxRequest checks if queries specify a command. -func ValidInfluxRequest(p chronograf.Query) error { - if p.Command == "" { - return fmt.Errorf("query field required") - } - return nil -} - -type postInfluxResponse struct { - Results interface{} `json:"results"` // results from influx -} - -// Influx proxies requests to influxdb. -func (s *Service) Influx(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - var req chronograf.Query - if err = json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - if err = ValidInfluxRequest(req); err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", id, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - response, err := ts.Query(ctx, req) - if err != nil { - if err == chronograf.ErrUpstreamTimeout { - msg := "Timeout waiting for Influx response" - Error(w, http.StatusRequestTimeout, msg, s.Logger) - return - } - // TODO: Here I want to return the error code from influx. - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := postInfluxResponse{ - Results: response, - } - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -func (s *Service) Write(w http.ResponseWriter, r *http.Request) { - id, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, id) - if err != nil { - notFound(w, id, s.Logger) - return - } - - u, err := url.Parse(src.URL) - if err != nil { - msg := fmt.Sprintf("Error parsing source url: %v", err) - Error(w, http.StatusUnprocessableEntity, msg, s.Logger) - return - } - u.Path = "/write" - u.RawQuery = r.URL.RawQuery - - director := func(req *http.Request) { - // Set the Host header of the original source URL - req.Host = u.Host - req.URL = u - // Because we are acting as a proxy, influxdb needs to have the - // basic auth or bearer token information set as a header directly - auth := influx.DefaultAuthorization(&src) - auth.Set(req) - } - - proxy := &httputil.ReverseProxy{ - Director: director, - } - - // The connection to influxdb is using a self-signed certificate. - // This modifies uses the same values as http.DefaultTransport but specifies - // InsecureSkipVerify - if src.InsecureSkipVerify { - proxy.Transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - - proxy.ServeHTTP(w, r) -} diff --git a/chronograf/server/influx_test.go b/chronograf/server/influx_test.go deleted file mode 100644 index 715fb6681e..0000000000 --- a/chronograf/server/influx_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package server - -import ( - "bytes" - "context" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestService_Influx(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - TimeSeries TimeSeriesClient - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - type want struct { - StatusCode int - ContentType string - Body string - } - tests := []struct { - name string - fields fields - args args - ID string - want want - }{ - { - name: "Proxies request to Influxdb", - fields: fields{ - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 1337, - URL: "http://any.url", - }, nil - }, - }, - TimeSeries: &mocks.TimeSeries{ - ConnectF: func(ctx context.Context, src *chronograf.Source) error { - return nil - }, - QueryF: func(ctx context.Context, query chronograf.Query) (chronograf.Response, error) { - return mocks.NewResponse( - `{"results":[{"statement_id":0,"series":[{"name":"cpu","columns":["key","value"],"values":[["cpu","cpu-total"],["cpu","cpu0"],["cpu","cpu1"],["cpu","cpu2"],["cpu","cpu3"],["host","pineapples-MBP"],["host","pineapples-MacBook-Pro.local"]]}]}]}`, - nil, - ), - nil - }, - }, - }, - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "POST", - "http://any.url", - ioutil.NopCloser( - bytes.NewReader([]byte( - `{"db":"bob", "rp":"joe", "query":"SELECT mean(\"usage_user\") FROM cpu WHERE \"cpu\" = 'cpu-total' AND time > now() - 10m GROUP BY host;"}`, - )), - ), - ), - }, - ID: "1", - want: want{ - StatusCode: http.StatusOK, - ContentType: "application/json", - Body: `{"results":{"results":[{"statement_id":0,"series":[{"name":"cpu","columns":["key","value"],"values":[["cpu","cpu-total"],["cpu","cpu0"],["cpu","cpu1"],["cpu","cpu2"],["cpu","cpu3"],["host","pineapples-MBP"],["host","pineapples-MacBook-Pro.local"]]}]}]}} -`, - }, - }, - } - - for _, tt := range tests { - tt.args.r = tt.args.r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: tt.ID, - }, - })) - h := &Service{ - Store: &mocks.Store{ - SourcesStore: tt.fields.SourcesStore, - }, - TimeSeriesClient: tt.fields.TimeSeries, - } - h.Influx(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - contentType := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.want.StatusCode { - t.Errorf("%q. Influx() = got %v, want %v", tt.name, resp.StatusCode, tt.want.StatusCode) - } - if contentType != tt.want.ContentType { - t.Errorf("%q. Influx() = got %v, want %v", tt.name, contentType, tt.want.ContentType) - } - if string(body) != tt.want.Body { - t.Errorf("%q. Influx() =\ngot ***%v***\nwant ***%v***\n", tt.name, string(body), tt.want.Body) - } - - } -} diff --git a/chronograf/server/kapacitors.go b/chronograf/server/kapacitors.go deleted file mode 100644 index 3fc09da3a7..0000000000 --- a/chronograf/server/kapacitors.go +++ /dev/null @@ -1,791 +0,0 @@ -package server - -// TODO(desa): resolve kapacitor dependency - -//type postKapacitorRequest struct { -// Name *string `json:"name"` // User facing name of kapacitor instance.; Required: true -// URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true -// Username string `json:"username,omitempty"` // Username for authentication to kapacitor -// Password string `json:"password,omitempty"` -// InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. -// Active bool `json:"active"` -// Organization string `json:"organization"` // Organization is the organization ID that resource belongs to -//} -// -//func (p *postKapacitorRequest) Valid(defaultOrgID string) error { -// if p.Name == nil || p.URL == nil { -// return fmt.Errorf("name and url required") -// } -// -// if p.Organization == "" { -// p.Organization = defaultOrgID -// } -// -// url, err := url.ParseRequestURI(*p.URL) -// if err != nil { -// return fmt.Errorf("invalid source URI: %v", err) -// } -// if len(url.Scheme) == 0 { -// return fmt.Errorf("invalid URL; no URL scheme defined") -// } -// -// return nil -//} -// -//type kapaLinks struct { -// Proxy string `json:"proxy"` // URL location of proxy endpoint for this source -// Self string `json:"self"` // Self link mapping to this resource -// Rules string `json:"rules"` // Rules link for defining roles alerts for kapacitor -// Tasks string `json:"tasks"` // Tasks link to define a task against the proxy -// Ping string `json:"ping"` // Ping path to kapacitor -//} -// -//type kapacitor struct { -// ID int `json:"id,string"` // Unique identifier representing a kapacitor instance. -// Name string `json:"name"` // User facing name of kapacitor instance. -// URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092) -// Username string `json:"username,omitempty"` // Username for authentication to kapacitor -// Password string `json:"password,omitempty"` -// InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. -// Active bool `json:"active"` -// Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor -//} -// -//// NewKapacitor adds valid kapacitor store store. -//func (s *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) { -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// _, err = s.Store.Sources(ctx).Get(ctx, srcID) -// if err != nil { -// notFound(w, srcID, s.Logger) -// return -// } -// -// var req postKapacitorRequest -// if err = json.NewDecoder(r.Body).Decode(&req); err != nil { -// invalidJSON(w, s.Logger) -// return -// } -// -// defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) -// if err != nil { -// unknownErrorWithMessage(w, err, s.Logger) -// return -// } -// -// if err := req.Valid(defaultOrg.ID); err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// -// srv := chronograf.Server{ -// SrcID: srcID, -// Name: *req.Name, -// Username: req.Username, -// Password: req.Password, -// InsecureSkipVerify: req.InsecureSkipVerify, -// URL: *req.URL, -// Active: req.Active, -// Organization: req.Organization, -// } -// -// if srv, err = s.Store.Servers(ctx).Add(ctx, srv); err != nil { -// msg := fmt.Errorf("error storing kapacitor %v: %v", req, err) -// unknownErrorWithMessage(w, msg, s.Logger) -// return -// } -// -// res := newKapacitor(srv) -// location(w, res.Links.Self) -// encodeJSON(w, http.StatusCreated, res, s.Logger) -//} -// -//func newKapacitor(srv chronograf.Server) kapacitor { -// httpAPISrcs := "/chronograf/v1/sources" -// return kapacitor{ -// ID: srv.ID, -// Name: srv.Name, -// Username: srv.Username, -// URL: srv.URL, -// Active: srv.Active, -// InsecureSkipVerify: srv.InsecureSkipVerify, -// Links: kapaLinks{ -// Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID), -// Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID), -// Rules: fmt.Sprintf("%s/%d/kapacitors/%d/rules", httpAPISrcs, srv.SrcID, srv.ID), -// Tasks: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/tasks", httpAPISrcs, srv.SrcID, srv.ID), -// Ping: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/ping", httpAPISrcs, srv.SrcID, srv.ID), -// }, -// } -//} -// -//type kapacitors struct { -// Kapacitors []kapacitor `json:"kapacitors"` -//} -// -//// Kapacitors retrieves all kapacitors from store. -//func (s *Service) Kapacitors(w http.ResponseWriter, r *http.Request) { -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// mrSrvs, err := s.Store.Servers(ctx).All(ctx) -// if err != nil { -// Error(w, http.StatusInternalServerError, "Error loading kapacitors", s.Logger) -// return -// } -// -// srvs := []kapacitor{} -// for _, srv := range mrSrvs { -// if srv.SrcID == srcID && srv.Type == "" { -// srvs = append(srvs, newKapacitor(srv)) -// } -// } -// -// res := kapacitors{ -// Kapacitors: srvs, -// } -// -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// KapacitorsID retrieves a kapacitor with ID from store. -//func (s *Service) KapacitorsID(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID || srv.Type != "" { -// notFound(w, id, s.Logger) -// return -// } -// -// res := newKapacitor(srv) -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// RemoveKapacitor deletes kapacitor from store. -//func (s *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID || srv.Type != "" { -// notFound(w, id, s.Logger) -// return -// } -// -// if err = s.Store.Servers(ctx).Delete(ctx, srv); err != nil { -// unknownErrorWithMessage(w, err, s.Logger) -// return -// } -// -// w.WriteHeader(http.StatusNoContent) -//} -// -//type patchKapacitorRequest struct { -// Name *string `json:"name,omitempty"` // User facing name of kapacitor instance. -// URL *string `json:"url,omitempty"` // URL for the kapacitor -// Username *string `json:"username,omitempty"` // Username for kapacitor auth -// Password *string `json:"password,omitempty"` -// InsecureSkipVerify *bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. -// Active *bool `json:"active"` -//} -// -//func (p *patchKapacitorRequest) Valid() error { -// if p.URL != nil { -// url, err := url.ParseRequestURI(*p.URL) -// if err != nil { -// return fmt.Errorf("invalid source URI: %v", err) -// } -// if len(url.Scheme) == 0 { -// return fmt.Errorf("invalid URL; no URL scheme defined") -// } -// } -// return nil -//} -// -//// UpdateKapacitor incrementally updates a kapacitor definition in the store -//func (s *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID || srv.Type != "" { -// notFound(w, id, s.Logger) -// return -// } -// -// var req patchKapacitorRequest -// if err := json.NewDecoder(r.Body).Decode(&req); err != nil { -// invalidJSON(w, s.Logger) -// return -// } -// -// if err := req.Valid(); err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// -// if req.Name != nil { -// srv.Name = *req.Name -// } -// if req.URL != nil { -// srv.URL = *req.URL -// } -// if req.Password != nil { -// srv.Password = *req.Password -// } -// if req.Username != nil { -// srv.Username = *req.Username -// } -// if req.InsecureSkipVerify != nil { -// srv.InsecureSkipVerify = *req.InsecureSkipVerify -// } -// if req.Active != nil { -// srv.Active = *req.Active -// } -// -// if err := s.Store.Servers(ctx).Update(ctx, srv); err != nil { -// msg := fmt.Sprintf("Error updating kapacitor ID %d", id) -// Error(w, http.StatusInternalServerError, msg, s.Logger) -// return -// } -// -// res := newKapacitor(srv) -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// KapacitorRulesPost proxies POST to kapacitor -//func (s *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// -// var req chronograf.AlertRule -// if err = json.NewDecoder(r.Body).Decode(&req); err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// // TODO: validate this data -// /* -// if err := req.Valid(); err != nil { -// invalidData(w, err) -// return -// } -// */ -// -// if req.Name == "" { -// req.Name = req.ID -// } -// -// req.ID = "" -// task, err := c.Create(ctx, req) -// if err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// res := newAlertResponse(task, srv.SrcID, srv.ID) -// location(w, res.Links.Self) -// encodeJSON(w, http.StatusCreated, res, s.Logger) -//} -// -//type alertLinks struct { -// Self string `json:"self"` -// Kapacitor string `json:"kapacitor"` -// Output string `json:"output"` -//} -// -//type alertResponse struct { -// chronograf.AlertRule -// Links alertLinks `json:"links"` -//} -// -//// newAlertResponse formats task into an alertResponse -//func newAlertResponse(task *kapa.Task, srcID, kapaID int) *alertResponse { -// res := &alertResponse{ -// AlertRule: task.Rule, -// Links: alertLinks{ -// Self: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/rules/%s", srcID, kapaID, task.ID), -// Kapacitor: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srcID, kapaID, url.QueryEscape(task.Href)), -// Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srcID, kapaID, url.QueryEscape(task.HrefOutput)), -// }, -// } -// -// if res.AlertNodes.Alerta == nil { -// res.AlertNodes.Alerta = []*chronograf.Alerta{} -// } -// -// for i, a := range res.AlertNodes.Alerta { -// if a.Service == nil { -// a.Service = []string{} -// res.AlertNodes.Alerta[i] = a -// } -// } -// -// if res.AlertNodes.Email == nil { -// res.AlertNodes.Email = []*chronograf.Email{} -// } -// -// for i, a := range res.AlertNodes.Email { -// if a.To == nil { -// a.To = []string{} -// res.AlertNodes.Email[i] = a -// } -// } -// -// if res.AlertNodes.Exec == nil { -// res.AlertNodes.Exec = []*chronograf.Exec{} -// } -// -// for i, a := range res.AlertNodes.Exec { -// if a.Command == nil { -// a.Command = []string{} -// res.AlertNodes.Exec[i] = a -// } -// } -// -// if res.AlertNodes.HipChat == nil { -// res.AlertNodes.HipChat = []*chronograf.HipChat{} -// } -// -// if res.AlertNodes.Kafka == nil { -// res.AlertNodes.Kafka = []*chronograf.Kafka{} -// } -// -// if res.AlertNodes.Log == nil { -// res.AlertNodes.Log = []*chronograf.Log{} -// } -// -// if res.AlertNodes.OpsGenie == nil { -// res.AlertNodes.OpsGenie = []*chronograf.OpsGenie{} -// } -// -// for i, a := range res.AlertNodes.OpsGenie { -// if a.Teams == nil { -// a.Teams = []string{} -// res.AlertNodes.OpsGenie[i] = a -// } -// -// if a.Recipients == nil { -// a.Recipients = []string{} -// res.AlertNodes.OpsGenie[i] = a -// } -// } -// -// if res.AlertNodes.OpsGenie2 == nil { -// res.AlertNodes.OpsGenie2 = []*chronograf.OpsGenie{} -// } -// -// for i, a := range res.AlertNodes.OpsGenie2 { -// if a.Teams == nil { -// a.Teams = []string{} -// res.AlertNodes.OpsGenie2[i] = a -// } -// -// if a.Recipients == nil { -// a.Recipients = []string{} -// res.AlertNodes.OpsGenie2[i] = a -// } -// } -// -// if res.AlertNodes.PagerDuty == nil { -// res.AlertNodes.PagerDuty = []*chronograf.PagerDuty{} -// } -// -// if res.AlertNodes.PagerDuty2 == nil { -// res.AlertNodes.PagerDuty2 = []*chronograf.PagerDuty{} -// } -// -// if res.AlertNodes.Posts == nil { -// res.AlertNodes.Posts = []*chronograf.Post{} -// } -// -// for i, a := range res.AlertNodes.Posts { -// if a.Headers == nil { -// a.Headers = map[string]string{} -// res.AlertNodes.Posts[i] = a -// } -// } -// -// if res.AlertNodes.Pushover == nil { -// res.AlertNodes.Pushover = []*chronograf.Pushover{} -// } -// -// if res.AlertNodes.Sensu == nil { -// res.AlertNodes.Sensu = []*chronograf.Sensu{} -// } -// -// for i, a := range res.AlertNodes.Sensu { -// if a.Handlers == nil { -// a.Handlers = []string{} -// res.AlertNodes.Sensu[i] = a -// } -// } -// -// if res.AlertNodes.Slack == nil { -// res.AlertNodes.Slack = []*chronograf.Slack{} -// } -// -// if res.AlertNodes.Talk == nil { -// res.AlertNodes.Talk = []*chronograf.Talk{} -// } -// -// if res.AlertNodes.TCPs == nil { -// res.AlertNodes.TCPs = []*chronograf.TCP{} -// } -// -// if res.AlertNodes.Telegram == nil { -// res.AlertNodes.Telegram = []*chronograf.Telegram{} -// } -// -// if res.AlertNodes.VictorOps == nil { -// res.AlertNodes.VictorOps = []*chronograf.VictorOps{} -// } -// -// if res.Query != nil { -// if res.Query.ID == "" { -// res.Query.ID = res.ID -// } -// -// if res.Query.Fields == nil { -// res.Query.Fields = make([]chronograf.Field, 0) -// } -// -// if res.Query.GroupBy.Tags == nil { -// res.Query.GroupBy.Tags = make([]string, 0) -// } -// -// if res.Query.Tags == nil { -// res.Query.Tags = make(map[string][]string) -// } -// } -// return res -//} -// -//// ValidRuleRequest checks if the requested rule change is valid -//func ValidRuleRequest(rule chronograf.AlertRule) error { -// if rule.Query == nil { -// return fmt.Errorf("invalid alert rule: no query defined") -// } -// var hasFuncs bool -// for _, f := range rule.Query.Fields { -// if f.Type == "func" && len(f.Args) > 0 { -// hasFuncs = true -// } -// } -// // All kapacitor rules with functions must have a window that is applied -// // every amount of time -// if rule.Every == "" && hasFuncs { -// return fmt.Errorf(`invalid alert rule: functions require an "every" window`) -// } -// return nil -//} -// -//// KapacitorRulesPut proxies PATCH to kapacitor -//func (s *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// -// tid := httprouter.GetParamFromContext(ctx, "tid") -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// var req chronograf.AlertRule -// if err = json.NewDecoder(r.Body).Decode(&req); err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// // TODO: validate this data -// /* -// if err := req.Valid(); err != nil { -// invalidData(w, err) -// return -// } -// */ -// -// // Check if the rule exists and is scoped correctly -// if _, err = c.Get(ctx, tid); err != nil { -// if err == chronograf.ErrAlertNotFound { -// notFound(w, id, s.Logger) -// return -// } -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// // Replace alert completely with this new alert. -// req.ID = tid -// task, err := c.Update(ctx, c.Href(tid), req) -// if err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// res := newAlertResponse(task, srv.SrcID, srv.ID) -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// KapacitorStatus is the current state of a running task -//type KapacitorStatus struct { -// Status string `json:"status"` -//} -// -//// Valid check if the kapacitor status is enabled or disabled -//func (k *KapacitorStatus) Valid() error { -// if k.Status == "enabled" || k.Status == "disabled" { -// return nil -// } -// return fmt.Errorf("invalid Kapacitor status: %s", k.Status) -//} -// -//// KapacitorRulesStatus proxies PATCH to kapacitor to enable/disable tasks -//func (s *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// -// tid := httprouter.GetParamFromContext(ctx, "tid") -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// -// var req KapacitorStatus -// if err = json.NewDecoder(r.Body).Decode(&req); err != nil { -// invalidJSON(w, s.Logger) -// return -// } -// if err := req.Valid(); err != nil { -// invalidData(w, err, s.Logger) -// return -// } -// -// // Check if the rule exists and is scoped correctly -// _, err = c.Get(ctx, tid) -// if err != nil { -// if err == chronograf.ErrAlertNotFound { -// notFound(w, id, s.Logger) -// return -// } -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// var task *kapa.Task -// if req.Status == "enabled" { -// task, err = c.Enable(ctx, c.Href(tid)) -// } else { -// task, err = c.Disable(ctx, c.Href(tid)) -// } -// -// if err != nil { -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// res := newAlertResponse(task, srv.SrcID, srv.ID) -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// KapacitorRulesGet retrieves all rules -//func (s *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// tasks, err := c.All(ctx) -// if err != nil { -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// res := allAlertsResponse{ -// Rules: []*alertResponse{}, -// } -// for _, task := range tasks { -// ar := newAlertResponse(task, srv.SrcID, srv.ID) -// res.Rules = append(res.Rules, ar) -// } -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//type allAlertsResponse struct { -// Rules []*alertResponse `json:"rules"` -//} -// -//// KapacitorRulesID retrieves specific task -//func (s *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// tid := httprouter.GetParamFromContext(ctx, "tid") -// -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// -// // Check if the rule exists within scope -// task, err := c.Get(ctx, tid) -// if err != nil { -// if err == chronograf.ErrAlertNotFound { -// notFound(w, id, s.Logger) -// return -// } -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// res := newAlertResponse(task, srv.SrcID, srv.ID) -// encodeJSON(w, http.StatusOK, res, s.Logger) -//} -// -//// KapacitorRulesDelete proxies DELETE to kapacitor -//func (s *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) { -// id, err := paramID("kid", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// srcID, err := paramID("id", r) -// if err != nil { -// Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) -// return -// } -// -// ctx := r.Context() -// srv, err := s.Store.Servers(ctx).Get(ctx, id) -// if err != nil || srv.SrcID != srcID { -// notFound(w, id, s.Logger) -// return -// } -// -// c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) -// -// tid := httprouter.GetParamFromContext(ctx, "tid") -// // Check if the rule is linked to this server and kapacitor -// if _, err := c.Get(ctx, tid); err != nil { -// if err == chronograf.ErrAlertNotFound { -// notFound(w, id, s.Logger) -// return -// } -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// if err := c.Delete(ctx, c.Href(tid)); err != nil { -// Error(w, http.StatusInternalServerError, err.Error(), s.Logger) -// return -// } -// -// w.WriteHeader(http.StatusNoContent) -//} diff --git a/chronograf/server/kapacitors_test.go b/chronograf/server/kapacitors_test.go deleted file mode 100644 index 59f544b3fc..0000000000 --- a/chronograf/server/kapacitors_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package server_test - -//const tickScript = ` -//stream -// |from() -// .measurement('cpu') -// |alert() -// .crit(lambda: "usage_idle" < 10) -// .log('/tmp/alert') -//` -// -//func TestValidRuleRequest(t *testing.T) { -// tests := []struct { -// name string -// rule chronograf.AlertRule -// wantErr bool -// }{ -// { -// name: "No every with functions", -// rule: chronograf.AlertRule{ -// Query: &chronograf.QueryConfig{ -// Fields: []chronograf.Field{ -// { -// Value: "max", -// Type: "func", -// Args: []chronograf.Field{ -// { -// Value: "oldmanpeabody", -// Type: "field", -// }, -// }, -// }, -// }, -// }, -// }, -// wantErr: true, -// }, -// { -// name: "With every", -// rule: chronograf.AlertRule{ -// Every: "10s", -// Query: &chronograf.QueryConfig{ -// Fields: []chronograf.Field{ -// { -// Value: "max", -// Type: "func", -// Args: []chronograf.Field{ -// { -// Value: "oldmanpeabody", -// Type: "field", -// }, -// }, -// }, -// }, -// }, -// }, -// }, -// { -// name: "No query config", -// rule: chronograf.AlertRule{}, -// wantErr: true, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// if err := server.ValidRuleRequest(tt.rule); (err != nil) != tt.wantErr { -// t.Errorf("ValidRuleRequest() error = %v, wantErr %v", err, tt.wantErr) -// } -// }) -// } -//} -// -//func Test_KapacitorRulesGet(t *testing.T) { -// kapaTests := []struct { -// name string -// requestPath string -// mockAlerts []chronograf.AlertRule -// expected []chronograf.AlertRule -// }{ -// { -// name: "basic", -// requestPath: "/chronograf/v1/sources/1/kapacitors/1/rules", -// mockAlerts: []chronograf.AlertRule{ -// { -// ID: "cpu_alert", -// Name: "cpu_alert", -// Status: "enabled", -// Type: "stream", -// DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}}, -// TICKScript: tickScript, -// }, -// }, -// expected: []chronograf.AlertRule{ -// { -// ID: "cpu_alert", -// Name: "cpu_alert", -// Status: "enabled", -// Type: "stream", -// DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}}, -// TICKScript: tickScript, -// AlertNodes: chronograf.AlertNodes{ -// Posts: []*chronograf.Post{}, -// TCPs: []*chronograf.TCP{}, -// Email: []*chronograf.Email{}, -// Exec: []*chronograf.Exec{}, -// Log: []*chronograf.Log{}, -// VictorOps: []*chronograf.VictorOps{}, -// PagerDuty: []*chronograf.PagerDuty{}, -// PagerDuty2: []*chronograf.PagerDuty{}, -// Pushover: []*chronograf.Pushover{}, -// Sensu: []*chronograf.Sensu{}, -// Slack: []*chronograf.Slack{}, -// Telegram: []*chronograf.Telegram{}, -// HipChat: []*chronograf.HipChat{}, -// Alerta: []*chronograf.Alerta{}, -// OpsGenie: []*chronograf.OpsGenie{}, -// OpsGenie2: []*chronograf.OpsGenie{}, -// Talk: []*chronograf.Talk{}, -// Kafka: []*chronograf.Kafka{}, -// }, -// }, -// }, -// }, -// } -// -// for _, test := range kapaTests { -// test := test // needed to avoid data race -// t.Run(test.name, func(t *testing.T) { -// t.Parallel() -// -// // setup mock kapa API -// kapaSrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { -// params := r.URL.Query() -// limit, err := strconv.Atoi(params.Get("limit")) -// if err != nil { -// rw.WriteHeader(http.StatusBadRequest) -// return -// } -// offset, err := strconv.Atoi(params.Get("offset")) -// if err != nil { -// rw.WriteHeader(http.StatusBadRequest) -// return -// } -// -// tsks := []map[string]interface{}{} -// for _, task := range test.mockAlerts { -// tsks = append(tsks, map[string]interface{}{ -// "id": task.ID, -// "script": tickScript, -// "status": "enabled", -// "type": "stream", -// "dbrps": []chronograf.DBRP{ -// { -// DB: "telegraf", -// RP: "autogen", -// }, -// }, -// "link": map[string]interface{}{ -// "rel": "self", -// "href": "/kapacitor/v1/tasks/cpu_alert", -// }, -// }) -// } -// -// var tasks map[string]interface{} -// -// if offset >= len(tsks) { -// tasks = map[string]interface{}{ -// "tasks": []map[string]interface{}{}, -// } -// } else if limit+offset > len(tsks) { -// tasks = map[string]interface{}{ -// "tasks": tsks[offset:], -// } -// } -// //} else { -// //tasks = map[string]interface{}{ -// //"tasks": tsks[offset : offset+limit], -// //} -// //} -// -// err = json.NewEncoder(rw).Encode(&tasks) -// if err != nil { -// t.Error("Failed to encode JSON. err:", err) -// } -// })) -// defer kapaSrv.Close() -// -// // setup mock service and test logger -// testLogger := mocks.TestLogger{} -// svc := &server.Service{ -// Store: &mocks.Store{ -// SourcesStore: &mocks.SourcesStore{ -// GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { -// return chronograf.Source{ -// ID: ID, -// InsecureSkipVerify: true, -// }, nil -// }, -// }, -// ServersStore: &mocks.ServersStore{ -// GetF: func(ctx context.Context, ID int) (chronograf.Server, error) { -// return chronograf.Server{ -// SrcID: ID, -// URL: kapaSrv.URL, -// }, nil -// }, -// }, -// }, -// Logger: &testLogger, -// } -// -// // setup request and response recorder -// req := httptest.NewRequest("GET", test.requestPath, strings.NewReader("")) -// rr := httptest.NewRecorder() -// -// // setup context and request params -// bg := context.Background() -// params := httprouter.Params{ -// { -// Key: "id", -// Value: "1", -// }, -// { -// Key: "kid", -// Value: "1", -// }, -// } -// ctx := httprouter.WithParams(bg, params) -// req = req.WithContext(ctx) -// -// // invoke KapacitorRulesGet endpoint -// svc.KapacitorRulesGet(rr, req) -// -// // destructure response -// frame := struct { -// Rules []struct { -// chronograf.AlertRule -// Links json.RawMessage `json:"links"` -// } `json:"rules"` -// }{} -// -// resp := rr.Result() -// -// err := json.NewDecoder(resp.Body).Decode(&frame) -// if err != nil { -// t.Fatal("Err decoding kapa rule response: err:", err) -// } -// -// actual := make([]chronograf.AlertRule, len(frame.Rules)) -// -// for i := range frame.Rules { -// actual[i] = frame.Rules[i].AlertRule -// } -// -// if resp.StatusCode != http.StatusOK { -// t.Fatal("Expected HTTP 200 OK but got", resp.Status) -// } -// -// if !cmp.Equal(test.expected, actual) { -// t.Fatalf("%q - Alert rules differ! diff:\n%s\n", test.name, cmp.Diff(test.expected, actual)) -// } -// }) -// } -//} diff --git a/chronograf/server/layout.go b/chronograf/server/layout.go deleted file mode 100644 index 9c3ad03277..0000000000 --- a/chronograf/server/layout.go +++ /dev/null @@ -1,119 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" -) - -type link struct { - Href string `json:"href"` - Rel string `json:"rel"` -} - -type layoutResponse struct { - chronograf.Layout - Link link `json:"link"` -} - -func newLayoutResponse(layout chronograf.Layout) layoutResponse { - httpAPILayouts := "/chronograf/v1/layouts" - href := fmt.Sprintf("%s/%s", httpAPILayouts, layout.ID) - rel := "self" - - for idx, cell := range layout.Cells { - axes := []string{"x", "y", "y2"} - - if cell.Axes == nil { - layout.Cells[idx].Axes = make(map[string]chronograf.Axis, len(axes)) - } - - if cell.CellColors == nil { - layout.Cells[idx].CellColors = []chronograf.CellColor{} - } - - for _, axis := range axes { - if _, found := cell.Axes[axis]; !found { - layout.Cells[idx].Axes[axis] = chronograf.Axis{ - Bounds: []string{}, - } - } - } - } - - return layoutResponse{ - Layout: layout, - Link: link{ - Href: href, - Rel: rel, - }, - } -} - -type getLayoutsResponse struct { - Layouts []layoutResponse `json:"layouts"` -} - -// Layouts retrieves all layouts from store -func (s *Service) Layouts(w http.ResponseWriter, r *http.Request) { - // Construct a filter sieve for both applications and measurements - filtered := map[string]bool{} - for _, a := range r.URL.Query()["app"] { - filtered[a] = true - } - - for _, m := range r.URL.Query()["measurement"] { - filtered[m] = true - } - - ctx := r.Context() - layouts, err := s.Store.Layouts(ctx).All(ctx) - if err != nil { - Error(w, http.StatusInternalServerError, "Error loading layouts", s.Logger) - return - } - - filter := func(layout *chronograf.Layout) bool { - // If the length of the filter is zero then all values are acceptable. - if len(filtered) == 0 { - return true - } - - // If filter contains either measurement or application - return filtered[layout.Measurement] || filtered[layout.Application] - } - - res := getLayoutsResponse{ - Layouts: []layoutResponse{}, - } - - seen := make(map[string]bool) - for _, layout := range layouts { - // remove duplicates - if seen[layout.Measurement+layout.ID] { - continue - } - // filter for data that belongs to provided application or measurement - if filter(&layout) { - res.Layouts = append(res.Layouts, newLayoutResponse(layout)) - } - } - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// LayoutsID retrieves layout with ID from store -func (s *Service) LayoutsID(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "id") - - layout, err := s.Store.Layouts(ctx).Get(ctx, id) - if err != nil { - Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), s.Logger) - return - } - - res := newLayoutResponse(layout) - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/layout_test.go b/chronograf/server/layout_test.go deleted file mode 100644 index c2c4103703..0000000000 --- a/chronograf/server/layout_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package server_test - -import ( - "context" - "encoding/json" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/server" -) - -func Test_Layouts(t *testing.T) { - layoutTests := []struct { - name string - expected chronograf.Layout - allLayouts []chronograf.Layout - focusedApp string // should filter all layouts to this app only - shouldErr bool - }{ - { - "empty layout", - chronograf.Layout{}, - []chronograf.Layout{}, - "", - false, - }, - { - "several layouts", - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - }, - []chronograf.Layout{ - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - }, - }, - "", - false, - }, - { - "filtered app", - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - }, - []chronograf.Layout{ - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - }, - chronograf.Layout{ - ID: "b020101b-ea6b-4c8c-9f0e-db0ba501f4ef", - Application: "chronograf", - Measurement: "chronograf", - }, - }, - "influxdb", - false, - }, - { - "axis zero values", - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - Cells: []chronograf.Cell{ - { - X: 0, - Y: 0, - W: 4, - H: 4, - I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", - Name: "A Graph", - CellColors: []chronograf.CellColor{}, - Axes: map[string]chronograf.Axis{ - "x": chronograf.Axis{ - Bounds: []string{}, - }, - "y": chronograf.Axis{ - Bounds: []string{}, - }, - "y2": chronograf.Axis{ - Bounds: []string{}, - }, - }, - }, - }, - }, - []chronograf.Layout{ - chronograf.Layout{ - ID: "d20a21c8-69f1-4780-90fe-e69f5e4d138c", - Application: "influxdb", - Measurement: "influxdb", - Cells: []chronograf.Cell{ - { - X: 0, - Y: 0, - W: 4, - H: 4, - I: "3b0e646b-2ca3-4df2-95a5-fd80915459dd", - CellColors: []chronograf.CellColor{}, - Name: "A Graph", - }, - }, - }, - }, - "", - false, - }, - } - - for _, test := range layoutTests { - test := test - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - // setup mock chronograf.Service and mock logger - lg := &mocks.TestLogger{} - svc := server.Service{ - Store: &mocks.Store{LayoutsStore: &mocks.LayoutsStore{ - AllF: func(ctx context.Context) ([]chronograf.Layout, error) { - if len(test.allLayouts) == 0 { - return []chronograf.Layout{ - test.expected, - }, nil - } else { - return test.allLayouts, nil - } - }, - }, - }, - Logger: lg, - } - - // setup mock request and response - rr := httptest.NewRecorder() - reqURL := url.URL{ - Path: "/chronograf/v1/layouts", - } - params := reqURL.Query() - - // add query params required by test - if test.focusedApp != "" { - params.Add("app", test.focusedApp) - } - - // re-inject query params - reqURL.RawQuery = params.Encode() - - req := httptest.NewRequest("GET", reqURL.RequestURI(), strings.NewReader("")) - - // invoke handler for layouts endpoint - svc.Layouts(rr, req) - - // create a throwaway frame to unwrap Layouts - respFrame := struct { - Layouts []struct { - chronograf.Layout - Link interface{} `json:"-"` - } `json:"layouts"` - }{} - - // decode resp into respFrame - resp := rr.Result() - if err := json.NewDecoder(resp.Body).Decode(&respFrame); err != nil { - t.Fatalf("%q - Error unmarshalling JSON: err: %s", test.name, err.Error()) - } - - // compare actual and expected - if !cmp.Equal(test.expected, respFrame.Layouts[0].Layout) { - t.Fatalf("%q - Expected layouts to be equal: diff:\n\t%s", test.name, cmp.Diff(test.expected, respFrame.Layouts[0].Layout)) - } - }) - } -} diff --git a/chronograf/server/links.go b/chronograf/server/links.go deleted file mode 100644 index acfdfd7cf5..0000000000 --- a/chronograf/server/links.go +++ /dev/null @@ -1,59 +0,0 @@ -package server - -import ( - "errors" - "net/url" -) - -type getFluxLinksResponse struct { - AST string `json:"ast"` - Self string `json:"self"` - Suggestions string `json:"suggestions"` -} - -type getConfigLinksResponse struct { - Self string `json:"self"` // Location of the whole global application configuration - Auth string `json:"auth"` // Location of the auth section of the global application configuration -} - -type getOrganizationConfigLinksResponse struct { - Self string `json:"self"` // Location of the organization configuration - LogViewer string `json:"logViewer"` // Location of the organization-specific log viewer configuration -} - -type getExternalLinksResponse struct { - StatusFeed *string `json:"statusFeed,omitempty"` // Location of the a JSON Feed for client's Status page News Feed - CustomLinks []CustomLink `json:"custom,omitempty"` // Any custom external links for client's User menu -} - -// CustomLink is a handler that returns a custom link to be used in server's routes response, within ExternalLinks -type CustomLink struct { - Name string `json:"name"` - URL string `json:"url"` -} - -// NewCustomLinks transforms `--custom-link` CLI flag data or `CUSTOM_LINKS` ENV -// var data into a data structure that the Chronograf client will expect -func NewCustomLinks(links map[string]string) ([]CustomLink, error) { - customLinks := make([]CustomLink, 0, len(links)) - for name, link := range links { - if name == "" { - return nil, errors.New("customLink missing key for Name") - } - if link == "" { - return nil, errors.New("customLink missing value for URL") - } - _, err := url.Parse(link) - if err != nil { - return nil, err - } - - customLink := CustomLink{ - Name: name, - URL: link, - } - customLinks = append(customLinks, customLink) - } - - return customLinks, nil -} diff --git a/chronograf/server/links_test.go b/chronograf/server/links_test.go deleted file mode 100644 index 0ff6835fd6..0000000000 --- a/chronograf/server/links_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package server - -import ( - "reflect" - "testing" -) - -func TestNewCustomLinks(t *testing.T) { - tests := []struct { - name string - args map[string]string - want []CustomLink - wantErr bool - }{ - { - name: "Unknown error in NewCustomLinks", - args: map[string]string{ - "cubeapple": "https://cube.apple", - }, - want: []CustomLink{ - { - Name: "cubeapple", - URL: "https://cube.apple", - }, - }, - }, - { - name: "CustomLink missing Name", - args: map[string]string{ - "": "https://cube.apple", - }, - wantErr: true, - }, - { - name: "CustomLink missing URL", - args: map[string]string{ - "cubeapple": "", - }, - wantErr: true, - }, - { - name: "Missing protocol scheme", - args: map[string]string{ - "cubeapple": ":k%8a#", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - got, err := NewCustomLinks(tt.args) - if (err != nil) != tt.wantErr { - t.Errorf("%q. NewCustomLinks() error = %v, wantErr %v", tt.name, err, tt.wantErr) - continue - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("%q. NewCustomLinks() = %v, want %v", tt.name, got, tt.want) - } - } -} diff --git a/chronograf/server/logger.go b/chronograf/server/logger.go deleted file mode 100644 index cb88bf0606..0000000000 --- a/chronograf/server/logger.go +++ /dev/null @@ -1,63 +0,0 @@ -package server - -import ( - "net/http" - "time" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -// statusWriterFlusher captures the status header of an http.ResponseWriter -// and is a flusher -type statusWriter struct { - http.ResponseWriter - Flusher http.Flusher - status int -} - -func (w *statusWriter) WriteHeader(status int) { - w.status = status - w.ResponseWriter.WriteHeader(status) -} - -func (w *statusWriter) Status() int { return w.status } - -// Flush is here because the underlying HTTP chunked transfer response writer -// to implement http.Flusher. Without it data is silently buffered. This -// was discovered when proxying kapacitor chunked logs. -func (w *statusWriter) Flush() { - if w.Flusher != nil { - w.Flusher.Flush() - } -} - -// Logger is middleware that logs the request -func Logger(logger chronograf.Logger, next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - now := time.Now() - logger.WithField("component", "server"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL). - Debug("Request") - - sw := &statusWriter{ - ResponseWriter: w, - } - if f, ok := w.(http.Flusher); ok { - sw.Flusher = f - } - next.ServeHTTP(sw, r) - later := time.Now() - elapsed := later.Sub(now) - - logger. - WithField("component", "server"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("response_time", elapsed.String()). - WithField("status", sw.Status()). - Info("Response: ", http.StatusText(sw.Status())) - } - return http.HandlerFunc(fn) -} diff --git a/chronograf/server/logout.go b/chronograf/server/logout.go deleted file mode 100644 index dd7c2cabe4..0000000000 --- a/chronograf/server/logout.go +++ /dev/null @@ -1,24 +0,0 @@ -package server - -import ( - "net/http" - "path" -) - -// Logout chooses the correct provider logout route and redirects to it -func Logout(nextURL, basepath string, routes AuthRoutes) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - principal, err := getPrincipal(ctx) - if err != nil { - http.Redirect(w, r, path.Join(basepath, nextURL), http.StatusTemporaryRedirect) - return - } - route, ok := routes.Lookup(principal.Issuer) - if !ok { - http.Redirect(w, r, path.Join(basepath, nextURL), http.StatusTemporaryRedirect) - return - } - http.Redirect(w, r, route.Logout, http.StatusTemporaryRedirect) - } -} diff --git a/chronograf/server/mapping.go b/chronograf/server/mapping.go deleted file mode 100644 index 134e9f6a8f..0000000000 --- a/chronograf/server/mapping.go +++ /dev/null @@ -1,264 +0,0 @@ -package server - -import ( - "context" - - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" -) - -func (s *Service) mapPrincipalToSuperAdmin(p oauth2.Principal) bool { - if p.Issuer != "auth0" { - return false - } - - groups := strings.Split(p.Group, ",") - superAdmin := false - for _, group := range groups { - if group != "" && group == s.SuperAdminProviderGroups.auth0 { - superAdmin = true - break - } - } - return superAdmin -} - -func (s *Service) mapPrincipalToRoles(ctx context.Context, p oauth2.Principal) ([]chronograf.Role, error) { - mappings, err := s.Store.Mappings(ctx).All(ctx) - if err != nil { - return nil, err - } - roles := []chronograf.Role{} -MappingsLoop: - for _, mapping := range mappings { - if applyMapping(mapping, p) { - org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &mapping.Organization}) - if err != nil { - continue MappingsLoop - } - - for _, role := range roles { - if role.Organization == org.ID { - continue MappingsLoop - } - } - roles = append(roles, chronograf.Role{Organization: org.ID, Name: org.DefaultRole}) - } - } - - return roles, nil -} - -func applyMapping(m chronograf.Mapping, p oauth2.Principal) bool { - switch m.Provider { - case chronograf.MappingWildcard, p.Issuer: - default: - return false - } - - switch m.Scheme { - case chronograf.MappingWildcard, "oauth2": - default: - return false - } - - if m.ProviderOrganization == chronograf.MappingWildcard { - return true - } - - groups := strings.Split(p.Group, ",") - - return matchGroup(m.ProviderOrganization, groups) -} - -func matchGroup(match string, groups []string) bool { - for _, group := range groups { - if match == group { - return true - } - } - - return false -} - -type mappingsRequest chronograf.Mapping - -// Valid determines if a mapping request is valid -func (m *mappingsRequest) Valid() error { - if m.Provider == "" { - return fmt.Errorf("mapping must specify provider") - } - if m.Scheme == "" { - return fmt.Errorf("mapping must specify scheme") - } - if m.ProviderOrganization == "" { - return fmt.Errorf("mapping must specify group") - } - - return nil -} - -type mappingResponse struct { - Links selfLinks `json:"links"` - chronograf.Mapping -} - -func newMappingResponse(m chronograf.Mapping) *mappingResponse { - - return &mappingResponse{ - Links: selfLinks{ - Self: fmt.Sprintf("/chronograf/v1/mappings/%s", m.ID), - }, - Mapping: m, - } -} - -type mappingsResponse struct { - Links selfLinks `json:"links"` - Mappings []*mappingResponse `json:"mappings"` -} - -func newMappingsResponse(ms []chronograf.Mapping) *mappingsResponse { - mappings := []*mappingResponse{} - for _, m := range ms { - mappings = append(mappings, newMappingResponse(m)) - } - return &mappingsResponse{ - Links: selfLinks{ - Self: "/chronograf/v1/mappings", - }, - Mappings: mappings, - } -} - -// Mappings retrieves all mappings -func (s *Service) Mappings(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - mappings, err := s.Store.Mappings(ctx).All(ctx) - if err != nil { - Error(w, http.StatusInternalServerError, "failed to retrieve mappings from database", s.Logger) - return - } - - res := newMappingsResponse(mappings) - - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// NewMapping adds a new mapping -func (s *Service) NewMapping(w http.ResponseWriter, r *http.Request) { - var req mappingsRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - if err := req.Valid(); err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx := r.Context() - - // validate that the organization exists - if !s.organizationExists(ctx, req.Organization) { - invalidData(w, fmt.Errorf("organization does not exist"), s.Logger) - return - } - - mapping := &chronograf.Mapping{ - Organization: req.Organization, - Scheme: req.Scheme, - Provider: req.Provider, - ProviderOrganization: req.ProviderOrganization, - } - - m, err := s.Store.Mappings(ctx).Add(ctx, mapping) - if err != nil { - Error(w, http.StatusInternalServerError, "failed to add mapping to database", s.Logger) - return - } - - cu := newMappingResponse(*m) - location(w, cu.Links.Self) - encodeJSON(w, http.StatusCreated, cu, s.Logger) -} - -// UpdateMapping updates a mapping -func (s *Service) UpdateMapping(w http.ResponseWriter, r *http.Request) { - var req mappingsRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - if err := req.Valid(); err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx := r.Context() - - // validate that the organization exists - if !s.organizationExists(ctx, req.Organization) { - invalidData(w, fmt.Errorf("organization does not exist"), s.Logger) - return - } - - mapping := &chronograf.Mapping{ - ID: req.ID, - Organization: req.Organization, - Scheme: req.Scheme, - Provider: req.Provider, - ProviderOrganization: req.ProviderOrganization, - } - - err := s.Store.Mappings(ctx).Update(ctx, mapping) - if err != nil { - Error(w, http.StatusInternalServerError, "failed to update mapping in database", s.Logger) - return - } - - cu := newMappingResponse(*mapping) - location(w, cu.Links.Self) - encodeJSON(w, http.StatusOK, cu, s.Logger) -} - -// RemoveMapping removes a mapping -func (s *Service) RemoveMapping(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "id") - - m, err := s.Store.Mappings(ctx).Get(ctx, id) - if err == chronograf.ErrMappingNotFound { - Error(w, http.StatusNotFound, err.Error(), s.Logger) - return - } - - if err != nil { - Error(w, http.StatusInternalServerError, "failed to retrieve mapping from database", s.Logger) - return - } - - if err := s.Store.Mappings(ctx).Delete(ctx, m); err != nil { - Error(w, http.StatusInternalServerError, "failed to remove mapping from database", s.Logger) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (s *Service) organizationExists(ctx context.Context, orgID string) bool { - if _, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID}); err != nil { - return false - } - - return true -} diff --git a/chronograf/server/mapping_test.go b/chronograf/server/mapping_test.go deleted file mode 100644 index c2f026e193..0000000000 --- a/chronograf/server/mapping_test.go +++ /dev/null @@ -1,356 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -func TestMappings_All(t *testing.T) { - type fields struct { - MappingsStore chronograf.MappingsStore - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - wants wants - }{ - { - name: "get all mappings", - fields: fields{ - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/mappings"},"mappings":[{"links":{"self":"/chronograf/v1/mappings/"},"id":"","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}]}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - MappingsStore: tt.fields.MappingsStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - s.Mappings(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Mappings() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Mappings() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Mappings() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestMappings_Add(t *testing.T) { - type fields struct { - MappingsStore chronograf.MappingsStore - OrganizationsStore chronograf.OrganizationsStore - } - type args struct { - mapping *chronograf.Mapping - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "create new mapping", - fields: fields{ - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - }, - MappingsStore: &mocks.MappingsStore{ - AddF: func(ctx context.Context, m *chronograf.Mapping) (*chronograf.Mapping, error) { - m.ID = "0" - return m, nil - }, - }, - }, - args: args{ - mapping: &chronograf.Mapping{ - Organization: "0", - Provider: "*", - Scheme: "*", - ProviderOrganization: "*", - }, - }, - wants: wants{ - statusCode: 201, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/mappings/0"},"id":"0","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - MappingsStore: tt.fields.MappingsStore, - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - buf, _ := json.Marshal(tt.args.mapping) - r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - - s.NewMapping(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Add() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Add() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Add() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestMappings_Update(t *testing.T) { - type fields struct { - MappingsStore chronograf.MappingsStore - OrganizationsStore chronograf.OrganizationsStore - } - type args struct { - mapping *chronograf.Mapping - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "update new mapping", - fields: fields{ - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - }, - MappingsStore: &mocks.MappingsStore{ - UpdateF: func(ctx context.Context, m *chronograf.Mapping) error { - return nil - }, - }, - }, - args: args{ - mapping: &chronograf.Mapping{ - ID: "1", - Organization: "0", - Provider: "*", - Scheme: "*", - ProviderOrganization: "*", - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/mappings/1"},"id":"1","organizationId":"0","provider":"*","scheme":"*","providerOrganization":"*"}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - MappingsStore: tt.fields.MappingsStore, - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - buf, _ := json.Marshal(tt.args.mapping) - r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - r = r.WithContext(httprouter.WithParams( - context.Background(), - httprouter.Params{ - { - Key: "id", - Value: tt.args.mapping.ID, - }, - })) - - s.UpdateMapping(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Add() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Add() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Add() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestMappings_Remove(t *testing.T) { - type fields struct { - MappingsStore chronograf.MappingsStore - } - type args struct { - id string - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "remove mapping", - fields: fields{ - MappingsStore: &mocks.MappingsStore{ - GetF: func(ctx context.Context, id string) (*chronograf.Mapping, error) { - return &chronograf.Mapping{ - ID: "1", - Organization: "0", - Provider: "*", - Scheme: "*", - ProviderOrganization: "*", - }, nil - }, - DeleteF: func(ctx context.Context, m *chronograf.Mapping) error { - return nil - }, - }, - }, - args: args{}, - wants: wants{ - statusCode: 204, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - MappingsStore: tt.fields.MappingsStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - - r = r.WithContext(httprouter.WithParams( - context.Background(), - httprouter.Params{ - { - Key: "id", - Value: tt.args.id, - }, - })) - - s.RemoveMapping(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. Remove() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. Remove() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. Remove() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} diff --git a/chronograf/server/me.go b/chronograf/server/me.go deleted file mode 100644 index 6a1f6d6601..0000000000 --- a/chronograf/server/me.go +++ /dev/null @@ -1,400 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "sort" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/organizations" - "golang.org/x/net/context" -) - -type meLinks struct { - Self string `json:"self"` // Self link mapping to this resource -} - -type meResponse struct { - *chronograf.User - Links meLinks `json:"links"` - Organizations []chronograf.Organization `json:"organizations"` - CurrentOrganization *chronograf.Organization `json:"currentOrganization,omitempty"` -} - -type noAuthMeResponse struct { - Links meLinks `json:"links"` -} - -func newNoAuthMeResponse() noAuthMeResponse { - return noAuthMeResponse{ - Links: meLinks{ - Self: "/chronograf/v1/me", - }, - } -} - -// If new user response is nil, return an empty meResponse because it -// indicates authentication is not needed -func newMeResponse(usr *chronograf.User, org string) meResponse { - base := "/chronograf/v1" - name := "me" - if usr != nil { - base = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org) - name = PathEscape(fmt.Sprintf("%d", usr.ID)) - } - - return meResponse{ - User: usr, - Links: meLinks{ - Self: fmt.Sprintf("%s/%s", base, name), - }, - } -} - -// TODO: This Scheme value is hard-coded temporarily since we only currently -// support OAuth2. This hard-coding should be removed whenever we add -// support for other authentication schemes. -func getScheme(ctx context.Context) (string, error) { - return "oauth2", nil -} - -func getPrincipal(ctx context.Context) (oauth2.Principal, error) { - principal, ok := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal) - if !ok { - return oauth2.Principal{}, fmt.Errorf("token not found") - } - - return principal, nil -} - -func getValidPrincipal(ctx context.Context) (oauth2.Principal, error) { - p, err := getPrincipal(ctx) - if err != nil { - return p, err - } - if p.Subject == "" { - return oauth2.Principal{}, fmt.Errorf("token not found") - } - if p.Issuer == "" { - return oauth2.Principal{}, fmt.Errorf("token not found") - } - return p, nil -} - -type meRequest struct { - // Organization is the OrganizationID - Organization string `json:"organization"` -} - -// UpdateMe changes the user's current organization on the JWT and responds -// with the same semantics as Me -func (s *Service) UpdateMe(auth oauth2.Authenticator) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - serverCtx := serverContext(ctx) - principal, err := auth.Validate(ctx, r) - if err != nil { - s.Logger.Error(fmt.Sprintf("Invalid principal: %v", err)) - Error(w, http.StatusForbidden, "invalid principal", s.Logger) - return - } - var req meRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - // validate that the organization exists - org, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &req.Organization}) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - // validate that user belongs to organization - ctx = context.WithValue(ctx, organizations.ContextKey, req.Organization) - - p, err := getValidPrincipal(ctx) - if err != nil { - invalidData(w, err, s.Logger) - return - } - if p.Organization == "" { - defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - p.Organization = defaultOrg.ID - } - scheme, err := getScheme(ctx) - if err != nil { - invalidData(w, err, s.Logger) - return - } - _, err = s.Store.Users(ctx).Get(ctx, chronograf.UserQuery{ - Name: &p.Subject, - Provider: &p.Issuer, - Scheme: &scheme, - }) - if err == chronograf.ErrUserNotFound { - // If the user was not found, check to see if they are a super admin. If - // they are, add them to the organization. - u, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{ - Name: &p.Subject, - Provider: &p.Issuer, - Scheme: &scheme, - }) - if err != nil { - Error(w, http.StatusForbidden, err.Error(), s.Logger) - return - } - - if !u.SuperAdmin { - // Since a user is not a part of this organization and not a super admin, - // we should tell them that they are Forbidden (403) from accessing this resource - Error(w, http.StatusForbidden, chronograf.ErrUserNotFound.Error(), s.Logger) - return - } - - // If the user is a super admin give them an admin role in the - // requested organization. - u.Roles = append(u.Roles, chronograf.Role{ - Organization: org.ID, - Name: org.DefaultRole, - }) - if err := s.Store.Users(serverCtx).Update(serverCtx, u); err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - } else if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - // TODO: change to principal.CurrentOrganization - principal.Organization = req.Organization - - if err := auth.Authorize(ctx, w, principal); err != nil { - Error(w, http.StatusInternalServerError, err.Error(), s.Logger) - return - } - - ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal) - - s.Me(w, r.WithContext(ctx)) - } -} - -// Me does a findOrCreate based on the username in the context -func (s *Service) Me(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !s.UseAuth { - // If there's no authentication, return an empty user - res := newNoAuthMeResponse() - encodeJSON(w, http.StatusOK, res, s.Logger) - return - } - - p, err := getValidPrincipal(ctx) - if err != nil { - invalidData(w, err, s.Logger) - return - } - scheme, err := getScheme(ctx) - if err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx = context.WithValue(ctx, organizations.ContextKey, p.Organization) - serverCtx := serverContext(ctx) - - defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - if p.Organization == "" { - p.Organization = defaultOrg.ID - } - - usr, err := s.Store.Users(serverCtx).Get(serverCtx, chronograf.UserQuery{ - Name: &p.Subject, - Provider: &p.Issuer, - Scheme: &scheme, - }) - if err != nil && err != chronograf.ErrUserNotFound { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - // user exists - if usr != nil { - superAdmin := s.mapPrincipalToSuperAdmin(p) - if superAdmin && !usr.SuperAdmin { - usr.SuperAdmin = superAdmin - err := s.Store.Users(serverCtx).Update(serverCtx, usr) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - } - - currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization}) - if err == chronograf.ErrOrganizationNotFound { - // The intent is to force a the user to go through another auth flow - Error(w, http.StatusForbidden, "user's current organization was not found", s.Logger) - return - } - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - orgs, err := s.usersOrganizations(serverCtx, usr) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - res := newMeResponse(usr, currentOrg.ID) - res.Organizations = orgs - res.CurrentOrganization = currentOrg - encodeJSON(w, http.StatusOK, res, s.Logger) - return - } - - // Because we didnt find a user, making a new one - user := &chronograf.User{ - Name: p.Subject, - Provider: p.Issuer, - // TODO: This Scheme value is hard-coded temporarily since we only currently - // support OAuth2. This hard-coding should be removed whenever we add - // support for other authentication schemes. - Scheme: scheme, - // TODO(desa): this needs a better name - SuperAdmin: s.newUsersAreSuperAdmin(), - } - - superAdmin := s.mapPrincipalToSuperAdmin(p) - if superAdmin { - user.SuperAdmin = superAdmin - } - - roles, err := s.mapPrincipalToRoles(serverCtx, p) - if err != nil { - Error(w, http.StatusInternalServerError, err.Error(), s.Logger) - return - } - - if !superAdmin && len(roles) == 0 { - Error(w, http.StatusForbidden, "This Chronograf is private. To gain access, you must be explicitly added by an administrator.", s.Logger) - return - } - - // If the user is a superadmin, give them a role in the default organization - if user.SuperAdmin { - hasDefaultOrgRole := false - for _, role := range roles { - if role.Organization == defaultOrg.ID { - hasDefaultOrgRole = true - break - } - } - if !hasDefaultOrgRole { - roles = append(roles, chronograf.Role{ - Name: defaultOrg.DefaultRole, - Organization: defaultOrg.ID, - }) - } - } - - user.Roles = roles - - newUser, err := s.Store.Users(serverCtx).Add(serverCtx, user) - if err != nil { - msg := fmt.Errorf("error storing user %s: %v", user.Name, err) - unknownErrorWithMessage(w, msg, s.Logger) - return - } - - orgs, err := s.usersOrganizations(serverCtx, newUser) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - currentOrg, err := s.Store.Organizations(serverCtx).Get(serverCtx, chronograf.OrganizationQuery{ID: &p.Organization}) - if err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - res := newMeResponse(newUser, currentOrg.ID) - res.Organizations = orgs - res.CurrentOrganization = currentOrg - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -func (s *Service) firstUser() bool { - serverCtx := serverContext(context.Background()) - numUsers, err := s.Store.Users(serverCtx).Num(serverCtx) - if err != nil { - return false - } - - return numUsers == 0 -} -func (s *Service) newUsersAreSuperAdmin() bool { - // It's not necessary to enforce that the first user is superAdmin here, since - // superAdminNewUsers defaults to true, but there's nothing else in the - // application that dictates that it must be true. - // So for that reason, we kept this here for now. We've discussed the - // future possibility of allowing users to override default values via CLI and - // this case could possibly happen then. - if s.firstUser() { - return true - } - serverCtx := serverContext(context.Background()) - cfg, err := s.Store.Config(serverCtx).Get(serverCtx) - if err != nil { - return false - } - return cfg.Auth.SuperAdminNewUsers -} - -func (s *Service) usersOrganizations(ctx context.Context, u *chronograf.User) ([]chronograf.Organization, error) { - if u == nil { - // TODO(desa): better error - return nil, fmt.Errorf("user was nil") - } - - orgIDs := map[string]bool{} - for _, role := range u.Roles { - orgIDs[role.Organization] = true - } - - orgs := []chronograf.Organization{} - for orgID := range orgIDs { - org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &orgID}) - - // There can be race conditions between deleting a organization and the me query - if err == chronograf.ErrOrganizationNotFound { - continue - } - - // Any other error should cause an error to be returned - if err != nil { - return nil, err - } - orgs = append(orgs, *org) - } - - sort.Slice(orgs, func(i, j int) bool { - return orgs[i].ID < orgs[j].ID - }) - - return orgs, nil -} diff --git a/chronograf/server/me_test.go b/chronograf/server/me_test.go deleted file mode 100644 index 5008fc4422..0000000000 --- a/chronograf/server/me_test.go +++ /dev/null @@ -1,1455 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -func TestService_Me(t *testing.T) { - type fields struct { - UsersStore chronograf.UsersStore - OrganizationsStore chronograf.OrganizationsStore - MappingsStore chronograf.MappingsStore - ConfigStore chronograf.ConfigStore - SuperAdminProviderGroups superAdminProviderGroups - Logger chronograf.Logger - UseAuth bool - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - tests := []struct { - name string - fields fields - args args - principal oauth2.Principal - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Existing user - not member of any organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "0": - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - case "1": - return &chronograf.Organization{ - ID: "1", - Name: "The Bad Place", - }, nil - } - return nil, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"me","roles":null,"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer"}}`, - }, - { - name: "Existing superadmin - not member of any organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "0": - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - case "1": - return &chronograf.Organization{ - ID: "1", - Name: "The Bad Place", - }, nil - } - return nil, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - SuperAdmin: true, - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"me","roles":null,"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer"}}`, - }, - { - name: "Existing user - organization doesn't exist", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "0": - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.ViewerRoleName, - }, nil - } - return nil, chronograf.ErrOrganizationNotFound - }, - }, - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - Organization: "1", - }, - wantStatus: http.StatusForbidden, - wantContentType: "application/json", - wantBody: `{"code":403,"message":"user's current organization was not found"}`, - }, - { - name: "default mapping applies to new user", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: true, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - AllF: func(ctx context.Context) ([]chronograf.Organization, error) { - return []chronograf.Organization{ - { - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`, - }, - { - name: "New user - New users not super admin, not first user", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - AllF: func(ctx context.Context) ([]chronograf.Organization, error) { - return []chronograf.Organization{ - { - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`, - }, - { - name: "New user - New users not super admin, first user", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - AllF: func(ctx context.Context) ([]chronograf.Organization, error) { - return []chronograf.Organization{ - { - ID: "0", - Name: "The Gnarly Default", - DefaultRole: roles.ViewerRoleName, - }, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 0, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer"}}`, - }, - { - name: "Error adding user", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - }, nil - }, - AllF: func(ctx context.Context) ([]chronograf.Organization, error) { - return []chronograf.Organization{ - { - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.ViewerRoleName, - }, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return nil, fmt.Errorf("why Heavy?") - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "heroku", - }, - wantStatus: http.StatusForbidden, - wantContentType: "application/json", - wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`, - }, - { - name: "No Auth", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: false, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"links":{"self":"/chronograf/v1/me"}}`, - }, - { - name: "Empty Principal", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - ConfigStore: &mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - Logger: &chronograf.NoopLogger{}, - }, - wantStatus: http.StatusUnprocessableEntity, - principal: oauth2.Principal{ - Subject: "", - Issuer: "", - }, - }, - { - name: "new user - Chronograf is private", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - }, - wantStatus: http.StatusForbidden, - wantContentType: "application/json", - wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`, - }, - { - name: "new user - Chronograf is private, user is in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "not_example,example", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`, - }, - { - name: "new user - Chronograf is private, user is not in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{}, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "not_example", - }, - wantStatus: http.StatusForbidden, - wantContentType: "application/json", - wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`, - }, - { - name: "new user - Chronograf is not private, user is in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "example", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`, - }, - { - name: "new user - Chronograf is not private (has a fully open wildcard mapping to an org), user is not in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{ - Auth: chronograf.AuthConfig{ - SuperAdminNewUsers: false, - }, - }, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return nil, chronograf.ErrUserNotFound - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "not_example", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`, - }, - { - name: "Existing user - Chronograf is not private, user doesn't have SuperAdmin status, user is in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{}, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "secret", - Provider: "auth0", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "0", - }, - }, - }, nil - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "example", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`, - }, - { - name: "Existing user - Chronograf is not private, user has SuperAdmin status, user is in auth0 superadmin group", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - }, - fields: fields{ - UseAuth: true, - SuperAdminProviderGroups: superAdminProviderGroups{ - auth0: "example", - }, - Logger: &chronograf.NoopLogger{}, - ConfigStore: mocks.ConfigStore{ - Config: &chronograf.Config{}, - }, - MappingsStore: &mocks.MappingsStore{ - AllF: func(ctx context.Context) ([]chronograf.Mapping, error) { - return []chronograf.Mapping{ - { - Organization: "0", - Provider: chronograf.MappingWildcard, - Scheme: chronograf.MappingWildcard, - ProviderOrganization: chronograf.MappingWildcard, - }, - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "The Bad Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - UsersStore: &mocks.UsersStore{ - NumF: func(ctx context.Context) (int, error) { - // This function gets to verify that there is at least one first user - return 1, nil - }, - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "secret", - Provider: "auth0", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.MemberRoleName, - Organization: "0", - }, - }, - SuperAdmin: true, - }, nil - }, - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return u, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "secret", - Issuer: "auth0", - Group: "example", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"secret","roles":[{"name":"member","organization":"0"}],"provider":"auth0","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"The Bad Place","defaultRole":"member"}],"currentOrganization":{"id":"0","name":"The Bad Place","defaultRole":"member"}}`, - }, - } - for _, tt := range tests { - tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal)) - s := &Service{ - Store: &mocks.Store{ - UsersStore: tt.fields.UsersStore, - OrganizationsStore: tt.fields.OrganizationsStore, - MappingsStore: tt.fields.MappingsStore, - ConfigStore: tt.fields.ConfigStore, - }, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - SuperAdminProviderGroups: tt.fields.SuperAdminProviderGroups, - } - - s.Me(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. Me() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. Me() = %v, want %v", tt.name, content, tt.wantContentType) - } - if tt.wantBody == "" { - continue - } - if eq, err := jsonEqual(tt.wantBody, string(body)); err != nil || !eq { - t.Errorf("%q. Me() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - } -} - -func TestService_UpdateMe(t *testing.T) { - type fields struct { - UsersStore chronograf.UsersStore - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - UseAuth bool - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - meRequest *meRequest - auth mocks.Authenticator - } - tests := []struct { - name string - fields fields - args args - principal oauth2.Principal - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Set the current User's organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - meRequest: &meRequest{ - Organization: "1337", - }, - auth: mocks.Authenticator{}, - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.AdminRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - switch *q.ID { - case "0": - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.AdminRoleName, - }, nil - case "1337": - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - } - return nil, nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"1337","name":"The ShillBillThrilliettas"}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas"}}`, - }, - { - name: "Change the current User's organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - meRequest: &meRequest{ - Organization: "1337", - }, - auth: mocks.Authenticator{}, - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.EditorRoleName, - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - switch *q.ID { - case "1337": - return &chronograf.Organization{ - ID: "1337", - Name: "The ThrillShilliettos", - }, nil - case "0": - return &chronograf.Organization{ - ID: "0", - Name: "Default", - DefaultRole: roles.EditorRoleName, - }, nil - } - return nil, nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - Organization: "1338", - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/users/0"},"organizations":[{"id":"1337","name":"The ThrillShilliettos"}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos"}}`, - }, - { - name: "Unable to find requested user in valid organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - meRequest: &meRequest{ - Organization: "1337", - }, - auth: mocks.Authenticator{}, - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1338", - }, - }, - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - if q.ID == nil { - return nil, fmt.Errorf("invalid organization query: missing ID") - } - return &chronograf.Organization{ - ID: "1337", - Name: "The ShillBillThrilliettas", - }, nil - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - Organization: "1338", - }, - wantStatus: http.StatusForbidden, - wantContentType: "application/json", - wantBody: `{"code":403,"message":"user not found"}`, - }, - { - name: "Unable to find requested organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest("GET", "http://example.com/foo", nil), - meRequest: &meRequest{ - Organization: "1337", - }, - auth: mocks.Authenticator{}, - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) { - if q.Name == nil || q.Provider == nil || q.Scheme == nil { - return nil, fmt.Errorf("invalid user query: missing Name, Provider, and/or Scheme") - } - return &chronograf.User{ - Name: "me", - Provider: "github", - Scheme: "oauth2", - Roles: []chronograf.Role{ - { - Name: roles.AdminRoleName, - Organization: "1337", - }, - }, - }, nil - }, - UpdateF: func(ctx context.Context, u *chronograf.User) error { - return nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "0", - }, nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return nil, chronograf.ErrOrganizationNotFound - }, - }, - }, - principal: oauth2.Principal{ - Subject: "me", - Issuer: "github", - Organization: "1338", - }, - wantStatus: http.StatusBadRequest, - wantContentType: "application/json", - wantBody: `{"code":400,"message":"organization not found"}`, - }, - } - for _, tt := range tests { - tt.args.r = tt.args.r.WithContext(context.WithValue(context.Background(), oauth2.PrincipalKey, tt.principal)) - s := &Service{ - Store: &Store{ - UsersStore: tt.fields.UsersStore, - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - } - - buf, _ := json.Marshal(tt.args.meRequest) - tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - tt.args.auth.Principal = tt.principal - - s.UpdateMe(&tt.args.auth)(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. UpdateMe() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. UpdateMe() = %v, want %v", tt.name, content, tt.wantContentType) - } - if eq, err := jsonEqual(tt.wantBody, string(body)); err != nil || !eq { - t.Errorf("%q. UpdateMe() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - } -} diff --git a/chronograf/server/middle.go b/chronograf/server/middle.go deleted file mode 100644 index b6ba8afc2e..0000000000 --- a/chronograf/server/middle.go +++ /dev/null @@ -1,57 +0,0 @@ -package server - -import ( - "net/http" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" -) - -// RouteMatchesPrincipal checks that the organization on context matches the organization -// in the route. -func RouteMatchesPrincipal( - store DataStore, - useAuth bool, - logger chronograf.Logger, - next http.HandlerFunc, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !useAuth { - next(w, r) - return - } - - log := logger. - WithField("component", "org_match"). - WithField("remote_addr", r.RemoteAddr). - WithField("method", r.Method). - WithField("url", r.URL) - - orgID := httprouter.GetParamFromContext(ctx, "oid") - p, err := getValidPrincipal(ctx) - if err != nil { - log.Error("Failed to retrieve principal from context") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - if p.Organization == "" { - defaultOrg, err := store.Organizations(ctx).DefaultOrganization(ctx) - if err != nil { - log.Error("Failed to look up default organization") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - p.Organization = defaultOrg.ID - } - - if orgID != p.Organization { - log.Error("Route organization does not match the organization on principal") - Error(w, http.StatusForbidden, "User is not authorized", logger) - return - } - - next(w, r) - } -} diff --git a/chronograf/server/middle_test.go b/chronograf/server/middle_test.go deleted file mode 100644 index 12dbb00712..0000000000 --- a/chronograf/server/middle_test.go +++ /dev/null @@ -1,195 +0,0 @@ -package server - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" -) - -func TestRouteMatchesPrincipal(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - useAuth bool - principal *oauth2.Principal - routerParams *httprouter.Params - } - type wants struct { - matches bool - } - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "route matches request params", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "default", - }, nil - }, - }, - }, - args: args{ - useAuth: true, - principal: &oauth2.Principal{ - Subject: "user", - Issuer: "github", - Organization: "default", - }, - routerParams: &httprouter.Params{ - { - Key: "oid", - Value: "default", - }, - }, - }, - wants: wants{ - matches: true, - }, - }, - { - name: "route does not match request params", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "default", - }, nil - }, - }, - }, - args: args{ - useAuth: true, - principal: &oauth2.Principal{ - Subject: "user", - Issuer: "github", - Organization: "default", - }, - routerParams: &httprouter.Params{ - { - Key: "oid", - Value: "other", - }, - }, - }, - wants: wants{ - matches: false, - }, - }, - { - name: "missing principal", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "default", - }, nil - }, - }, - }, - args: args{ - useAuth: true, - principal: nil, - routerParams: &httprouter.Params{ - { - Key: "oid", - Value: "other", - }, - }, - }, - wants: wants{ - matches: false, - }, - }, - { - name: "not using auth", - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "default", - }, nil - }, - }, - }, - args: args{ - useAuth: false, - principal: &oauth2.Principal{ - Subject: "user", - Issuer: "github", - Organization: "default", - }, - routerParams: &httprouter.Params{ - { - Key: "oid", - Value: "other", - }, - }, - }, - wants: wants{ - matches: true, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - store := &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - } - var matches bool - next := func(w http.ResponseWriter, r *http.Request) { - matches = true - } - fn := RouteMatchesPrincipal( - store, - tt.args.useAuth, - tt.fields.Logger, - next, - ) - - w := httptest.NewRecorder() - url := "http://any.url" - r := httptest.NewRequest( - "GET", - url, - nil, - ) - if tt.args.routerParams != nil { - r = r.WithContext(httprouter.WithParams(r.Context(), *tt.args.routerParams)) - } - if tt.args.principal == nil { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil)) - } else { - r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal)) - } - fn(w, r) - - if matches != tt.wants.matches { - t.Errorf("%q. RouteMatchesPrincipal() = %v, expected %v", tt.name, matches, tt.wants.matches) - } - - if !matches && w.Code != http.StatusForbidden { - t.Errorf("%q. RouteMatchesPrincipal() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden) - } - - }) - } -} diff --git a/chronograf/server/mountable_router.go b/chronograf/server/mountable_router.go deleted file mode 100644 index 1ae275cfdf..0000000000 --- a/chronograf/server/mountable_router.go +++ /dev/null @@ -1,59 +0,0 @@ -package server - -import ( - "net/http" - libpath "path" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -var _ chronograf.Router = &MountableRouter{} - -// MountableRouter is an implementation of a chronograf.Router which supports -// prefixing each route of a Delegated chronograf.Router with a prefix. -type MountableRouter struct { - Prefix string - Delegate chronograf.Router -} - -// DELETE defines a route responding to a DELETE request that will be prefixed -// with the configured route prefix -func (mr *MountableRouter) DELETE(path string, handler http.HandlerFunc) { - mr.Delegate.DELETE(libpath.Join(mr.Prefix, path), handler) -} - -// GET defines a route responding to a GET request that will be prefixed -// with the configured route prefix -func (mr *MountableRouter) GET(path string, handler http.HandlerFunc) { - mr.Delegate.GET(libpath.Join(mr.Prefix, path), handler) -} - -// POST defines a route responding to a POST request that will be prefixed -// with the configured route prefix -func (mr *MountableRouter) POST(path string, handler http.HandlerFunc) { - mr.Delegate.POST(libpath.Join(mr.Prefix, path), handler) -} - -// PUT defines a route responding to a PUT request that will be prefixed -// with the configured route prefix -func (mr *MountableRouter) PUT(path string, handler http.HandlerFunc) { - mr.Delegate.PUT(libpath.Join(mr.Prefix, path), handler) -} - -// PATCH defines a route responding to a PATCH request that will be prefixed -// with the configured route prefix -func (mr *MountableRouter) PATCH(path string, handler http.HandlerFunc) { - mr.Delegate.PATCH(libpath.Join(mr.Prefix, path), handler) -} - -// Handler defines a prefixed route responding to a request type specified in -// the method parameter -func (mr *MountableRouter) Handler(method string, path string, handler http.Handler) { - mr.Delegate.Handler(method, libpath.Join(mr.Prefix, path), handler) -} - -// ServeHTTP is an implementation of http.Handler which delegates to the -// configured Delegate's implementation of http.Handler -func (mr *MountableRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - mr.Delegate.ServeHTTP(rw, r) -} diff --git a/chronograf/server/mountable_router_test.go b/chronograf/server/mountable_router_test.go deleted file mode 100644 index 2eec8a0593..0000000000 --- a/chronograf/server/mountable_router_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package server_test - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf/server" -) - -func Test_MountableRouter_MountsRoutesUnderPrefix(t *testing.T) { - t.Parallel() - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - expected := "Hello?! McFly?! Anybody in there?!" - mr.GET("/biff", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - fmt.Fprint(rw, expected) - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - resp, err := http.Get(ts.URL + "/chronograf/biff") - if err != nil { - t.Fatal("Unexpected error fetching from mounted router: err:", err) - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Unexpected error decoding response body: err:", err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatal("Expected 200 but received", resp.StatusCode) - } - - if string(body) != expected { - t.Fatalf("Unexpected response body: Want: \"%s\". Got: \"%s\"", expected, string(body)) - } -} - -func Test_MountableRouter_PrefixesPosts(t *testing.T) { - t.Parallel() - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - expected := "Great Scott!" - actual := make([]byte, len(expected)) - mr.POST("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - if _, err := io.ReadFull(r.Body, actual); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - } else { - rw.WriteHeader(http.StatusOK) - } - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - resp, err := http.Post(ts.URL+"/chronograf/doc", "text/plain", strings.NewReader(expected)) - if err != nil { - t.Fatal("Unexpected error posting to mounted router: err:", err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatal("Expected 200 but received", resp.StatusCode) - } - - if string(actual) != expected { - t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) - } -} - -func Test_MountableRouter_PrefixesPuts(t *testing.T) { - t.Parallel() - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - expected := "Great Scott!" - actual := make([]byte, len(expected)) - mr.PUT("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - if _, err := io.ReadFull(r.Body, actual); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - } else { - rw.WriteHeader(http.StatusOK) - } - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - req := httptest.NewRequest(http.MethodPut, ts.URL+"/chronograf/doc", strings.NewReader(expected)) - req.Header.Set("Content-Type", "text/plain; charset=utf-8") - req.Header.Set("Content-Length", fmt.Sprintf("%d", len(expected))) - req.RequestURI = "" - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatal("Unexpected error posting to mounted router: err:", err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatal("Expected 200 but received", resp.StatusCode) - } - - if string(actual) != expected { - t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) - } -} - -func Test_MountableRouter_PrefixesDeletes(t *testing.T) { - t.Parallel() - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - mr.DELETE("/proto1985", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(http.StatusNoContent) - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - req := httptest.NewRequest(http.MethodDelete, ts.URL+"/chronograf/proto1985", nil) - req.RequestURI = "" - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatal("Unexpected error sending request to mounted router: err:", err) - } - - if resp.StatusCode != http.StatusNoContent { - t.Fatal("Expected 204 but received", resp.StatusCode) - } -} - -func Test_MountableRouter_PrefixesPatches(t *testing.T) { - t.Parallel() - - type Character struct { - Name string - Items []string - } - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - biff := Character{"biff", []string{"sports almanac"}} - mr.PATCH("/1955", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - c := Character{} - err := json.NewDecoder(r.Body).Decode(&c) - if err != nil { - rw.WriteHeader(http.StatusBadRequest) - } else { - biff.Items = c.Items - rw.WriteHeader(http.StatusOK) - } - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - r, w := io.Pipe() - go func() { - _ = json.NewEncoder(w).Encode(Character{"biff", []string{}}) - w.Close() - }() - - req := httptest.NewRequest(http.MethodPatch, ts.URL+"/chronograf/1955", r) - req.RequestURI = "" - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatal("Unexpected error sending request to mounted router: err:", err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatal("Expected 200 but received", resp.StatusCode) - } - - if len(biff.Items) != 0 { - t.Fatal("Failed to alter history, biff still has the sports almanac") - } -} - -func Test_MountableRouter_PrefixesHandler(t *testing.T) { - t.Parallel() - - mr := &server.MountableRouter{ - Prefix: "/chronograf", - Delegate: httprouter.New(), - } - - mr.Handler(http.MethodGet, "/recklessAmountOfPower", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(http.StatusOK) - rw.Write([]byte("1.21 Gigawatts!")) - })) - - ts := httptest.NewServer(mr) - defer ts.Close() - - req := httptest.NewRequest(http.MethodGet, ts.URL+"/chronograf/recklessAmountOfPower", nil) - req.RequestURI = "" - - client := http.Client{} - resp, err := client.Do(req) - if err != nil { - t.Fatal("Unexpected error sending request to mounted router: err:", err) - } - - if resp.StatusCode != http.StatusOK { - t.Fatal("Expected 200 but received", resp.StatusCode) - } -} diff --git a/chronograf/server/mux.go b/chronograf/server/mux.go deleted file mode 100644 index 7671291bc7..0000000000 --- a/chronograf/server/mux.go +++ /dev/null @@ -1,399 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "path" - "strconv" - "strings" - - "github.com/NYTimes/gziphandler" - "github.com/bouk/httprouter" - jhttprouter "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/oauth2" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -const ( - // JSONType the mimetype for a json request - JSONType = "application/json" -) - -// MuxOpts are the options for the router. Mostly related to auth. -type MuxOpts struct { - Logger chronograf.Logger - Develop bool // Develop loads assets from filesystem instead of bindata - Basepath string // URL path prefix under which all chronograf routes will be mounted - UseAuth bool // UseAuth turns on Github OAuth and JWT - Auth oauth2.Authenticator // Auth is used to authenticate and authorize - ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux)) - StatusFeedURL string // JSON Feed URL for the client Status page News Feed - CustomLinks map[string]string // Any custom external links for client's User menu -} - -// NewMux attaches all the route handlers; handler returned servers chronograf. -func NewMux(opts MuxOpts, service Service) http.Handler { - hr := httprouter.New() - - /* React Application */ - assets := Assets(AssetsOpts{ - Develop: opts.Develop, - Logger: opts.Logger, - }) - - // Prefix any URLs found in the React assets with any configured basepath - prefixedAssets := NewDefaultURLPrefixer(opts.Basepath, assets, opts.Logger) - - // Compress the assets with gzip if an accepted encoding - compressed := gziphandler.GzipHandler(prefixedAssets) - - // The react application handles all the routing if the server does not - // know about the route. This means that we never have unknown routes on - // the server. - hr.NotFound = compressed - - var router chronograf.Router = hr - - // Set route prefix for all routes if basepath is present - if opts.Basepath != "" { - router = &MountableRouter{ - Prefix: opts.Basepath, - Delegate: hr, - } - - //The assets handler is always unaware of basepaths, so the - // basepath needs to always be removed before sending requests to it - hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound) - } - - EnsureMember := func(next http.HandlerFunc) http.HandlerFunc { - return AuthorizedUser( - service.Store, - opts.UseAuth, - roles.MemberRoleName, - opts.Logger, - next, - ) - } - _ = EnsureMember - EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc { - return AuthorizedUser( - service.Store, - opts.UseAuth, - roles.ViewerRoleName, - opts.Logger, - next, - ) - } - EnsureEditor := func(next http.HandlerFunc) http.HandlerFunc { - return AuthorizedUser( - service.Store, - opts.UseAuth, - roles.EditorRoleName, - opts.Logger, - next, - ) - } - EnsureAdmin := func(next http.HandlerFunc) http.HandlerFunc { - return AuthorizedUser( - service.Store, - opts.UseAuth, - roles.AdminRoleName, - opts.Logger, - next, - ) - } - EnsureSuperAdmin := func(next http.HandlerFunc) http.HandlerFunc { - return AuthorizedUser( - service.Store, - opts.UseAuth, - roles.SuperAdminStatus, - opts.Logger, - next, - ) - } - - rawStoreAccess := func(next http.HandlerFunc) http.HandlerFunc { - return RawStoreAccess(opts.Logger, next) - } - - ensureOrgMatches := func(next http.HandlerFunc) http.HandlerFunc { - return RouteMatchesPrincipal( - service.Store, - opts.UseAuth, - opts.Logger, - next, - ) - } - - /* Documentation */ - router.GET("/swagger.json", Spec()) - router.GET("/docs", Redoc("/swagger.json")) - - /* API */ - // Organizations - router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations)) - router.POST("/chronograf/v1/organizations", EnsureSuperAdmin(service.NewOrganization)) - - router.GET("/chronograf/v1/organizations/:oid", EnsureAdmin(service.OrganizationID)) - router.PATCH("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.UpdateOrganization)) - router.DELETE("/chronograf/v1/organizations/:oid", EnsureSuperAdmin(service.RemoveOrganization)) - - // Mappings - router.GET("/chronograf/v1/mappings", EnsureSuperAdmin(service.Mappings)) - router.POST("/chronograf/v1/mappings", EnsureSuperAdmin(service.NewMapping)) - - router.PUT("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.UpdateMapping)) - router.DELETE("/chronograf/v1/mappings/:id", EnsureSuperAdmin(service.RemoveMapping)) - - // Source Proxy to Influx; Has gzip compression around the handler - influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx))) - router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx) - - // Write proxies line protocol write requests to InfluxDB - router.POST("/chronograf/v1/sources/:id/write", EnsureViewer(service.Write)) - - // Queries is used to analyze a specific queries and does not create any - // resources. It's a POST because Queries are POSTed to InfluxDB, but this - // only modifies InfluxDB resources with certain metaqueries, e.g. DROP DATABASE. - // - // Admins should ensure that the InfluxDB source as the proper permissions - // intended for Chronograf Users with the Viewer Role type. - router.POST("/chronograf/v1/sources/:id/queries", EnsureViewer(service.Queries)) - - // Annotations are user-defined events associated with this source - router.GET("/chronograf/v1/sources/:id/annotations", EnsureViewer(service.Annotations)) - router.POST("/chronograf/v1/sources/:id/annotations", EnsureEditor(service.NewAnnotation)) - router.GET("/chronograf/v1/sources/:id/annotations/:aid", EnsureViewer(service.Annotation)) - router.DELETE("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.RemoveAnnotation)) - router.PATCH("/chronograf/v1/sources/:id/annotations/:aid", EnsureEditor(service.UpdateAnnotation)) - - // All possible permissions for users in this source - router.GET("/chronograf/v1/sources/:id/permissions", EnsureViewer(service.Permissions)) - - // Services are resources that chronograf proxies to - router.GET("/chronograf/v1/sources/:id/services", EnsureViewer(service.Services)) - router.POST("/chronograf/v1/sources/:id/services", EnsureEditor(service.NewService)) - router.GET("/chronograf/v1/sources/:id/services/:kid", EnsureViewer(service.ServiceID)) - router.PATCH("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.UpdateService)) - router.DELETE("/chronograf/v1/sources/:id/services/:kid", EnsureEditor(service.RemoveService)) - - // Service Proxy - router.GET("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureViewer(service.ProxyGet)) - router.POST("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPost)) - router.PATCH("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyPatch)) - router.DELETE("/chronograf/v1/sources/:id/services/:kid/proxy", EnsureEditor(service.ProxyDelete)) - - // Layouts - router.GET("/chronograf/v1/layouts", EnsureViewer(service.Layouts)) - router.GET("/chronograf/v1/layouts/:id", EnsureViewer(service.LayoutsID)) - - // Users associated with Chronograf - router.GET("/chronograf/v1/me", service.Me) - - // Set current chronograf organization the user is logged into - router.PUT("/chronograf/v1/me", service.UpdateMe(opts.Auth)) - - // TODO(desa): what to do about admin's being able to set superadmin - router.GET("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.Users))) - router.POST("/chronograf/v1/organizations/:oid/users", EnsureAdmin(ensureOrgMatches(service.NewUser))) - - router.GET("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UserID))) - router.DELETE("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.RemoveUser))) - router.PATCH("/chronograf/v1/organizations/:oid/users/:id", EnsureAdmin(ensureOrgMatches(service.UpdateUser))) - - router.GET("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.Users))) - router.POST("/chronograf/v1/users", EnsureSuperAdmin(rawStoreAccess(service.NewUser))) - - router.GET("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UserID))) - router.DELETE("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.RemoveUser))) - router.PATCH("/chronograf/v1/users/:id", EnsureSuperAdmin(rawStoreAccess(service.UpdateUser))) - - // Dashboards - router.GET("/chronograf/v1/dashboards", EnsureViewer(service.Dashboards)) - router.POST("/chronograf/v1/dashboards", EnsureEditor(service.NewDashboard)) - - router.GET("/chronograf/v1/dashboards/:id", EnsureViewer(service.DashboardID)) - router.DELETE("/chronograf/v1/dashboards/:id", EnsureEditor(service.RemoveDashboard)) - router.PUT("/chronograf/v1/dashboards/:id", EnsureEditor(service.ReplaceDashboard)) - router.PATCH("/chronograf/v1/dashboards/:id", EnsureEditor(service.UpdateDashboard)) - // Dashboard Cells - router.GET("/chronograf/v1/dashboards/:id/cells", EnsureViewer(service.DashboardCells)) - router.POST("/chronograf/v1/dashboards/:id/cells", EnsureEditor(service.NewDashboardCell)) - - router.GET("/chronograf/v1/dashboards/:id/cells/:cid", EnsureViewer(service.DashboardCellID)) - router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.RemoveDashboardCell)) - router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.ReplaceDashboardCell)) - // Dashboard Templates - router.GET("/chronograf/v1/dashboards/:id/templates", EnsureViewer(service.Templates)) - router.POST("/chronograf/v1/dashboards/:id/templates", EnsureEditor(service.NewTemplate)) - - router.GET("/chronograf/v1/dashboards/:id/templates/:tid", EnsureViewer(service.TemplateID)) - router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.RemoveTemplate)) - router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", EnsureEditor(service.ReplaceTemplate)) - - // Databases - router.GET("/chronograf/v1/sources/:id/dbs", EnsureViewer(service.GetDatabases)) - router.POST("/chronograf/v1/sources/:id/dbs", EnsureEditor(service.NewDatabase)) - - router.DELETE("/chronograf/v1/sources/:id/dbs/:db", EnsureEditor(service.DropDatabase)) - - // Retention Policies - router.GET("/chronograf/v1/sources/:id/dbs/:db/rps", EnsureViewer(service.RetentionPolicies)) - router.POST("/chronograf/v1/sources/:id/dbs/:db/rps", EnsureEditor(service.NewRetentionPolicy)) - - router.PUT("/chronograf/v1/sources/:id/dbs/:db/rps/:rp", EnsureEditor(service.UpdateRetentionPolicy)) - router.DELETE("/chronograf/v1/sources/:id/dbs/:db/rps/:rp", EnsureEditor(service.DropRetentionPolicy)) - - // Measurements - router.GET("/chronograf/v1/sources/:id/dbs/:db/measurements", EnsureViewer(service.Measurements)) - - // Global application config for Chronograf - router.GET("/chronograf/v1/config", EnsureSuperAdmin(service.Config)) - router.GET("/chronograf/v1/config/auth", EnsureSuperAdmin(service.AuthConfig)) - router.PUT("/chronograf/v1/config/auth", EnsureSuperAdmin(service.ReplaceAuthConfig)) - - // Organization config settings for Chronograf - router.GET("/chronograf/v1/org_config", EnsureViewer(service.OrganizationConfig)) - router.GET("/chronograf/v1/org_config/logviewer", EnsureViewer(service.OrganizationLogViewerConfig)) - router.PUT("/chronograf/v1/org_config/logviewer", EnsureEditor(service.ReplaceOrganizationLogViewerConfig)) - - router.GET("/chronograf/v1/env", EnsureViewer(service.Environment)) - - allRoutes := &AllRoutes{ - Logger: opts.Logger, - StatusFeed: opts.StatusFeedURL, - CustomLinks: opts.CustomLinks, - } - - getPrincipal := func(r *http.Request) oauth2.Principal { - p, _ := HasAuthorizedToken(opts.Auth, r) - return p - } - allRoutes.GetPrincipal = getPrincipal - router.Handler("GET", "/chronograf/v1/", allRoutes) - - var out http.Handler - - /* Authentication */ - if opts.UseAuth { - // Encapsulate the router with OAuth2 - var auth http.Handler - auth, allRoutes.AuthRoutes = AuthAPI(opts, router) - allRoutes.LogoutLink = path.Join(opts.Basepath, "/oauth/logout") - - // Create middleware that redirects to the appropriate provider logout - router.GET("/oauth/logout", Logout("/", opts.Basepath, allRoutes.AuthRoutes)) - out = Logger(opts.Logger, FlushingHandler(auth)) - } else { - out = Logger(opts.Logger, FlushingHandler(router)) - } - - return out -} - -// AuthAPI adds the OAuth routes if auth is enabled. -func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes) { - routes := AuthRoutes{} - for _, pf := range opts.ProviderFuncs { - pf(func(p oauth2.Provider, m oauth2.Mux) { - urlName := PathEscape(strings.ToLower(p.Name())) - - loginPath := path.Join("/oauth", urlName, "login") - logoutPath := path.Join("/oauth", urlName, "logout") - callbackPath := path.Join("/oauth", urlName, "callback") - - router.Handler("GET", loginPath, m.Login()) - router.Handler("GET", logoutPath, m.Logout()) - router.Handler("GET", callbackPath, m.Callback()) - routes = append(routes, AuthRoute{ - Name: p.Name(), - Label: strings.Title(p.Name()), - // AuthRoutes are content served to the page. When Basepath is set, it - // says that all content served to the page will be prefixed with the - // basepath. Since these routes are consumed by JS, it will need the - // basepath set to traverse a proxy correctly - Login: path.Join(opts.Basepath, loginPath), - Logout: path.Join(opts.Basepath, logoutPath), - Callback: path.Join(opts.Basepath, callbackPath), - }) - }) - } - - rootPath := path.Join(opts.Basepath, "/chronograf/v1") - logoutPath := path.Join(opts.Basepath, "/oauth/logout") - - tokenMiddleware := AuthorizedToken(opts.Auth, opts.Logger, router) - // Wrap the API with token validation middleware. - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cleanPath := path.Clean(r.URL.Path) // compare ignoring path garbage, trailing slashes, etc. - if (strings.HasPrefix(cleanPath, rootPath) && len(cleanPath) > len(rootPath)) || cleanPath == logoutPath { - tokenMiddleware.ServeHTTP(w, r) - return - } - router.ServeHTTP(w, r) - }), routes -} - -func encodeJSON(w http.ResponseWriter, status int, v interface{}, logger chronograf.Logger) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - if err := json.NewEncoder(w).Encode(v); err != nil { - unknownErrorWithMessage(w, err, logger) - } -} - -// Error writes an JSON message -func Error(w http.ResponseWriter, code int, msg string, logger chronograf.Logger) { - e := ErrorMessage{ - Code: code, - Message: msg, - } - b, err := json.Marshal(e) - if err != nil { - code = http.StatusInternalServerError - b = []byte(`{"code": 500, "message":"server_error"}`) - } - - logger. - WithField("component", "server"). - WithField("http_status ", code). - Error("Error message ", msg) - w.Header().Set("Content-Type", JSONType) - w.WriteHeader(code) - _, _ = w.Write(b) -} - -func invalidData(w http.ResponseWriter, err error, logger chronograf.Logger) { - Error(w, http.StatusUnprocessableEntity, fmt.Sprintf("%v", err), logger) -} - -func invalidJSON(w http.ResponseWriter, logger chronograf.Logger) { - Error(w, http.StatusBadRequest, "unparsable JSON", logger) -} - -func unknownErrorWithMessage(w http.ResponseWriter, err error, logger chronograf.Logger) { - Error(w, http.StatusInternalServerError, fmt.Sprintf("unknown error: %v", err), logger) -} - -func notFound(w http.ResponseWriter, id interface{}, logger chronograf.Logger) { - Error(w, http.StatusNotFound, fmt.Sprintf("ID %v not found", id), logger) -} - -func paramID(key string, r *http.Request) (int, error) { - ctx := r.Context() - param := jhttprouter.ParamsFromContext(ctx).ByName(key) - id, err := strconv.Atoi(param) - if err != nil { - return -1, fmt.Errorf("error converting ID %s", param) - } - return id, nil -} - -func paramStr(key string, r *http.Request) (string, error) { - ctx := r.Context() - param := jhttprouter.ParamsFromContext(ctx).ByName(key) - return param, nil -} diff --git a/chronograf/server/org_config.go b/chronograf/server/org_config.go deleted file mode 100644 index 1ea4394390..0000000000 --- a/chronograf/server/org_config.go +++ /dev/null @@ -1,180 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -type organizationConfigLinks struct { - Self string `json:"self"` // Self link mapping to this resource - LogViewer string `json:"logViewer"` // LogViewer link to the organization log viewer config endpoint -} - -type organizationConfigResponse struct { - Links organizationConfigLinks `json:"links"` - chronograf.OrganizationConfig -} - -func newOrganizationConfigResponse(c chronograf.OrganizationConfig) *organizationConfigResponse { - return &organizationConfigResponse{ - Links: organizationConfigLinks{ - Self: "/chronograf/v1/org_config", - LogViewer: "/chronograf/v1/org_config/logviewer", - }, - OrganizationConfig: c, - } -} - -type logViewerConfigResponse struct { - Links selfLinks `json:"links"` - chronograf.LogViewerConfig -} - -func newLogViewerConfigResponse(c chronograf.LogViewerConfig) *logViewerConfigResponse { - return &logViewerConfigResponse{ - Links: selfLinks{ - Self: "/chronograf/v1/org_config/logviewer", - }, - LogViewerConfig: c, - } -} - -// OrganizationConfig retrieves the organization-wide config settings -func (s *Service) OrganizationConfig(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - orgID, ok := hasOrganizationContext(ctx) - if !ok { - Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger) - return - } - - config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newOrganizationConfigResponse(*config) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// OrganizationLogViewerConfig retrieves the log viewer UI section of the organization config -// This uses a FindOrCreate function to ensure that any new organizations have -// default organization config values, without having to associate organization creation with -// organization config creation. -func (s *Service) OrganizationLogViewerConfig(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - orgID, ok := hasOrganizationContext(ctx) - if !ok { - Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger) - return - } - - config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newLogViewerConfigResponse(config.LogViewer) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// ReplaceOrganizationLogViewerConfig replaces the log viewer UI section of the organization config -func (s *Service) ReplaceOrganizationLogViewerConfig(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - orgID, ok := hasOrganizationContext(ctx) - if !ok { - Error(w, http.StatusBadRequest, "Organization not found on context", s.Logger) - return - } - - var logViewerConfig chronograf.LogViewerConfig - if err := json.NewDecoder(r.Body).Decode(&logViewerConfig); err != nil { - invalidJSON(w, s.Logger) - return - } - if err := validLogViewerConfig(logViewerConfig); err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - config, err := s.Store.OrganizationConfig(ctx).FindOrCreate(ctx, orgID) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - config.LogViewer = logViewerConfig - if err := s.Store.OrganizationConfig(ctx).Put(ctx, config); err != nil { - unknownErrorWithMessage(w, err, s.Logger) - return - } - - res := newLogViewerConfigResponse(config.LogViewer) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// validLogViewerConfig ensures that the request body log viewer UI config is valid -// to be valid, it must: not be empty, have at least one column, not have multiple -// columns with the same name or position value, each column must have a visibility -// of either "visible" or "hidden" and if a column is of type severity, it must have -// at least one severity format of type icon, text, or both -func validLogViewerConfig(c chronograf.LogViewerConfig) error { - if len(c.Columns) == 0 { - return fmt.Errorf("invalid log viewer config: must have at least 1 column") - } - - nameMatcher := map[string]bool{} - positionMatcher := map[int32]bool{} - - for _, clm := range c.Columns { - iconCount := 0 - textCount := 0 - visibility := 0 - - // check that each column has a unique value for the name and position properties - if _, ok := nameMatcher[clm.Name]; ok { - return fmt.Errorf("invalid log viewer config: Duplicate column name %s", clm.Name) - } - nameMatcher[clm.Name] = true - if _, ok := positionMatcher[clm.Position]; ok { - return fmt.Errorf("invalid log viewer config: Multiple columns with same position value") - } - positionMatcher[clm.Position] = true - - for _, e := range clm.Encodings { - if e.Type == "visibility" { - visibility++ - if !(e.Value == "visible" || e.Value == "hidden") { - return fmt.Errorf("invalid log viewer config: invalid visibility in column %s", clm.Name) - } - } - - if clm.Name == "severity" { - if e.Value == "icon" { - iconCount++ - } else if e.Value == "text" { - textCount++ - } - } - } - - if visibility != 1 { - return fmt.Errorf("invalid log viewer config: missing visibility encoding in column %s", clm.Name) - } - - if clm.Name == "severity" { - if iconCount+textCount == 0 || iconCount > 1 || textCount > 1 { - return fmt.Errorf("invalid log viewer config: invalid number of severity format encodings in column %s", clm.Name) - } - } - } - - return nil -} diff --git a/chronograf/server/org_config_test.go b/chronograf/server/org_config_test.go deleted file mode 100644 index 3ec842fefd..0000000000 --- a/chronograf/server/org_config_test.go +++ /dev/null @@ -1,1076 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "io/ioutil" - "net/http/httptest" - "testing" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/organizations" -) - -func TestOrganizationConfig(t *testing.T) { - type args struct { - organizationID string - } - type fields struct { - organizationConfigStore chronograf.OrganizationConfigStore - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - args args - fields fields - wants wants - }{ - { - name: "Get organization configuration", - args: args{ - organizationID: "default", - }, - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "default": - return &chronograf.OrganizationConfig{ - OrganizationID: "default", - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "time", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - { - Name: "severity", - Position: 1, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - { - Type: "label", - Value: "icon", - }, - { - Type: "label", - Value: "text", - }, - }, - }, - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "message", - Position: 3, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "facility", - Position: 4, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "procid", - Position: 5, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - { - Type: "displayName", - Value: "Proc ID", - }, - }, - }, - { - Name: "appname", - Position: 6, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "displayName", - Value: "Application", - }, - }, - }, - { - Name: "host", - Position: 7, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"organization":"default","logViewer":{"columns":[{"name":"time","position":0,"encodings":[{"type":"visibility","value":"hidden"}]},{"name":"severity","position":1,"encodings":[{"type":"visibility","value":"visible"},{"type":"label","value":"icon"},{"type":"label","value":"text"}]},{"name":"timestamp","position":2,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"message","position":3,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"facility","position":4,"encodings":[{"type":"visibility","value":"visible"}]},{"name":"procid","position":5,"encodings":[{"type":"visibility","value":"visible"},{"type":"displayName","value":"Proc ID"}]},{"name":"appname","position":6,"encodings":[{"type":"visibility","value":"visible"},{"type":"displayName","value":"Application"}]},{"name":"host","position":7,"encodings":[{"type":"visibility","value":"visible"}]}]}}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationConfigStore: tt.fields.organizationConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID) - r = r.WithContext(ctx) - - s.OrganizationConfig(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. OrganizationConfig() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. OrganizationConfig() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. OrganizationConfig() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestLogViewerOrganizationConfig(t *testing.T) { - type args struct { - organizationID string - } - type fields struct { - organizationConfigStore chronograf.OrganizationConfigStore - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - args args - fields fields - wants wants - }{ - { - name: "Get log viewer configuration", - args: args{ - organizationID: "default", - }, - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "default": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "emergency", - Name: "ruby", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "displayName", - Value: "Log Severity", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - }, - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/org_config/logviewer"},"columns":[{"name":"severity","position":0,"encodings":[{"type":"color","value":"emergency","name":"ruby"},{"type":"color","value":"info","name":"rainforest"},{"type":"displayName","value":"Log Severity"}]}]}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationConfigStore: tt.fields.organizationConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID) - r = r.WithContext(ctx) - - s.OrganizationLogViewerConfig(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. LogViewerOrganizationConfig() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. LogViewerOrganizationConfig() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. LogViewerOrganizationConfig() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func TestReplaceLogViewerOrganizationConfig(t *testing.T) { - type fields struct { - organizationConfigStore chronograf.OrganizationConfigStore - } - type args struct { - payload interface{} // expects JSON serializable struct - organizationID string - } - type wants struct { - statusCode int - contentType string - body string - } - - tests := []struct { - name string - fields fields - args args - wants wants - }{ - { - name: "Set log viewer configuration", - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "1337": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "visibility", - Value: "visible", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - PutF: func(ctx context.Context, target *chronograf.OrganizationConfig) error { - return nil - }, - }, - }, - args: args{ - payload: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 1, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "info", - Name: "pineapple", - }, - { - Type: "color", - Value: "emergency", - Name: "ruby", - }, - { - Type: "visibility", - Value: "visible", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - { - Name: "messages", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "displayName", - Value: "Log Messages", - }, - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - organizationID: "1337", - }, - wants: wants{ - statusCode: 200, - contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/org_config/logviewer"},"columns":[{"name":"severity","position":1,"encodings":[{"type":"color","value":"info","name":"pineapple"},{"type":"color","value":"emergency","name":"ruby"},{"type":"visibility","value":"visible"},{"type":"label","value":"icon"}]},{"name":"messages","position":0,"encodings":[{"type":"displayName","value":"Log Messages"},{"type":"visibility","value":"visible"}]}]}`, - }, - }, - { - name: "Set invalid log viewer configuration – empty", - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "1337": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "icon", - }, - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - PutF: func(ctx context.Context, target *chronograf.OrganizationConfig) error { - return nil - }, - }, - }, - args: args{ - payload: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{}, - }, - organizationID: "1337", - }, - wants: wants{ - statusCode: 400, - contentType: "application/json", - body: `{"code":400,"message":"invalid log viewer config: must have at least 1 column"}`, - }, - }, - { - name: "Set invalid log viewer configuration - duplicate column name", - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "1337": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "procid", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - PutF: func(ctx context.Context, target *chronograf.OrganizationConfig) error { - return nil - }, - }, - }, - args: args{ - payload: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "procid", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - { - Name: "procid", - Position: 1, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - }, - }, - organizationID: "1337", - }, - wants: wants{ - statusCode: 400, - contentType: "application/json", - body: `{"code":400,"message":"invalid log viewer config: Duplicate column name procid"}`, - }, - }, - { - name: "Set invalid log viewer configuration - multiple columns with same position value", - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "1337": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "procid", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - PutF: func(ctx context.Context, target *chronograf.OrganizationConfig) error { - return nil - }, - }, - }, - args: args{ - payload: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "procid", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - { - Name: "timestamp", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "hidden", - }, - }, - }, - }, - }, - organizationID: "1337", - }, - wants: wants{ - statusCode: 400, - contentType: "application/json", - body: `{"code":400,"message":"invalid log viewer config: Multiple columns with same position value"}`, - }, - }, - { - name: "Set invalid log viewer configuration – no visibility", - fields: fields{ - organizationConfigStore: &mocks.OrganizationConfigStore{ - FindOrCreateF: func(ctx context.Context, orgID string) (*chronograf.OrganizationConfig, error) { - switch orgID { - case "1337": - return &chronograf.OrganizationConfig{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - }, - }, - }, nil - default: - return nil, chronograf.ErrOrganizationConfigNotFound - } - }, - PutF: func(ctx context.Context, target *chronograf.OrganizationConfig) error { - return nil - }, - }, - }, - args: args{ - payload: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 1, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "color", - Value: "info", - Name: "pineapple", - }, - { - Type: "color", - Value: "emergency", - Name: "ruby", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - }, - }, - organizationID: "1337", - }, - wants: wants{ - statusCode: 400, - contentType: "application/json", - body: `{"code":400,"message":"invalid log viewer config: missing visibility encoding in column severity"}`, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationConfigStore: tt.fields.organizationConfigStore, - }, - Logger: &chronograf.NoopLogger{}, - } - - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "http://any.url", nil) - ctx := context.WithValue(r.Context(), organizations.ContextKey, tt.args.organizationID) - r = r.WithContext(ctx) - buf, _ := json.Marshal(tt.args.payload) - r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - - s.ReplaceOrganizationLogViewerConfig(w, r) - - resp := w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wants.statusCode { - t.Errorf("%q. ReplaceLogViewerOrganizationConfig() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) - } - if tt.wants.contentType != "" && content != tt.wants.contentType { - t.Errorf("%q. ReplaceLogViewerOrganizationConfig() = %v, want %v", tt.name, content, tt.wants.contentType) - } - if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { - t.Errorf("%q. ReplaceLogViewerOrganizationConfig() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) - } - }) - } -} - -func Test_validLogViewerConfig(t *testing.T) { - type args struct { - LogViewer chronograf.LogViewerConfig - } - - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "cannot have 0 columns", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: nil, - }, - }, - wantErr: true, - }, - { - name: "can have 1 column", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "can have more than 1 column", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "message", - Position: 3, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "facility", - Position: 4, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "cannot have multiple columns with the same name value", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "timestamp", - Position: 3, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "cannot have multiple columns with the same position value", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - { - Name: "message", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "each column must have a visibility encoding value of either 'visible' or 'hidden'", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "timestamp", - Position: 2, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "bob", - }, - }, - }, - { - Name: "message", - Position: 3, - Encodings: []chronograf.ColumnEncoding{ - - { - Type: "visibility", - Value: "visible", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "severity column can have 1 of each icon and text label encoding", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "icon", - }, - { - Type: "label", - Value: "text", - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "severity column can 1 icon label encoding", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "severity column can have 1 text label encoding", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "text", - }, - }, - }, - }, - }, - }, - wantErr: false, - }, - { - name: "severity column cannot have 0 label encodings", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "severity column cannot have more than 1 icon label encoding", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "icon", - }, - { - Type: "label", - Value: "icon", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "severity column cannot have more than 1 text label encoding", - args: args{ - LogViewer: chronograf.LogViewerConfig{ - Columns: []chronograf.LogViewerColumn{ - { - Name: "severity", - Position: 0, - Encodings: []chronograf.ColumnEncoding{ - { - Type: "visibility", - Value: "visible", - }, - { - Type: "color", - Value: "info", - Name: "rainforest", - }, - { - Type: "label", - Value: "text", - }, - { - Type: "label", - Value: "text", - }, - }, - }, - }, - }, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := validLogViewerConfig(tt.args.LogViewer) - - if (tt.wantErr && got == nil) || (!tt.wantErr && got != nil) { - t.Errorf("%q. validLogViewerConfig().\ngot: %v\nwantErr: %v", tt.name, got, tt.wantErr) - } - }) - } -} diff --git a/chronograf/server/organizations.go b/chronograf/server/organizations.go deleted file mode 100644 index 01f2cb8ce9..0000000000 --- a/chronograf/server/organizations.go +++ /dev/null @@ -1,232 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/organizations" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -type organizationRequest struct { - Name string `json:"name"` - DefaultRole string `json:"defaultRole"` -} - -func (r *organizationRequest) ValidCreate() error { - if r.Name == "" { - return fmt.Errorf("name required on Chronograf Organization request body") - } - - return r.ValidDefaultRole() -} - -func (r *organizationRequest) ValidUpdate() error { - if r.Name == "" && r.DefaultRole == "" { - return fmt.Errorf("no fields to update") - } - - if r.DefaultRole != "" { - return r.ValidDefaultRole() - } - - return nil -} - -func (r *organizationRequest) ValidDefaultRole() error { - if r.DefaultRole == "" { - r.DefaultRole = roles.MemberRoleName - } - - switch r.DefaultRole { - case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName: - return nil - default: - return fmt.Errorf("default role must be member, viewer, editor, or admin") - } -} - -type organizationResponse struct { - Links selfLinks `json:"links"` - chronograf.Organization -} - -func newOrganizationResponse(o *chronograf.Organization) *organizationResponse { - if o == nil { - o = &chronograf.Organization{} - } - return &organizationResponse{ - Organization: *o, - Links: selfLinks{ - Self: fmt.Sprintf("/chronograf/v1/organizations/%s", o.ID), - }, - } -} - -type organizationsResponse struct { - Links selfLinks `json:"links"` - Organizations []*organizationResponse `json:"organizations"` -} - -func newOrganizationsResponse(orgs []chronograf.Organization) *organizationsResponse { - orgsResp := make([]*organizationResponse, len(orgs)) - for i, org := range orgs { - orgsResp[i] = newOrganizationResponse(&org) - } - return &organizationsResponse{ - Organizations: orgsResp, - Links: selfLinks{ - Self: "/chronograf/v1/organizations", - }, - } -} - -// Organizations retrieves all organizations from store -func (s *Service) Organizations(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - orgs, err := s.Store.Organizations(ctx).All(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newOrganizationsResponse(orgs) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// NewOrganization adds a new organization to store -func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) { - var req organizationRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - if err := req.ValidCreate(); err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx := r.Context() - org := &chronograf.Organization{ - Name: req.Name, - DefaultRole: req.DefaultRole, - } - - res, err := s.Store.Organizations(ctx).Add(ctx, org) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - // Now that the organization was created, add the user - // making the request to the organization - user, ok := hasUserContext(ctx) - if !ok { - // Best attempt at cleanup the organization if there were any errors - _ = s.Store.Organizations(ctx).Delete(ctx, res) - Error(w, http.StatusInternalServerError, "failed to retrieve user from context", s.Logger) - return - } - - user.Roles = []chronograf.Role{ - { - Organization: res.ID, - Name: roles.AdminRoleName, - }, - } - - orgCtx := context.WithValue(ctx, organizations.ContextKey, res.ID) - _, err = s.Store.Users(orgCtx).Add(orgCtx, user) - if err != nil { - // Best attempt at cleanup the organization if there were any errors adding user to org - _ = s.Store.Organizations(ctx).Delete(ctx, res) - s.Logger.Error("failed to add user to organization", err.Error()) - Error(w, http.StatusInternalServerError, "failed to add user to organization", s.Logger) - return - } - - co := newOrganizationResponse(res) - location(w, co.Links.Self) - encodeJSON(w, http.StatusCreated, co, s.Logger) -} - -// OrganizationID retrieves a organization with ID from store -func (s *Service) OrganizationID(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - id := httprouter.GetParamFromContext(ctx, "oid") - - org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id}) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newOrganizationResponse(org) - encodeJSON(w, http.StatusOK, res, s.Logger) -} - -// UpdateOrganization updates an organization in the organizations store -func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) { - var req organizationRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - - if err := req.ValidUpdate(); err != nil { - invalidData(w, err, s.Logger) - return - } - - ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "oid") - - org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id}) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - if req.Name != "" { - org.Name = req.Name - } - - if req.DefaultRole != "" { - org.DefaultRole = req.DefaultRole - } - - err = s.Store.Organizations(ctx).Update(ctx, org) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - - res := newOrganizationResponse(org) - location(w, res.Links.Self) - encodeJSON(w, http.StatusOK, res, s.Logger) - -} - -// RemoveOrganization removes an organization in the organizations store -func (s *Service) RemoveOrganization(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - id := httprouter.GetParamFromContext(ctx, "oid") - - org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id}) - if err != nil { - Error(w, http.StatusNotFound, err.Error(), s.Logger) - return - } - if err := s.Store.Organizations(ctx).Delete(ctx, org); err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - w.WriteHeader(http.StatusNoContent) -} diff --git a/chronograf/server/organizations_test.go b/chronograf/server/organizations_test.go deleted file mode 100644 index 7f355753ba..0000000000 --- a/chronograf/server/organizations_test.go +++ /dev/null @@ -1,726 +0,0 @@ -package server - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/bouk/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" - "github.com/influxdata/influxdb/v2/chronograf/roles" -) - -func TestService_OrganizationID(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - tests := []struct { - name string - fields fields - args args - id string - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Get Single Organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "1337": - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - default: - return nil, fmt.Errorf("organization with ID %s not found", *q.ID) - } - }, - }, - }, - id: "1337", - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place"}`, - }, - { - name: "Get Single Organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "1337": - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - default: - return nil, fmt.Errorf("organization with ID %s not found", *q.ID) - } - }, - }, - }, - id: "1337", - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"id":"1337","name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - } - - tt.args.r = tt.args.r.WithContext(httprouter.WithParams( - context.Background(), - httprouter.Params{ - { - Key: "oid", - Value: tt.id, - }, - })) - - s.OrganizationID(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. OrganizationID() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. OrganizationID() = %v, want %v", tt.name, content, tt.wantContentType) - } - if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq { - t.Errorf("%q. OrganizationID() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - }) - } -} - -func TestService_Organizations(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - tests := []struct { - name string - fields fields - args args - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Get Organizations", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - AllF: func(ctx context.Context) ([]chronograf.Organization, error) { - return []chronograf.Organization{ - { - ID: "1337", - Name: "The Good Place", - }, - { - ID: "100", - Name: "The Bad Place", - }, - }, nil - }, - }, - }, - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"links":{"self":"/chronograf/v1/organizations"},"organizations":[{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place"},{"links":{"self":"/chronograf/v1/organizations/100"},"id":"100","name":"The Bad Place"}]}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - } - - s.Organizations(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. Organizations() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. Organizations() = %v, want %v", tt.name, content, tt.wantContentType) - } - if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq { - t.Errorf("%q. Organizations() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - }) - } -} - -func TestService_UpdateOrganization(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - org *organizationRequest - } - tests := []struct { - name string - fields fields - args args - id string - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Update Organization name", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{ - Name: "The Bad Place", - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - UpdateF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - }, - }, - id: "1337", - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"}}`, - }, - { - name: "Update Organization - nothing to update", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{}, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - UpdateF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - DefaultRole: roles.ViewerRoleName, - }, nil - }, - }, - }, - id: "1337", - wantStatus: http.StatusUnprocessableEntity, - wantContentType: "application/json", - wantBody: `{"code":422,"message":"no fields to update"}`, - }, - { - name: "Update Organization default role", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{ - DefaultRole: roles.ViewerRoleName, - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - UpdateF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - DefaultRole: roles.MemberRoleName, - }, nil - }, - }, - }, - id: "1337", - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer"}`, - }, - { - name: "Update Organization - invalid update", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{}, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - UpdateF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return nil, nil - }, - }, - }, - id: "1337", - wantStatus: http.StatusUnprocessableEntity, - wantContentType: "application/json", - wantBody: `{"code":422,"message":"no fields to update"}`, - }, - { - name: "Update Organization - invalid role", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{ - DefaultRole: "sillyrole", - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - UpdateF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - return nil, nil - }, - }, - }, - id: "1337", - wantStatus: http.StatusUnprocessableEntity, - wantContentType: "application/json", - wantBody: `{"code":422,"message":"default role must be member, viewer, editor, or admin"}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - } - - tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(), - httprouter.Params{ - { - Key: "oid", - Value: tt.id, - }, - })) - - buf, _ := json.Marshal(tt.args.org) - tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - s.UpdateOrganization(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, content, tt.wantContentType) - } - if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq { - t.Errorf("%q. NewOrganization() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - }) - } -} - -func TestService_RemoveOrganization(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - Logger chronograf.Logger - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - tests := []struct { - name string - fields fields - args args - id string - wantStatus int - }{ - { - name: "Update Organization name", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - OrganizationsStore: &mocks.OrganizationsStore{ - DeleteF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) { - switch *q.ID { - case "1337": - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - default: - return nil, fmt.Errorf("organization with ID %s not found", *q.ID) - } - }, - }, - }, - id: "1337", - wantStatus: http.StatusNoContent, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - } - - tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(), - httprouter.Params{ - { - Key: "oid", - Value: tt.id, - }, - })) - s.RemoveOrganization(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - }) - } -} - -func TestService_NewOrganization(t *testing.T) { - type fields struct { - OrganizationsStore chronograf.OrganizationsStore - UsersStore chronograf.UsersStore - Logger chronograf.Logger - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - org *organizationRequest - user *chronograf.User - } - tests := []struct { - name string - fields fields - args args - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "Create Organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - user: &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, - org: &organizationRequest{ - Name: "The Good Place", - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - }, - }, - }, - wantStatus: http.StatusCreated, - wantContentType: "application/json", - wantBody: `{"id":"1337","name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`, - }, - { - name: "Fail to create Organization - no org name", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - user: &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, - org: &organizationRequest{}, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) { - return nil, nil - }, - }, - }, - wantStatus: http.StatusUnprocessableEntity, - wantContentType: "application/json", - wantBody: `{"code":422,"message":"name required on Chronograf Organization request body"}`, - }, - { - name: "Create Organization - no user on context", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{ - Name: "The Good Place", - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, nil - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - }, - DeleteF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - }, - }, - wantStatus: http.StatusInternalServerError, - wantContentType: "application/json", - wantBody: `{"code":500,"message":"failed to retrieve user from context"}`, - }, - { - name: "Create Organization - failed to add user to organization", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "GET", - "http://any.url", // can be any valid URL as we are bypassing mux - nil, - ), - org: &organizationRequest{ - Name: "The Good Place", - }, - user: &chronograf.User{ - ID: 1, - Name: "bobetta", - Provider: "github", - Scheme: "oauth2", - }, - }, - fields: fields{ - Logger: &chronograf.NoopLogger{}, - UsersStore: &mocks.UsersStore{ - AddF: func(ctx context.Context, u *chronograf.User) (*chronograf.User, error) { - return nil, fmt.Errorf("failed to add user to org") - }, - }, - OrganizationsStore: &mocks.OrganizationsStore{ - AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) { - return &chronograf.Organization{ - ID: "1337", - Name: "The Good Place", - }, nil - }, - DeleteF: func(ctx context.Context, o *chronograf.Organization) error { - return nil - }, - }, - }, - wantStatus: http.StatusInternalServerError, - wantContentType: "application/json", - wantBody: `{"code":500,"message":"failed to add user to organization"}`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := &Service{ - Store: &mocks.Store{ - OrganizationsStore: tt.fields.OrganizationsStore, - UsersStore: tt.fields.UsersStore, - }, - Logger: tt.fields.Logger, - } - - ctx := tt.args.r.Context() - ctx = context.WithValue(ctx, UserContextKey, tt.args.user) - tt.args.r = tt.args.r.WithContext(ctx) - - buf, _ := json.Marshal(tt.args.org) - tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf)) - s.NewOrganization(tt.args.w, tt.args.r) - - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. NewOrganization() = %v, want %v", tt.name, content, tt.wantContentType) - } - if eq, _ := jsonEqual(string(body), tt.wantBody); tt.wantBody != "" && !eq { - t.Errorf("%q. NewOrganization() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - }) - } -} diff --git a/chronograf/server/path.go b/chronograf/server/path.go deleted file mode 100644 index c1293e3cca..0000000000 --- a/chronograf/server/path.go +++ /dev/null @@ -1,10 +0,0 @@ -package server - -import "net/url" - -// PathEscape escapes the string so it can be safely placed inside a URL path segment. -// Change to url.PathEscape for go 1.8 -func PathEscape(str string) string { - u := &url.URL{Path: str} - return u.String() -} diff --git a/chronograf/server/permissions.go b/chronograf/server/permissions.go deleted file mode 100644 index ae7123e567..0000000000 --- a/chronograf/server/permissions.go +++ /dev/null @@ -1,55 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -// Permissions returns all possible permissions for this source. -func (s *Service) Permissions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - src, err := s.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, s.Logger) - return - } - - ts, err := s.TimeSeries(src) - if err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - if err = ts.Connect(ctx, &src); err != nil { - msg := fmt.Sprintf("unable to connect to source %d: %v", srcID, err) - Error(w, http.StatusBadRequest, msg, s.Logger) - return - } - - perms := ts.Permissions(ctx) - if err != nil { - Error(w, http.StatusBadRequest, err.Error(), s.Logger) - return - } - httpAPISrcs := "/chronograf/v1/sources" - res := struct { - Permissions chronograf.Permissions `json:"permissions"` - Links map[string]string `json:"links"` // Links are URI locations related to user - }{ - Permissions: perms, - Links: map[string]string{ - "self": fmt.Sprintf("%s/%d/permissions", httpAPISrcs, srcID), - "source": fmt.Sprintf("%s/%d", httpAPISrcs, srcID), - }, - } - encodeJSON(w, http.StatusOK, res, s.Logger) -} diff --git a/chronograf/server/permissions_test.go b/chronograf/server/permissions_test.go deleted file mode 100644 index 1d0db37fa4..0000000000 --- a/chronograf/server/permissions_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package server - -import ( - "bytes" - "context" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestService_Permissions(t *testing.T) { - type fields struct { - SourcesStore chronograf.SourcesStore - TimeSeries TimeSeriesClient - Logger chronograf.Logger - UseAuth bool - } - type args struct { - w *httptest.ResponseRecorder - r *http.Request - } - tests := []struct { - name string - fields fields - args args - ID string - wantStatus int - wantContentType string - wantBody string - }{ - { - name: "New user for data source", - args: args{ - w: httptest.NewRecorder(), - r: httptest.NewRequest( - "POST", - "http://server.local/chronograf/v1/sources/1", - ioutil.NopCloser( - bytes.NewReader([]byte(`{"name": "marty", "password": "the_lake"}`)))), - }, - fields: fields{ - UseAuth: true, - Logger: &chronograf.NoopLogger{}, - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: 1, - Name: "muh source", - Username: "name", - Password: "hunter2", - URL: "http://localhost:8086", - }, nil - }, - }, - TimeSeries: &mocks.TimeSeries{ - ConnectF: func(ctx context.Context, src *chronograf.Source) error { - return nil - }, - PermissionsF: func(ctx context.Context) chronograf.Permissions { - return chronograf.Permissions{ - { - Scope: chronograf.AllScope, - Allowed: chronograf.Allowances{"READ", "WRITE"}, - }, - } - }, - }, - }, - ID: "1", - wantStatus: http.StatusOK, - wantContentType: "application/json", - wantBody: `{"permissions":[{"scope":"all","allowed":["READ","WRITE"]}],"links":{"self":"/chronograf/v1/sources/1/permissions","source":"/chronograf/v1/sources/1"}} -`, - }, - } - for _, tt := range tests { - tt.args.r = tt.args.r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: tt.ID, - }, - })) - h := &Service{ - Store: &mocks.Store{ - SourcesStore: tt.fields.SourcesStore, - }, - TimeSeriesClient: tt.fields.TimeSeries, - Logger: tt.fields.Logger, - UseAuth: tt.fields.UseAuth, - } - h.Permissions(tt.args.w, tt.args.r) - resp := tt.args.w.Result() - content := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode != tt.wantStatus { - t.Errorf("%q. Permissions() = %v, want %v", tt.name, resp.StatusCode, tt.wantStatus) - } - if tt.wantContentType != "" && content != tt.wantContentType { - t.Errorf("%q. Permissions() = %v, want %v", tt.name, content, tt.wantContentType) - } - if tt.wantBody != "" && string(body) != tt.wantBody { - t.Errorf("%q. Permissions() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wantBody) - } - } -} diff --git a/chronograf/server/prefixing_redirector.go b/chronograf/server/prefixing_redirector.go deleted file mode 100644 index 0317ceb399..0000000000 --- a/chronograf/server/prefixing_redirector.go +++ /dev/null @@ -1,34 +0,0 @@ -package server - -import ( - "net/http" -) - -type flushingResponseWriter struct { - http.ResponseWriter -} - -func (f *flushingResponseWriter) WriteHeader(status int) { - f.ResponseWriter.WriteHeader(status) -} - -// Flush is here because the underlying HTTP chunked transfer response writer -// to implement http.Flusher. Without it data is silently buffered. This -// was discovered when proxying kapacitor chunked logs. -func (f *flushingResponseWriter) Flush() { - if flusher, ok := f.ResponseWriter.(http.Flusher); ok { - flusher.Flush() - } -} - -// FlushingHandler may not actually do anything, but it was ostensibly -// implemented to flush response writers that can be flushed for the -// purposes in the comment above. -func FlushingHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - iw := &flushingResponseWriter{ - ResponseWriter: w, - } - next.ServeHTTP(iw, r) - }) -} diff --git a/chronograf/server/proxy.go b/chronograf/server/proxy.go deleted file mode 100644 index ad313184a0..0000000000 --- a/chronograf/server/proxy.go +++ /dev/null @@ -1,121 +0,0 @@ -package server - -import ( - "crypto/tls" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "strings" - "time" -) - -// Proxy proxies requests to services using the path query parameter. -func (s *Service) Proxy(w http.ResponseWriter, r *http.Request) { - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - id, err := paramID("kid", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - path := r.URL.Query().Get("path") - if path == "" { - Error(w, http.StatusUnprocessableEntity, "path query parameter required", s.Logger) - return - } - - ctx := r.Context() - srv, err := s.Store.Servers(ctx).Get(ctx, id) - if err != nil || srv.SrcID != srcID { - notFound(w, id, s.Logger) - return - } - - // To preserve any HTTP query arguments to the kapacitor path, - // we concat and parse them into u. - uri := singleJoiningSlash(srv.URL, path) - u, err := url.Parse(uri) - if err != nil { - msg := fmt.Sprintf("Error parsing kapacitor url: %v", err) - Error(w, http.StatusUnprocessableEntity, msg, s.Logger) - return - } - - director := func(req *http.Request) { - // Set the Host header of the original Kapacitor URL - req.Host = u.Host - req.URL = u - - // Because we are acting as a proxy, kapacitor needs to have the basic auth information set as - // a header directly - if srv.Username != "" && srv.Password != "" { - req.SetBasicAuth(srv.Username, srv.Password) - } - } - - // Without a FlushInterval the HTTP Chunked response for kapacitor logs is - // buffered and flushed every 30 seconds. - proxy := &httputil.ReverseProxy{ - Director: director, - FlushInterval: time.Second, - } - - // The connection to kapacitor is using a self-signed certificate. - // This modifies uses the same values as http.DefaultTransport but specifies - // InsecureSkipVerify - if srv.InsecureSkipVerify { - proxy.Transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - } - proxy.ServeHTTP(w, r) -} - -// ProxyPost proxies POST to service -func (s *Service) ProxyPost(w http.ResponseWriter, r *http.Request) { - s.Proxy(w, r) -} - -// ProxyPatch proxies PATCH to Service -func (s *Service) ProxyPatch(w http.ResponseWriter, r *http.Request) { - s.Proxy(w, r) -} - -// ProxyGet proxies GET to service -func (s *Service) ProxyGet(w http.ResponseWriter, r *http.Request) { - s.Proxy(w, r) -} - -// ProxyDelete proxies DELETE to service -func (s *Service) ProxyDelete(w http.ResponseWriter, r *http.Request) { - s.Proxy(w, r) -} - -func singleJoiningSlash(a, b string) string { - aslash := strings.HasSuffix(a, "/") - bslash := strings.HasPrefix(b, "/") - if aslash && bslash { - return a + b[1:] - } - if !aslash && !bslash { - return a + "/" + b - } - return a + b -} diff --git a/chronograf/server/queries.go b/chronograf/server/queries.go deleted file mode 100644 index 38ab656a33..0000000000 --- a/chronograf/server/queries.go +++ /dev/null @@ -1,134 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - "net/http" - "time" - - "golang.org/x/net/context" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/influx" - "github.com/influxdata/influxdb/v2/chronograf/influx/queries" -) - -// QueryRequest is query that will be converted to a queryConfig -type QueryRequest struct { - ID string `json:"id"` - Query string `json:"query"` -} - -// QueriesRequest converts all queries to queryConfigs with the help -// of the template variables -type QueriesRequest struct { - Queries []QueryRequest `json:"queries"` - TemplateVars []chronograf.TemplateVar `json:"tempVars,omitempty"` -} - -// QueryResponse is the return result of a QueryRequest including -// the raw query, the templated query, the queryConfig and the queryAST -type QueryResponse struct { - Duration int64 `json:"durationMs"` - ID string `json:"id"` - Query string `json:"query"` - QueryConfig chronograf.QueryConfig `json:"queryConfig"` - QueryAST *queries.SelectStatement `json:"queryAST,omitempty"` - QueryTemplated *string `json:"queryTemplated,omitempty"` -} - -// QueriesResponse is the response for a QueriesRequest -type QueriesResponse struct { - Queries []QueryResponse `json:"queries"` -} - -// Queries analyzes InfluxQL to produce front-end friendly QueryConfig -func (s *Service) Queries(w http.ResponseWriter, r *http.Request) { - srcID, err := paramID("id", r) - if err != nil { - Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) - return - } - - ctx := r.Context() - src, err := s.Store.Sources(ctx).Get(ctx, srcID) - if err != nil { - notFound(w, srcID, s.Logger) - return - } - - var req QueriesRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - invalidJSON(w, s.Logger) - return - } - res := QueriesResponse{ - Queries: make([]QueryResponse, len(req.Queries)), - } - - for i, q := range req.Queries { - qr := QueryResponse{ - 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 - } - qc.Shifts = []chronograf.TimeShift{} - qr.QueryConfig = qc - - if stmt, err := queries.ParseSelect(q.Query); err == nil { - qr.QueryAST = stmt - } - - if dur, err := influx.ParseTime(q.Query, time.Now()); err == nil { - ms := dur.Nanoseconds() / int64(time.Millisecond) - if ms == 0 { - ms = 1 - } - - qr.Duration = ms - } - - 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 -} diff --git a/chronograf/server/queries_test.go b/chronograf/server/queries_test.go deleted file mode 100644 index c8008861e4..0000000000 --- a/chronograf/server/queries_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package server - -import ( - "bytes" - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/influxdata/httprouter" - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/mocks" -) - -func TestService_Queries(t *testing.T) { - tests := []struct { - name string - SourcesStore chronograf.SourcesStore - ID string - w *httptest.ResponseRecorder - r *http.Request - want string - }{ - { - name: "bad json", - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`howdy`))), - want: `{"code":400,"message":"unparsable JSON"}`, - }, - { - name: "bad id", - ID: "howdy", - w: httptest.NewRecorder(), - r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte{})), - want: `{"code":422,"message":"error converting ID howdy"}`, - }, - { - name: "query with no template vars", - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ - "queries": [ - { - "query": "SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time > now() - 1m", - "id": "82b60d37-251e-4afe-ac93-ca20a3642b11" - } - ]}`))), - want: `{"queries":[{"durationMs":59999,"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"},"shifts":[]},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]} -`, - }, - { - name: "query with unparsable query", - SourcesStore: &mocks.SourcesStore{ - GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { - return chronograf.Source{ - ID: ID, - }, nil - }, - }, - ID: "1", - w: httptest.NewRecorder(), - r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ - "queries": [ - { - "query": "SHOW DATABASES", - "id": "82b60d37-251e-4afe-ac93-ca20a3642b11" - } - ]}`))), - want: `{"queries":[{"durationMs":0,"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null,"shifts":[]}}]} -`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.r = tt.r.WithContext(context.WithValue( - context.TODO(), - httprouter.ParamsKey, - httprouter.Params{ - { - Key: "id", - Value: tt.ID, - }, - })) - s := &Service{ - Store: &mocks.Store{ - SourcesStore: tt.SourcesStore, - }, - Logger: &mocks.TestLogger{}, - } - s.Queries(tt.w, tt.r) - got := tt.w.Body.String() - if got != tt.want { - t.Errorf("got:\n%s\nwant:\n%s\n", got, tt.want) - } - }) - } -} diff --git a/chronograf/server/queryconfig.go b/chronograf/server/queryconfig.go deleted file mode 100644 index 6f575a688f..0000000000 --- a/chronograf/server/queryconfig.go +++ /dev/null @@ -1,51 +0,0 @@ -package server - -import ( - "fmt" - - "github.com/influxdata/influxdb/v2/chronograf" - "github.com/influxdata/influxdb/v2/chronograf/influx" -) - -// ToQueryConfig converts InfluxQL into queryconfigs -// If influxql cannot be represented by a full query config, then, the -// query config's raw text is set to the query. -func ToQueryConfig(query string) chronograf.QueryConfig { - qc, err := influx.Convert(query) - if err == nil { - return qc - } - return chronograf.QueryConfig{ - RawText: &query, - Fields: []chronograf.Field{}, - GroupBy: chronograf.GroupBy{ - Tags: []string{}, - }, - Tags: make(map[string][]string), - } -} - -var validFieldTypes = map[string]bool{ - "func": true, - "field": true, - "integer": true, - "number": true, - "regex": true, - "wildcard": true, -} - -// ValidateQueryConfig checks any query config input -func ValidateQueryConfig(q *chronograf.QueryConfig) error { - for _, fld := range q.Fields { - invalid := fmt.Errorf(`invalid field type "%s" ; expect func, field, integer, number, regex, wildcard`, fld.Type) - if !validFieldTypes[fld.Type] { - return invalid - } - for _, arg := range fld.Args { - if !validFieldTypes[arg.Type] { - return invalid - } - } - } - return nil -} diff --git a/chronograf/server/queryconfig_test.go b/chronograf/server/queryconfig_test.go deleted file mode 100644 index c6ea49f837..0000000000 --- a/chronograf/server/queryconfig_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "testing" - - "github.com/influxdata/influxdb/v2/chronograf" -) - -func TestValidateQueryConfig(t *testing.T) { - tests := []struct { - name string - q *chronograf.QueryConfig - wantErr bool - }{ - { - name: "invalid field type", - q: &chronograf.QueryConfig{ - Fields: []chronograf.Field{ - { - Type: "invalid", - }, - }, - }, - wantErr: true, - }, - { - name: "invalid field args", - q: &chronograf.QueryConfig{ - Fields: []chronograf.Field{ - { - Type: "func", - Args: []chronograf.Field{ - { - Type: "invalid", - }, - }, - }, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := ValidateQueryConfig(tt.q); (err != nil) != tt.wantErr { - t.Errorf("ValidateQueryConfig() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/chronograf/server/redoc.go b/chronograf/server/redoc.go deleted file mode 100644 index 4cc39bf27d..0000000000 --- a/chronograf/server/redoc.go +++ /dev/null @@ -1,39 +0,0 @@ -package server - -import ( - "fmt" - "net/http" -) - -const index = ` - -
-