merged upstream, updated comments, added GroupFromClaims()

pull/10616/head
Benjamin Schweizer 2018-02-20 09:47:42 +01:00
parent 9d931390ee
commit 227009723d
184 changed files with 9102 additions and 1903 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.4.0.0
current_version = 1.4.1.3
files = README.md server/swagger.json
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.(?P<release>\d+)
serialize = {major}.{minor}.{patch}.{release}

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ backup/
# Binaries
/chronograf
/chronoctl
# Dotfiles
.pull-request

View File

@ -1,18 +1,54 @@
## v1.4.1.0 [unreleased]
## v1.4.2.0 [unreleased]
### Features
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule
1. [#2526](https://github.com/influxdata/chronograf/pull/2526): Add support for RS256/JWKS verification, support for id_token parsing (as in ADFS)
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations"
### UI Improvements
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
### Bug Fixes
## v1.4.1.3 [2018-02-14]
### Bug Fixes
1. [#2818](https://github.com/influxdata/chronograf/pull/2818): Allow self-signed certificates for Enterprise InfluxDB Meta nodes
## v1.4.1.2 [2018-02-13]
### Bug Fixes
1. [9321336](https://github.com/influxdata/chronograf/commit/9321336): Respect basepath when fetching server api routes
1. [#2812](https://github.com/influxdata/chronograf/pull/2812): Set default tempVar :interval: with data explorer csv download call.
1. [#2811](https://github.com/influxdata/chronograf/pull/2811): Display series with value of 0 in a cell legend
## v1.4.1.1 [2018-02-12]
### Features
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow multiple event handlers per rule
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to kapacitor config panel from alert rule builder
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
1. [#2784](https://github.com/influxdata/chronograf/pull/2784): Update go from 1.9.3 to 1.9.4
1. [#2765](https://github.com/influxdata/chronograf/pull/2765): Update to go 1.9.3 and node 6.12.3 for releases
1. [#2777](https://github.com/influxdata/chronograf/pull/2777): Allow user to delete themselves
1. [#2703](https://github.com/influxdata/chronograf/pull/2703): Add All Users page, visible only to super admins
1. [#2781](https://github.com/influxdata/chronograf/pull/2781): Introduce chronoctl binary for user CRUD operations
1. [#2699](https://github.com/influxdata/chronograf/pull/2699): Introduce Mappings to allow control over new user organization assignments
### UI Improvements
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Clarify terminology surrounding InfluxDB & Kapacitor connections
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
1. [#2774](https://github.com/influxdata/chronograf/pull/2774): Enable Save (⌘ + Enter) and Cancel (Escape) hotkeys in Cell Editor Overlay
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Enable customization of Single Stat "Base Color"
### Bug Fixes
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
1. [#2756](https://github.com/influxdata/chronograf/pull/2756): Display 200 most-recent TICKscript log messages; prevent overlapping
1. [#2757](https://github.com/influxdata/chronograf/pull/2757): Add "TO" to kapacitor SMTP config; improve config update error messages
1. [#2761](https://github.com/influxdata/chronograf/pull/2761): Remove cli options from sysvinit service file
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Fix disappearance of text in Single Stat graphs during editing
1. [#2780](https://github.com/influxdata/chronograf/pull/2780): Redirect to Alerts page after saving Alert Rule
## v1.4.0.1 [2018-1-9]
### Features
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
1. [#2672](https://github.com/influxdata/chronograf/pull/2672): Add telegraf interval configuration
### Bug Fixes
1. [#2689](https://github.com/influxdata/chronograf/pull/2689): Allow insecure (self-signed) certificates for kapacitor and influxdb
## v1.4.0.1 [unreleased]
### UI Improvements
1. [#2690](https://github.com/influxdata/chronograf/pull/2690): Add separate CLI flag for canned sources, kapacitors, dashboards, and organizations
1. [#2664](https://github.com/influxdata/chronograf/pull/2664): Fix positioning of custom time indicator
## v1.4.0.0 [2017-12-22]
### UI Improvements

View File

@ -5,6 +5,7 @@ RUN apk add --update ca-certificates && \
rm /var/cache/apk/*
ADD chronograf /usr/bin/chronograf
ADD chronoctl /usr/bin/chronoctl
ADD canned/*.json /usr/share/chronograf/canned/
ADD LICENSE /usr/share/chronograf/LICENSE
ADD agpl-3.0.md /usr/share/chronograf/agpl-3.0.md

112
Gopkg.lock generated
View File

@ -30,7 +30,7 @@
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "259d2a102b871d17f30e3cd9881a642961a1e486"
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
name = "github.com/elazarl/go-bindata-assetfs"
@ -39,18 +39,53 @@
[[projects]]
name = "github.com/gogo/protobuf"
packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"]
packages = [
"gogoproto",
"jsonpb",
"plugin/compare",
"plugin/defaultcheck",
"plugin/description",
"plugin/embedcheck",
"plugin/enumstringer",
"plugin/equal",
"plugin/face",
"plugin/gostring",
"plugin/marshalto",
"plugin/oneofcheck",
"plugin/populate",
"plugin/size",
"plugin/stringer",
"plugin/testgen",
"plugin/union",
"plugin/unmarshal",
"proto",
"protoc-gen-gogo",
"protoc-gen-gogo/descriptor",
"protoc-gen-gogo/generator",
"protoc-gen-gogo/grpc",
"protoc-gen-gogo/plugin",
"vanity",
"vanity/command"
]
revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b"
[[projects]]
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "8ee79997227bf9b34611aee7946ae64735e6fd93"
revision = "925541529c1fa6821df4e44ce2723319eb2be768"
version = "v1.0.0"
[[projects]]
name = "github.com/google/go-cmp"
packages = ["cmp","cmp/cmpopts"]
revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8"
packages = [
"cmp",
"cmp/cmpopts",
"cmp/internal/diff",
"cmp/internal/function",
"cmp/internal/value"
]
revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97"
version = "v0.1.0"
[[projects]]
name = "github.com/google/go-github"
@ -58,19 +93,35 @@
revision = "1bc362c7737e51014af7299e016444b654095ad9"
[[projects]]
branch = "master"
name = "github.com/google/go-querystring"
packages = ["query"]
revision = "9235644dd9e52eeae6fa48efd539fdc351a0af53"
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
name = "github.com/influxdata/influxdb"
packages = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"]
packages = [
"influxql",
"influxql/internal",
"influxql/neldermead",
"models",
"pkg/escape"
]
revision = "cd9363b52cac452113b95554d98a6be51beda24e"
version = "v1.1.5"
[[projects]]
name = "github.com/influxdata/kapacitor"
packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"]
packages = [
"client/v1",
"pipeline",
"pipeline/tick",
"services/k8s/client",
"tick",
"tick/ast",
"tick/stateful",
"udf/agent"
]
revision = "6de30070b39afde111fea5e041281126fe8aae31"
[[projects]]
@ -84,15 +135,15 @@
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
[[projects]]
name = "github.com/jteeuwen/go-bindata"
name = "github.com/kevinburke/go-bindata"
packages = ["."]
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
[[projects]]
branch = "master"
name = "github.com/pkg/errors"
packages = ["."]
revision = "ff09b135c25aae272398c51a07235b90a75aa4f0"
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/satori/go.uuid"
@ -107,39 +158,60 @@
[[projects]]
name = "github.com/tylerb/graceful"
packages = ["."]
revision = "50a48b6e73fcc75b45e22c05b79629a67c79e938"
version = "v1.2.13"
revision = "4654dfbb6ad53cb5e27f37d99b02e16c1872fbbb"
version = "v1.2.15"
[[projects]]
name = "golang.org/x/net"
packages = ["context","context/ctxhttp"]
packages = [
"context",
"context/ctxhttp"
]
revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe"
[[projects]]
name = "golang.org/x/oauth2"
packages = [".","github","heroku","internal"]
packages = [
".",
"github",
"heroku",
"internal"
]
revision = "1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = ["unix"]
revision = "f3918c30c5c2cb527c0b071a27c35120a6c0719a"
revision = "37707fdb30a5b38865cfb95e5aab41707daec7fd"
[[projects]]
name = "google.golang.org/api"
packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"]
packages = [
"gensupport",
"googleapi",
"googleapi/internal/uritemplates",
"oauth2/v2"
]
revision = "bc20c61134e1d25265dd60049f5735381e79b631"
[[projects]]
name = "google.golang.org/appengine"
packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"]
packages = [
"internal",
"internal/base",
"internal/datastore",
"internal/log",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "a5bd1aa82919723ff8ec5dd9d520329862de8181ca9dba75c6acb3a34df5f1a4"
inputs-digest = "11df631364d11bc05c8f71af1aa735360b5a40a793d32d47d1f1d8c694a55f6f"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,4 +1,4 @@
required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto","github.com/gogo/protobuf/jsonpb","github.com/gogo/protobuf/protoc-gen-gogo","github.com/gogo/protobuf/gogoproto"]
[[constraint]]
name = "github.com/NYTimes/gziphandler"
@ -41,8 +41,8 @@ required = ["github.com/jteeuwen/go-bindata","github.com/gogo/protobuf/proto","g
revision = "4cc2832a6e6d1d3b815e2b9d544b2a4dfb3ce8fa"
[[constraint]]
name = "github.com/jteeuwen/go-bindata"
revision = "a0ff2567cfb70903282db057e799fd826784d41d"
name = "github.com/kevinburke/go-bindata"
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
[[constraint]]
name = "github.com/satori/go.uuid"

View File

@ -2,7 +2,7 @@
VERSION ?= $(shell git describe --always --tags)
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
GOBINDATA := $(shell go list -f {{.Root}} github.com/kevinburke/go-bindata 2> /dev/null)
YARN := $(shell command -v yarn 2> /dev/null)
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go' -not -path "./vendor/*" )
@ -11,6 +11,7 @@ UISOURCES := $(shell find ui -type f -not \( -path ui/build/\* -o -path ui/node_
unexport LDFLAGS
LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.commit=${COMMIT}"
BINARY=chronograf
CTLBINARY=chronoctl
.DEFAULT_GOAL := all
@ -22,6 +23,7 @@ dev: dep dev-assets ${BINARY}
${BINARY}: $(SOURCES) .bindata .jsdep .godep
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
go build -o ${CTLBINARY} ${LDFLAGS} ./cmd/chronoctl
define CHRONOGIRAFFE
._ o o
@ -73,7 +75,7 @@ dep: .jsdep .godep
.godep:
ifndef GOBINDATA
@echo "Installing go-bindata"
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/kevinburke/go-bindata/...
endif
@touch .godep

View File

@ -136,7 +136,7 @@ option.
## Versions
The most recent version of Chronograf is
[v1.4.0.0](https://www.influxdata.com/downloads/).
[v1.4.1.3](https://www.influxdata.com/downloads/).
Spotted a bug or have a feature request? Please open
[an issue](https://github.com/influxdata/chronograf/issues/new)!
@ -156,7 +156,7 @@ The Chronograf team has identified and is working on the following issues:
## Installation
Check out the
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.3/introduction/installation/)
[INSTALLATION](https://docs.influxdata.com/chronograf/v1.4/introduction/installation/)
guide to get up and running with Chronograf with as little configuration and
code as possible.
@ -178,7 +178,7 @@ By default, chronograf runs on port `8888`.
To get started right away with Docker, you can pull down our latest release:
```sh
docker pull chronograf:1.4.0.0
docker pull chronograf:1.4.1.3
```
### From Source
@ -198,10 +198,10 @@ docker pull chronograf:1.4.0.0
## Documentation
[Getting Started](https://docs.influxdata.com/chronograf/v1.3/introduction/getting-started/)
[Getting Started](https://docs.influxdata.com/chronograf/v1.4/introduction/getting-started/)
will get you up and running with Chronograf with as little configuration and
code as possible. See our
[guides](https://docs.influxdata.com/chronograf/v1.3/guides/) to get familiar
[guides](https://docs.influxdata.com/chronograf/v1.4/guides/) to get familiar
with Chronograf's main features.
Documentation for Telegraf, InfluxDB, and Kapacitor are available at

View File

@ -41,6 +41,7 @@ type Client struct {
UsersStore *UsersStore
OrganizationsStore *OrganizationsStore
ConfigStore *ConfigStore
MappingsStore *MappingsStore
}
// NewClient initializes all stores
@ -60,6 +61,7 @@ func NewClient() *Client {
c.UsersStore = &UsersStore{client: c}
c.OrganizationsStore = &OrganizationsStore{client: c}
c.ConfigStore = &ConfigStore{client: c}
c.MappingsStore = &MappingsStore{client: c}
return c
}
@ -151,6 +153,10 @@ func (c *Client) initialize(ctx context.Context) error {
if _, err := tx.CreateBucketIfNotExists(BuildBucket); err != nil {
return err
}
// Always create Mapping bucket.
if _, err := tx.CreateBucketIfNotExists(MappingsBucket); err != nil {
return err
}
return nil
}); err != nil {
return err
@ -184,6 +190,9 @@ func (c *Client) migrate(ctx context.Context, build chronograf.BuildInfo) error
if err := c.BuildStore.Migrate(ctx, build); err != nil {
return err
}
if err := c.MappingsStore.Migrate(ctx); err != nil {
return err
}
}
return nil
}

View File

@ -34,7 +34,7 @@ func (s *ConfigStore) Migrate(ctx context.Context) error {
func (s *ConfigStore) Initialize(ctx context.Context) error {
cfg := chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: true,
SuperAdminNewUsers: false,
},
}
return s.Update(ctx, &cfg)

View File

@ -22,7 +22,7 @@ func TestConfig_Get(t *testing.T) {
wants: wants{
config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: true,
SuperAdminNewUsers: false,
},
},
},

View File

@ -274,6 +274,10 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
Type: c.Type,
Axes: axes,
Colors: colors,
Legend: &Legend{
Type: c.Legend.Type,
Orientation: c.Legend.Orientation,
},
}
}
templates := make([]*Template, len(d.Templates))
@ -394,6 +398,12 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
}
}
legend := chronograf.Legend{}
if c.Legend != nil {
legend.Type = c.Legend.Type
legend.Orientation = c.Legend.Orientation
}
cells[i] = chronograf.DashboardCell{
ID: c.ID,
X: c.X,
@ -405,6 +415,7 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
Type: c.Type,
Axes: axes,
CellColors: colors,
Legend: legend,
}
}
@ -570,19 +581,16 @@ func UnmarshalRole(data []byte, r *chronograf.Role) error {
// UnmarshalRolePB decodes a role from binary protobuf data.
func UnmarshalRolePB(data []byte, r *Role) error {
if err := proto.Unmarshal(data, r); err != nil {
return err
}
return nil
return proto.Unmarshal(data, r)
}
// MarshalOrganization encodes a organization to binary protobuf format.
func MarshalOrganization(o *chronograf.Organization) ([]byte, error) {
return MarshalOrganizationPB(&Organization{
ID: o.ID,
Name: o.Name,
DefaultRole: o.DefaultRole,
Public: o.Public,
})
}
@ -600,17 +608,13 @@ func UnmarshalOrganization(data []byte, o *chronograf.Organization) error {
o.ID = pb.ID
o.Name = pb.Name
o.DefaultRole = pb.DefaultRole
o.Public = pb.Public
return nil
}
// UnmarshalOrganizationPB decodes a organization from binary protobuf data.
func UnmarshalOrganizationPB(data []byte, o *Organization) error {
if err := proto.Unmarshal(data, o); err != nil {
return err
}
return nil
return proto.Unmarshal(data, o)
}
// MarshalConfig encodes a config to binary protobuf format.
@ -643,8 +647,43 @@ func UnmarshalConfig(data []byte, c *chronograf.Config) error {
// UnmarshalConfigPB decodes a config from binary protobuf data.
func UnmarshalConfigPB(data []byte, c *Config) error {
if err := proto.Unmarshal(data, c); err != nil {
return proto.Unmarshal(data, c)
}
// MarshalMapping encodes a mapping to binary protobuf format.
func MarshalMapping(m *chronograf.Mapping) ([]byte, error) {
return MarshalMappingPB(&Mapping{
Provider: m.Provider,
Scheme: m.Scheme,
ProviderOrganization: m.ProviderOrganization,
ID: m.ID,
Organization: m.Organization,
})
}
// MarshalMappingPB encodes a mapping to binary protobuf format.
func MarshalMappingPB(m *Mapping) ([]byte, error) {
return proto.Marshal(m)
}
// UnmarshalMapping decodes a mapping from binary protobuf data.
func UnmarshalMapping(data []byte, m *chronograf.Mapping) error {
var pb Mapping
if err := UnmarshalMappingPB(data, &pb); err != nil {
return err
}
m.Provider = pb.Provider
m.Scheme = pb.Scheme
m.ProviderOrganization = pb.ProviderOrganization
m.Organization = pb.Organization
m.ID = pb.ID
return nil
}
// UnmarshalMappingPB decodes a mapping from binary protobuf data.
func UnmarshalMappingPB(data []byte, m *Mapping) error {
return proto.Unmarshal(data, m)
}

View File

@ -1,6 +1,5 @@
// Code generated by protoc-gen-gogo.
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: internal.proto
// DO NOT EDIT!
/*
Package internal is a generated protocol buffer package.
@ -13,6 +12,7 @@ It has these top-level messages:
Dashboard
DashboardCell
Color
Legend
Axis
Template
TemplateValue
@ -26,6 +26,7 @@ It has these top-level messages:
AlertRule
User
Role
Mapping
Organization
Config
AuthConfig
@ -219,6 +220,7 @@ type DashboardCell struct {
ID string `protobuf:"bytes,8,opt,name=ID,proto3" json:"ID,omitempty"`
Axes map[string]*Axis `protobuf:"bytes,9,rep,name=axes" json:"axes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value"`
Colors []*Color `protobuf:"bytes,10,rep,name=colors" json:"colors,omitempty"`
Legend *Legend `protobuf:"bytes,11,opt,name=legend" json:"legend,omitempty"`
}
func (m *DashboardCell) Reset() { *m = DashboardCell{} }
@ -296,6 +298,13 @@ func (m *DashboardCell) GetColors() []*Color {
return nil
}
func (m *DashboardCell) GetLegend() *Legend {
if m != nil {
return m.Legend
}
return nil
}
type Color struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Type string `protobuf:"bytes,2,opt,name=Type,proto3" json:"Type,omitempty"`
@ -344,6 +353,30 @@ func (m *Color) GetValue() string {
return ""
}
type Legend struct {
Type string `protobuf:"bytes,1,opt,name=Type,proto3" json:"Type,omitempty"`
Orientation string `protobuf:"bytes,2,opt,name=Orientation,proto3" json:"Orientation,omitempty"`
}
func (m *Legend) Reset() { *m = Legend{} }
func (m *Legend) String() string { return proto.CompactTextString(m) }
func (*Legend) ProtoMessage() {}
func (*Legend) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
func (m *Legend) GetType() string {
if m != nil {
return m.Type
}
return ""
}
func (m *Legend) GetOrientation() string {
if m != nil {
return m.Orientation
}
return ""
}
type Axis struct {
LegacyBounds []int64 `protobuf:"varint,1,rep,packed,name=legacyBounds" json:"legacyBounds,omitempty"`
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
@ -357,7 +390,7 @@ type Axis struct {
func (m *Axis) Reset() { *m = Axis{} }
func (m *Axis) String() string { return proto.CompactTextString(m) }
func (*Axis) ProtoMessage() {}
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} }
func (*Axis) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
func (m *Axis) GetLegacyBounds() []int64 {
if m != nil {
@ -420,7 +453,7 @@ type Template struct {
func (m *Template) Reset() { *m = Template{} }
func (m *Template) String() string { return proto.CompactTextString(m) }
func (*Template) ProtoMessage() {}
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} }
func (*Template) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
func (m *Template) GetID() string {
if m != nil {
@ -473,7 +506,7 @@ type TemplateValue struct {
func (m *TemplateValue) Reset() { *m = TemplateValue{} }
func (m *TemplateValue) String() string { return proto.CompactTextString(m) }
func (*TemplateValue) ProtoMessage() {}
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} }
func (*TemplateValue) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func (m *TemplateValue) GetType() string {
if m != nil {
@ -508,7 +541,7 @@ type TemplateQuery struct {
func (m *TemplateQuery) Reset() { *m = TemplateQuery{} }
func (m *TemplateQuery) String() string { return proto.CompactTextString(m) }
func (*TemplateQuery) ProtoMessage() {}
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} }
func (*TemplateQuery) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
func (m *TemplateQuery) GetCommand() string {
if m != nil {
@ -566,7 +599,7 @@ type Server struct {
func (m *Server) Reset() { *m = Server{} }
func (m *Server) String() string { return proto.CompactTextString(m) }
func (*Server) ProtoMessage() {}
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} }
func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
func (m *Server) GetID() int64 {
if m != nil {
@ -635,7 +668,7 @@ type Layout struct {
func (m *Layout) Reset() { *m = Layout{} }
func (m *Layout) String() string { return proto.CompactTextString(m) }
func (*Layout) ProtoMessage() {}
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} }
func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
func (m *Layout) GetID() string {
if m != nil {
@ -689,7 +722,7 @@ type Cell struct {
func (m *Cell) Reset() { *m = Cell{} }
func (m *Cell) String() string { return proto.CompactTextString(m) }
func (*Cell) ProtoMessage() {}
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} }
func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
func (m *Cell) GetX() int32 {
if m != nil {
@ -783,7 +816,7 @@ type Query struct {
func (m *Query) Reset() { *m = Query{} }
func (m *Query) String() string { return proto.CompactTextString(m) }
func (*Query) ProtoMessage() {}
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{11} }
func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
func (m *Query) GetCommand() string {
if m != nil {
@ -857,7 +890,7 @@ type TimeShift struct {
func (m *TimeShift) Reset() { *m = TimeShift{} }
func (m *TimeShift) String() string { return proto.CompactTextString(m) }
func (*TimeShift) ProtoMessage() {}
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{12} }
func (*TimeShift) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
func (m *TimeShift) GetLabel() string {
if m != nil {
@ -888,7 +921,7 @@ type Range struct {
func (m *Range) Reset() { *m = Range{} }
func (m *Range) String() string { return proto.CompactTextString(m) }
func (*Range) ProtoMessage() {}
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{13} }
func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
func (m *Range) GetUpper() int64 {
if m != nil {
@ -914,7 +947,7 @@ type AlertRule struct {
func (m *AlertRule) Reset() { *m = AlertRule{} }
func (m *AlertRule) String() string { return proto.CompactTextString(m) }
func (*AlertRule) ProtoMessage() {}
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{14} }
func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
func (m *AlertRule) GetID() string {
if m != nil {
@ -956,7 +989,7 @@ type User struct {
func (m *User) Reset() { *m = User{} }
func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{15} }
func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
func (m *User) GetID() uint64 {
if m != nil {
@ -1008,7 +1041,7 @@ type Role struct {
func (m *Role) Reset() { *m = Role{} }
func (m *Role) String() string { return proto.CompactTextString(m) }
func (*Role) ProtoMessage() {}
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{16} }
func (*Role) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
func (m *Role) GetOrganization() string {
if m != nil {
@ -1024,17 +1057,64 @@ func (m *Role) GetName() string {
return ""
}
type Mapping struct {
Provider string `protobuf:"bytes,1,opt,name=Provider,proto3" json:"Provider,omitempty"`
Scheme string `protobuf:"bytes,2,opt,name=Scheme,proto3" json:"Scheme,omitempty"`
ProviderOrganization string `protobuf:"bytes,3,opt,name=ProviderOrganization,proto3" json:"ProviderOrganization,omitempty"`
ID string `protobuf:"bytes,4,opt,name=ID,proto3" json:"ID,omitempty"`
Organization string `protobuf:"bytes,5,opt,name=Organization,proto3" json:"Organization,omitempty"`
}
func (m *Mapping) Reset() { *m = Mapping{} }
func (m *Mapping) String() string { return proto.CompactTextString(m) }
func (*Mapping) ProtoMessage() {}
func (*Mapping) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
func (m *Mapping) GetProvider() string {
if m != nil {
return m.Provider
}
return ""
}
func (m *Mapping) GetScheme() string {
if m != nil {
return m.Scheme
}
return ""
}
func (m *Mapping) GetProviderOrganization() string {
if m != nil {
return m.ProviderOrganization
}
return ""
}
func (m *Mapping) GetID() string {
if m != nil {
return m.ID
}
return ""
}
func (m *Mapping) GetOrganization() string {
if m != nil {
return m.Organization
}
return ""
}
type Organization struct {
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"`
DefaultRole string `protobuf:"bytes,3,opt,name=DefaultRole,proto3" json:"DefaultRole,omitempty"`
Public bool `protobuf:"varint,4,opt,name=Public,proto3" json:"Public,omitempty"`
}
func (m *Organization) Reset() { *m = Organization{} }
func (m *Organization) String() string { return proto.CompactTextString(m) }
func (*Organization) ProtoMessage() {}
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{17} }
func (*Organization) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
func (m *Organization) GetID() string {
if m != nil {
@ -1057,13 +1137,6 @@ func (m *Organization) GetDefaultRole() string {
return ""
}
func (m *Organization) GetPublic() bool {
if m != nil {
return m.Public
}
return false
}
type Config struct {
Auth *AuthConfig `protobuf:"bytes,1,opt,name=Auth" json:"Auth,omitempty"`
}
@ -1071,7 +1144,7 @@ type Config struct {
func (m *Config) Reset() { *m = Config{} }
func (m *Config) String() string { return proto.CompactTextString(m) }
func (*Config) ProtoMessage() {}
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{18} }
func (*Config) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
func (m *Config) GetAuth() *AuthConfig {
if m != nil {
@ -1087,7 +1160,7 @@ type AuthConfig struct {
func (m *AuthConfig) Reset() { *m = AuthConfig{} }
func (m *AuthConfig) String() string { return proto.CompactTextString(m) }
func (*AuthConfig) ProtoMessage() {}
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{19} }
func (*AuthConfig) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{21} }
func (m *AuthConfig) GetSuperAdminNewUsers() bool {
if m != nil {
@ -1104,7 +1177,7 @@ type BuildInfo struct {
func (m *BuildInfo) Reset() { *m = BuildInfo{} }
func (m *BuildInfo) String() string { return proto.CompactTextString(m) }
func (*BuildInfo) ProtoMessage() {}
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{20} }
func (*BuildInfo) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{22} }
func (m *BuildInfo) GetVersion() string {
if m != nil {
@ -1125,6 +1198,7 @@ func init() {
proto.RegisterType((*Dashboard)(nil), "internal.Dashboard")
proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell")
proto.RegisterType((*Color)(nil), "internal.Color")
proto.RegisterType((*Legend)(nil), "internal.Legend")
proto.RegisterType((*Axis)(nil), "internal.Axis")
proto.RegisterType((*Template)(nil), "internal.Template")
proto.RegisterType((*TemplateValue)(nil), "internal.TemplateValue")
@ -1138,6 +1212,7 @@ func init() {
proto.RegisterType((*AlertRule)(nil), "internal.AlertRule")
proto.RegisterType((*User)(nil), "internal.User")
proto.RegisterType((*Role)(nil), "internal.Role")
proto.RegisterType((*Mapping)(nil), "internal.Mapping")
proto.RegisterType((*Organization)(nil), "internal.Organization")
proto.RegisterType((*Config)(nil), "internal.Config")
proto.RegisterType((*AuthConfig)(nil), "internal.AuthConfig")
@ -1147,89 +1222,93 @@ func init() {
func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) }
var fileDescriptorInternal = []byte{
// 1342 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0xdd, 0x8e, 0xe3, 0xc4,
0x12, 0x96, 0xe3, 0x38, 0xb1, 0x2b, 0xb3, 0x7b, 0x56, 0x3e, 0xab, 0xb3, 0x3e, 0x7b, 0xa4, 0xa3,
0x60, 0x81, 0x08, 0x82, 0x1d, 0xd0, 0xac, 0x90, 0x10, 0x02, 0xa4, 0xcc, 0x04, 0x2d, 0xc3, 0xfe,
0xcd, 0x76, 0x76, 0x86, 0x2b, 0xb4, 0xea, 0x38, 0x95, 0xc4, 0x5a, 0xc7, 0x36, 0x6d, 0x7b, 0x26,
0xe6, 0x61, 0x90, 0x90, 0x78, 0x02, 0xc4, 0x3d, 0xb7, 0x88, 0x5b, 0xde, 0x81, 0x57, 0xe0, 0x16,
0x55, 0x77, 0xfb, 0x27, 0x93, 0xb0, 0xda, 0x0b, 0xc4, 0x5d, 0x7f, 0x55, 0xed, 0xea, 0xfa, 0xf9,
0xaa, 0xba, 0x0d, 0x37, 0xc3, 0x38, 0x47, 0x11, 0xf3, 0xe8, 0x30, 0x15, 0x49, 0x9e, 0xb8, 0x76,
0x85, 0xfd, 0xdf, 0x3b, 0xd0, 0x9b, 0x26, 0x85, 0x08, 0xd0, 0xbd, 0x09, 0x9d, 0xd3, 0x89, 0x67,
0x0c, 0x8d, 0x91, 0xc9, 0x3a, 0xa7, 0x13, 0xd7, 0x85, 0xee, 0x13, 0xbe, 0x46, 0xaf, 0x33, 0x34,
0x46, 0x0e, 0x93, 0x6b, 0x92, 0x3d, 0x2f, 0x53, 0xf4, 0x4c, 0x25, 0xa3, 0xb5, 0x7b, 0x17, 0xec,
0xf3, 0x8c, 0xac, 0xad, 0xd1, 0xeb, 0x4a, 0x79, 0x8d, 0x49, 0x77, 0xc6, 0xb3, 0xec, 0x2a, 0x11,
0x73, 0xcf, 0x52, 0xba, 0x0a, 0xbb, 0xb7, 0xc0, 0x3c, 0x67, 0x8f, 0xbc, 0x9e, 0x14, 0xd3, 0xd2,
0xf5, 0xa0, 0x3f, 0xc1, 0x05, 0x2f, 0xa2, 0xdc, 0xeb, 0x0f, 0x8d, 0x91, 0xcd, 0x2a, 0x48, 0x76,
0x9e, 0x63, 0x84, 0x4b, 0xc1, 0x17, 0x9e, 0xad, 0xec, 0x54, 0xd8, 0x3d, 0x04, 0xf7, 0x34, 0xce,
0x30, 0x28, 0x04, 0x4e, 0x5f, 0x86, 0xe9, 0x05, 0x8a, 0x70, 0x51, 0x7a, 0x8e, 0x34, 0xb0, 0x47,
0x43, 0xa7, 0x3c, 0xc6, 0x9c, 0xd3, 0xd9, 0x20, 0x4d, 0x55, 0xd0, 0xf5, 0xe1, 0x60, 0xba, 0xe2,
0x02, 0xe7, 0x53, 0x0c, 0x04, 0xe6, 0xde, 0x40, 0xaa, 0xb7, 0x64, 0xb4, 0xe7, 0xa9, 0x58, 0xf2,
0x38, 0xfc, 0x96, 0xe7, 0x61, 0x12, 0x7b, 0x07, 0x6a, 0x4f, 0x5b, 0x46, 0x59, 0x62, 0x49, 0x84,
0xde, 0x0d, 0x95, 0x25, 0x5a, 0xfb, 0x3f, 0x19, 0xe0, 0x4c, 0x78, 0xb6, 0x9a, 0x25, 0x5c, 0xcc,
0x5f, 0x2b, 0xd7, 0xf7, 0xc0, 0x0a, 0x30, 0x8a, 0x32, 0xcf, 0x1c, 0x9a, 0xa3, 0xc1, 0xd1, 0x9d,
0xc3, 0xba, 0x88, 0xb5, 0x9d, 0x13, 0x8c, 0x22, 0xa6, 0x76, 0xb9, 0x1f, 0x80, 0x93, 0xe3, 0x3a,
0x8d, 0x78, 0x8e, 0x99, 0xd7, 0x95, 0x9f, 0xb8, 0xcd, 0x27, 0xcf, 0xb5, 0x8a, 0x35, 0x9b, 0x76,
0x42, 0xb1, 0x76, 0x43, 0xf1, 0x7f, 0xeb, 0xc0, 0x8d, 0xad, 0xe3, 0xdc, 0x03, 0x30, 0x36, 0xd2,
0x73, 0x8b, 0x19, 0x1b, 0x42, 0xa5, 0xf4, 0xda, 0x62, 0x46, 0x49, 0xe8, 0x4a, 0x72, 0xc3, 0x62,
0xc6, 0x15, 0xa1, 0x95, 0x64, 0x84, 0xc5, 0x8c, 0x95, 0xfb, 0x0e, 0xf4, 0xbf, 0x29, 0x50, 0x84,
0x98, 0x79, 0x96, 0xf4, 0xee, 0x5f, 0x8d, 0x77, 0xcf, 0x0a, 0x14, 0x25, 0xab, 0xf4, 0x94, 0x0d,
0xc9, 0x26, 0x45, 0x0d, 0xb9, 0x26, 0x59, 0x4e, 0xcc, 0xeb, 0x2b, 0x19, 0xad, 0x75, 0x16, 0x15,
0x1f, 0x28, 0x8b, 0x1f, 0x42, 0x97, 0x6f, 0x30, 0xf3, 0x1c, 0x69, 0xff, 0x8d, 0xbf, 0x48, 0xd8,
0xe1, 0x78, 0x83, 0xd9, 0xe7, 0x71, 0x2e, 0x4a, 0x26, 0xb7, 0xbb, 0x6f, 0x43, 0x2f, 0x48, 0xa2,
0x44, 0x64, 0x1e, 0x5c, 0x77, 0xec, 0x84, 0xe4, 0x4c, 0xab, 0xef, 0x3e, 0x00, 0xa7, 0xfe, 0x96,
0xe8, 0xfb, 0x12, 0x4b, 0x99, 0x09, 0x87, 0xd1, 0xd2, 0x7d, 0x13, 0xac, 0x4b, 0x1e, 0x15, 0xaa,
0x8a, 0x83, 0xa3, 0x9b, 0x8d, 0x99, 0xf1, 0x26, 0xcc, 0x98, 0x52, 0x7e, 0xdc, 0xf9, 0xc8, 0xf0,
0x97, 0x60, 0x49, 0xcb, 0x2d, 0x1e, 0x38, 0x15, 0x0f, 0x64, 0x7f, 0x75, 0x5a, 0xfd, 0x75, 0x0b,
0xcc, 0x2f, 0x70, 0xa3, 0x5b, 0x8e, 0x96, 0x35, 0x5b, 0xba, 0x2d, 0xb6, 0xdc, 0x06, 0xeb, 0x42,
0x1e, 0xae, 0xaa, 0xa8, 0x80, 0xff, 0xa3, 0x01, 0x5d, 0x3a, 0x9c, 0x6a, 0x1d, 0xe1, 0x92, 0x07,
0xe5, 0x71, 0x52, 0xc4, 0xf3, 0xcc, 0x33, 0x86, 0xe6, 0xc8, 0x64, 0x5b, 0x32, 0xf7, 0x3f, 0xd0,
0x9b, 0x29, 0x6d, 0x67, 0x68, 0x8e, 0x1c, 0xa6, 0x11, 0x99, 0x8e, 0xf8, 0x0c, 0x23, 0xed, 0x82,
0x02, 0xb4, 0x3b, 0x15, 0xb8, 0x08, 0x37, 0xda, 0x0d, 0x8d, 0x48, 0x9e, 0x15, 0x0b, 0x92, 0x2b,
0x4f, 0x34, 0x22, 0xa7, 0x67, 0x3c, 0xab, 0x8b, 0x4a, 0x6b, 0xb2, 0x9c, 0x05, 0x3c, 0xaa, 0xaa,
0xaa, 0x80, 0xff, 0xb3, 0x41, 0xdd, 0xae, 0x58, 0xba, 0x93, 0xa1, 0xff, 0x82, 0x4d, 0x0c, 0x7e,
0x71, 0xc9, 0x85, 0xce, 0x52, 0x9f, 0xf0, 0x05, 0x17, 0xee, 0xfb, 0xd0, 0x93, 0x29, 0xde, 0xd3,
0x31, 0x95, 0x39, 0x99, 0x15, 0xa6, 0xb7, 0xd5, 0x9c, 0xea, 0xb6, 0x38, 0x55, 0x07, 0x6b, 0xb5,
0x83, 0xbd, 0x07, 0x16, 0x91, 0xb3, 0x94, 0xde, 0xef, 0xb5, 0xac, 0x28, 0xac, 0x76, 0xf9, 0xe7,
0x70, 0x63, 0xeb, 0xc4, 0xfa, 0x24, 0x63, 0xfb, 0xa4, 0x86, 0x2e, 0x8e, 0xa6, 0x07, 0x4d, 0xba,
0x0c, 0x23, 0x0c, 0x72, 0x9c, 0xcb, 0x7c, 0xdb, 0xac, 0xc6, 0xfe, 0xf7, 0x46, 0x63, 0x57, 0x9e,
0x47, 0xb3, 0x2c, 0x48, 0xd6, 0x6b, 0x1e, 0xcf, 0xb5, 0xe9, 0x0a, 0x52, 0xde, 0xe6, 0x33, 0x6d,
0xba, 0x33, 0x9f, 0x11, 0x16, 0xa9, 0xae, 0x60, 0x47, 0xa4, 0xee, 0x10, 0x06, 0x6b, 0xe4, 0x59,
0x21, 0x70, 0x8d, 0x71, 0xae, 0x53, 0xd0, 0x16, 0xb9, 0x77, 0xa0, 0x9f, 0xf3, 0xe5, 0x0b, 0x22,
0xb9, 0xae, 0x64, 0xce, 0x97, 0x0f, 0xb1, 0x74, 0xff, 0x07, 0xce, 0x22, 0xc4, 0x68, 0x2e, 0x55,
0xaa, 0x9c, 0xb6, 0x14, 0x3c, 0xc4, 0xd2, 0xff, 0xc5, 0x80, 0xde, 0x14, 0xc5, 0x25, 0x8a, 0xd7,
0x1a, 0x72, 0xed, 0xcb, 0xc3, 0x7c, 0xc5, 0xe5, 0xd1, 0xdd, 0x7f, 0x79, 0x58, 0xcd, 0xe5, 0x71,
0x1b, 0xac, 0xa9, 0x08, 0x4e, 0x27, 0xd2, 0x23, 0x93, 0x29, 0x40, 0x6c, 0x1c, 0x07, 0x79, 0x78,
0x89, 0xfa, 0x46, 0xd1, 0x68, 0x67, 0xf6, 0xd9, 0x7b, 0x66, 0xdf, 0x77, 0x06, 0xf4, 0x1e, 0xf1,
0x32, 0x29, 0xf2, 0x1d, 0x16, 0x0e, 0x61, 0x30, 0x4e, 0xd3, 0x28, 0x0c, 0xd4, 0xd7, 0x2a, 0xa2,
0xb6, 0x88, 0x76, 0x3c, 0x6e, 0xe5, 0x57, 0xc5, 0xd6, 0x16, 0xd1, 0xb8, 0x38, 0x91, 0xf3, 0x5d,
0x0d, 0xeb, 0xd6, 0xb8, 0x50, 0x63, 0x5d, 0x2a, 0x29, 0x09, 0xe3, 0x22, 0x4f, 0x16, 0x51, 0x72,
0x25, 0xa3, 0xb5, 0x59, 0x8d, 0xfd, 0x5f, 0x3b, 0xd0, 0xfd, 0xa7, 0x66, 0xf2, 0x01, 0x18, 0xa1,
0x2e, 0xb6, 0x11, 0xd6, 0x13, 0xba, 0xdf, 0x9a, 0xd0, 0x1e, 0xf4, 0x4b, 0xc1, 0xe3, 0x25, 0x66,
0x9e, 0x2d, 0xa7, 0x4b, 0x05, 0xa5, 0x46, 0xf6, 0x91, 0x1a, 0xcd, 0x0e, 0xab, 0x60, 0xdd, 0x17,
0xd0, 0xea, 0x8b, 0xf7, 0xf4, 0x14, 0x1f, 0x48, 0x8f, 0xbc, 0xed, 0xb4, 0x5c, 0x1f, 0xde, 0x7f,
0xdf, 0x4c, 0xfe, 0xc3, 0x00, 0xab, 0x6e, 0xaa, 0x93, 0xed, 0xa6, 0x3a, 0x69, 0x9a, 0x6a, 0x72,
0x5c, 0x35, 0xd5, 0xe4, 0x98, 0x30, 0x3b, 0xab, 0x9a, 0x8a, 0x9d, 0x51, 0xb1, 0x1e, 0x88, 0xa4,
0x48, 0x8f, 0x4b, 0x55, 0x55, 0x87, 0xd5, 0x98, 0x98, 0xf8, 0xd5, 0x0a, 0x85, 0x4e, 0xb5, 0xc3,
0x34, 0x22, 0xde, 0x3e, 0x92, 0x03, 0x47, 0x25, 0x57, 0x01, 0xf7, 0x2d, 0xb0, 0x18, 0x25, 0x4f,
0x66, 0x78, 0xab, 0x2e, 0x52, 0xcc, 0x94, 0x96, 0x8c, 0xaa, 0xd7, 0x9b, 0x26, 0x70, 0xf5, 0x96,
0x7b, 0x17, 0x7a, 0xd3, 0x55, 0xb8, 0xc8, 0xab, 0xbb, 0xf0, 0xdf, 0xad, 0x81, 0x15, 0xae, 0x51,
0xea, 0x98, 0xde, 0xe2, 0x3f, 0x03, 0xa7, 0x16, 0x36, 0xee, 0x18, 0x6d, 0x77, 0x5c, 0xe8, 0x9e,
0xc7, 0x61, 0x5e, 0xb5, 0x2e, 0xad, 0x29, 0xd8, 0x67, 0x05, 0x8f, 0xf3, 0x30, 0x2f, 0xab, 0xd6,
0xad, 0xb0, 0x7f, 0x5f, 0xbb, 0x4f, 0xe6, 0xce, 0xd3, 0x14, 0x85, 0x1e, 0x03, 0x0a, 0xc8, 0x43,
0x92, 0x2b, 0x54, 0x13, 0xdc, 0x64, 0x0a, 0xf8, 0x5f, 0x83, 0x33, 0x8e, 0x50, 0xe4, 0xac, 0x88,
0x70, 0xdf, 0xcd, 0xf8, 0xe5, 0xf4, 0xe9, 0x93, 0xca, 0x03, 0x5a, 0x37, 0x2d, 0x6f, 0x5e, 0x6b,
0xf9, 0x87, 0x3c, 0xe5, 0xa7, 0x13, 0xc9, 0x73, 0x93, 0x69, 0xe4, 0xff, 0x60, 0x40, 0x97, 0x66,
0x4b, 0xcb, 0x74, 0xf7, 0x55, 0x73, 0xe9, 0x4c, 0x24, 0x97, 0xe1, 0x1c, 0x45, 0x15, 0x5c, 0x85,
0x65, 0xd2, 0x83, 0x15, 0xd6, 0x17, 0xb0, 0x46, 0xc4, 0x35, 0x7a, 0xea, 0x55, 0xbd, 0xd4, 0xe2,
0x1a, 0x89, 0x99, 0x52, 0xba, 0xff, 0x07, 0x98, 0x16, 0x29, 0x8a, 0xf1, 0x7c, 0x1d, 0xc6, 0xb2,
0xe8, 0x36, 0x6b, 0x49, 0xfc, 0xcf, 0xd4, 0xe3, 0x71, 0x67, 0x42, 0x19, 0xfb, 0x1f, 0x9a, 0xd7,
0x3d, 0xf7, 0xa3, 0xed, 0xef, 0xf6, 0x25, 0x72, 0x27, 0xda, 0x21, 0x0c, 0xf4, 0x4b, 0x5b, 0xbe,
0x5b, 0xf5, 0xb0, 0x6a, 0x89, 0x28, 0xe6, 0xb3, 0x62, 0x16, 0x85, 0x81, 0x8c, 0xd9, 0x66, 0x1a,
0xf9, 0x47, 0xd0, 0x3b, 0x49, 0xe2, 0x45, 0xb8, 0x74, 0x47, 0xd0, 0x1d, 0x17, 0xf9, 0x4a, 0x9e,
0x34, 0x38, 0xba, 0xdd, 0x6a, 0xb4, 0x22, 0x5f, 0xa9, 0x3d, 0x4c, 0xee, 0xf0, 0x3f, 0x01, 0x68,
0x64, 0xf4, 0x7c, 0x6f, 0xa2, 0x7f, 0x82, 0x57, 0x54, 0xa2, 0x4c, 0x5a, 0xb1, 0xd9, 0x1e, 0x8d,
0xff, 0x29, 0x38, 0xc7, 0x45, 0x18, 0xcd, 0x4f, 0xe3, 0x45, 0x42, 0xad, 0x7a, 0x81, 0x22, 0x6b,
0xf2, 0x53, 0x41, 0x72, 0x98, 0xba, 0xb6, 0xe6, 0xac, 0x46, 0xb3, 0x9e, 0xfc, 0x03, 0xba, 0xff,
0x67, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbe, 0x23, 0x76, 0x8f, 0x13, 0x0d, 0x00, 0x00,
// 1406 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x57, 0x5f, 0x8f, 0xdb, 0x44,
0x10, 0x97, 0x63, 0x3b, 0x89, 0x27, 0xd7, 0x52, 0x99, 0x13, 0x35, 0x45, 0x42, 0xc1, 0x02, 0x11,
0x04, 0x3d, 0xd0, 0x55, 0x48, 0x08, 0x41, 0xa5, 0xdc, 0x05, 0x95, 0xa3, 0xd7, 0xf6, 0xba, 0xb9,
0x3b, 0x9e, 0x50, 0xb5, 0x97, 0x4c, 0x12, 0xab, 0x8e, 0x6d, 0xd6, 0xf6, 0x5d, 0xcc, 0x87, 0x41,
0x42, 0x82, 0x2f, 0x80, 0x78, 0xe7, 0x15, 0xf1, 0x41, 0xf8, 0x0a, 0x3c, 0x21, 0xa1, 0xd9, 0x5d,
0xff, 0xc9, 0x25, 0xad, 0xfa, 0x80, 0x78, 0xdb, 0xdf, 0xcc, 0x66, 0x76, 0xfe, 0xfc, 0x66, 0xc6,
0x81, 0x9b, 0x41, 0x94, 0xa1, 0x88, 0x78, 0xb8, 0x97, 0x88, 0x38, 0x8b, 0xdd, 0x6e, 0x89, 0xfd,
0xbf, 0x5a, 0xd0, 0x1e, 0xc7, 0xb9, 0x98, 0xa0, 0x7b, 0x13, 0x5a, 0x47, 0x23, 0xcf, 0xe8, 0x1b,
0x03, 0x93, 0xb5, 0x8e, 0x46, 0xae, 0x0b, 0xd6, 0x63, 0xbe, 0x44, 0xaf, 0xd5, 0x37, 0x06, 0x0e,
0x93, 0x67, 0x92, 0x9d, 0x16, 0x09, 0x7a, 0xa6, 0x92, 0xd1, 0xd9, 0xbd, 0x03, 0xdd, 0xb3, 0x94,
0xac, 0x2d, 0xd1, 0xb3, 0xa4, 0xbc, 0xc2, 0xa4, 0x3b, 0xe1, 0x69, 0x7a, 0x15, 0x8b, 0xa9, 0x67,
0x2b, 0x5d, 0x89, 0xdd, 0x5b, 0x60, 0x9e, 0xb1, 0x63, 0xaf, 0x2d, 0xc5, 0x74, 0x74, 0x3d, 0xe8,
0x8c, 0x70, 0xc6, 0xf3, 0x30, 0xf3, 0x3a, 0x7d, 0x63, 0xd0, 0x65, 0x25, 0x24, 0x3b, 0xa7, 0x18,
0xe2, 0x5c, 0xf0, 0x99, 0xd7, 0x55, 0x76, 0x4a, 0xec, 0xee, 0x81, 0x7b, 0x14, 0xa5, 0x38, 0xc9,
0x05, 0x8e, 0x9f, 0x07, 0xc9, 0x39, 0x8a, 0x60, 0x56, 0x78, 0x8e, 0x34, 0xb0, 0x45, 0x43, 0xaf,
0x3c, 0xc2, 0x8c, 0xd3, 0xdb, 0x20, 0x4d, 0x95, 0xd0, 0xf5, 0x61, 0x67, 0xbc, 0xe0, 0x02, 0xa7,
0x63, 0x9c, 0x08, 0xcc, 0xbc, 0x9e, 0x54, 0xaf, 0xc9, 0xe8, 0xce, 0x13, 0x31, 0xe7, 0x51, 0xf0,
0x03, 0xcf, 0x82, 0x38, 0xf2, 0x76, 0xd4, 0x9d, 0xa6, 0x8c, 0xb2, 0xc4, 0xe2, 0x10, 0xbd, 0x1b,
0x2a, 0x4b, 0x74, 0xf6, 0x7f, 0x33, 0xc0, 0x19, 0xf1, 0x74, 0x71, 0x11, 0x73, 0x31, 0x7d, 0xa5,
0x5c, 0xdf, 0x05, 0x7b, 0x82, 0x61, 0x98, 0x7a, 0x66, 0xdf, 0x1c, 0xf4, 0xf6, 0x6f, 0xef, 0x55,
0x45, 0xac, 0xec, 0x1c, 0x62, 0x18, 0x32, 0x75, 0xcb, 0xfd, 0x04, 0x9c, 0x0c, 0x97, 0x49, 0xc8,
0x33, 0x4c, 0x3d, 0x4b, 0xfe, 0xc4, 0xad, 0x7f, 0x72, 0xaa, 0x55, 0xac, 0xbe, 0xb4, 0x11, 0x8a,
0xbd, 0x19, 0x8a, 0xff, 0x4f, 0x0b, 0x6e, 0xac, 0x3d, 0xe7, 0xee, 0x80, 0xb1, 0x92, 0x9e, 0xdb,
0xcc, 0x58, 0x11, 0x2a, 0xa4, 0xd7, 0x36, 0x33, 0x0a, 0x42, 0x57, 0x92, 0x1b, 0x36, 0x33, 0xae,
0x08, 0x2d, 0x24, 0x23, 0x6c, 0x66, 0x2c, 0xdc, 0x0f, 0xa0, 0xf3, 0x7d, 0x8e, 0x22, 0xc0, 0xd4,
0xb3, 0xa5, 0x77, 0xaf, 0xd5, 0xde, 0x3d, 0xcd, 0x51, 0x14, 0xac, 0xd4, 0x53, 0x36, 0x24, 0x9b,
0x14, 0x35, 0xe4, 0x99, 0x64, 0x19, 0x31, 0xaf, 0xa3, 0x64, 0x74, 0xd6, 0x59, 0x54, 0x7c, 0xa0,
0x2c, 0x7e, 0x0a, 0x16, 0x5f, 0x61, 0xea, 0x39, 0xd2, 0xfe, 0x3b, 0x2f, 0x48, 0xd8, 0xde, 0x70,
0x85, 0xe9, 0x57, 0x51, 0x26, 0x0a, 0x26, 0xaf, 0xbb, 0xef, 0x43, 0x7b, 0x12, 0x87, 0xb1, 0x48,
0x3d, 0xb8, 0xee, 0xd8, 0x21, 0xc9, 0x99, 0x56, 0xbb, 0x03, 0x68, 0x87, 0x38, 0xc7, 0x68, 0x2a,
0x99, 0xd1, 0xdb, 0xbf, 0x55, 0x5f, 0x3c, 0x96, 0x72, 0xa6, 0xf5, 0x77, 0x1e, 0x80, 0x53, 0xbd,
0x42, 0x44, 0x7f, 0x8e, 0x85, 0xcc, 0x99, 0xc3, 0xe8, 0xe8, 0xbe, 0x0b, 0xf6, 0x25, 0x0f, 0x73,
0x55, 0xef, 0xde, 0xfe, 0xcd, 0xda, 0xce, 0x70, 0x15, 0xa4, 0x4c, 0x29, 0x3f, 0x6f, 0x7d, 0x66,
0xf8, 0x73, 0xb0, 0xa5, 0x0f, 0x0d, 0xc6, 0x38, 0x25, 0x63, 0x64, 0x27, 0xb6, 0x1a, 0x9d, 0x78,
0x0b, 0xcc, 0xaf, 0x71, 0xa5, 0x9b, 0x93, 0x8e, 0x15, 0xaf, 0xac, 0x06, 0xaf, 0x76, 0xc1, 0x3e,
0x97, 0x8f, 0xab, 0x7a, 0x2b, 0xe0, 0xdf, 0x87, 0xb6, 0x8a, 0xa1, 0xb2, 0x6c, 0x34, 0x2c, 0xf7,
0xa1, 0xf7, 0x44, 0x04, 0x18, 0x65, 0x8a, 0x29, 0xea, 0xd1, 0xa6, 0xc8, 0xff, 0xd5, 0x00, 0x8b,
0x9c, 0x27, 0x56, 0x85, 0x38, 0xe7, 0x93, 0xe2, 0x20, 0xce, 0xa3, 0x69, 0xea, 0x19, 0x7d, 0x73,
0x60, 0xb2, 0x35, 0x99, 0xfb, 0x06, 0xb4, 0x2f, 0x94, 0xb6, 0xd5, 0x37, 0x07, 0x0e, 0xd3, 0x88,
0x5c, 0x0b, 0xf9, 0x05, 0x86, 0x3a, 0x04, 0x05, 0xe8, 0x76, 0x22, 0x70, 0x16, 0xac, 0x74, 0x18,
0x1a, 0x91, 0x3c, 0xcd, 0x67, 0x24, 0x57, 0x91, 0x68, 0x44, 0x01, 0x5c, 0xf0, 0xb4, 0xa2, 0x0f,
0x9d, 0xc9, 0x72, 0x3a, 0xe1, 0x61, 0xc9, 0x1f, 0x05, 0xfc, 0xdf, 0x0d, 0x9a, 0x2b, 0xaa, 0x1f,
0x36, 0x32, 0xfc, 0x26, 0x74, 0xa9, 0x57, 0x9e, 0x5d, 0x72, 0xa1, 0x03, 0xee, 0x10, 0x3e, 0xe7,
0xc2, 0xfd, 0x18, 0xda, 0xb2, 0x44, 0x5b, 0x7a, 0xb3, 0x34, 0x27, 0xb3, 0xca, 0xf4, 0xb5, 0x8a,
0xbd, 0x56, 0x83, 0xbd, 0x55, 0xb0, 0x76, 0x33, 0xd8, 0xbb, 0x60, 0x53, 0x1b, 0x14, 0xd2, 0xfb,
0xad, 0x96, 0x55, 0xb3, 0xa8, 0x5b, 0xfe, 0x19, 0xdc, 0x58, 0x7b, 0xb1, 0x7a, 0xc9, 0x58, 0x7f,
0xa9, 0xa6, 0x9b, 0xa3, 0xe9, 0x45, 0x33, 0x35, 0xc5, 0x10, 0x27, 0x19, 0x4e, 0x65, 0xbe, 0xbb,
0xac, 0xc2, 0xfe, 0x4f, 0x46, 0x6d, 0x57, 0xbe, 0x47, 0x53, 0x73, 0x12, 0x2f, 0x97, 0x3c, 0x9a,
0x6a, 0xd3, 0x25, 0xa4, 0xbc, 0x4d, 0x2f, 0xb4, 0xe9, 0xd6, 0xf4, 0x82, 0xb0, 0x48, 0x74, 0x05,
0x5b, 0x22, 0x21, 0xee, 0x2c, 0x91, 0xa7, 0xb9, 0xc0, 0x25, 0x46, 0x99, 0x4e, 0x41, 0x53, 0xe4,
0xde, 0x86, 0x4e, 0xc6, 0xe7, 0xcf, 0xa8, 0x49, 0x74, 0x25, 0x33, 0x3e, 0x7f, 0x88, 0x85, 0xfb,
0x16, 0x38, 0xb3, 0x00, 0xc3, 0xa9, 0x54, 0xa9, 0x72, 0x76, 0xa5, 0xe0, 0x21, 0x16, 0xfe, 0x1f,
0x06, 0xb4, 0xc7, 0x28, 0x2e, 0x51, 0xbc, 0xd2, 0x38, 0x6d, 0xae, 0x29, 0xf3, 0x25, 0x6b, 0xca,
0xda, 0xbe, 0xa6, 0xec, 0x7a, 0x4d, 0xed, 0x82, 0x3d, 0x16, 0x93, 0xa3, 0x91, 0xf4, 0xc8, 0x64,
0x0a, 0x10, 0x1b, 0x87, 0x93, 0x2c, 0xb8, 0x44, 0xbd, 0xbb, 0x34, 0xda, 0x98, 0xb2, 0xdd, 0x2d,
0x53, 0xf6, 0x47, 0x03, 0xda, 0xc7, 0xbc, 0x88, 0xf3, 0x6c, 0x83, 0x85, 0x7d, 0xe8, 0x0d, 0x93,
0x24, 0x0c, 0x26, 0x6b, 0x9d, 0xd7, 0x10, 0xd1, 0x8d, 0x47, 0x8d, 0xfc, 0xaa, 0xd8, 0x9a, 0x22,
0x1a, 0x37, 0x87, 0x72, 0x93, 0xa8, 0xb5, 0xd0, 0x18, 0x37, 0x6a, 0x81, 0x48, 0x25, 0x25, 0x61,
0x98, 0x67, 0xf1, 0x2c, 0x8c, 0xaf, 0x64, 0xb4, 0x5d, 0x56, 0x61, 0xff, 0xcf, 0x16, 0x58, 0xff,
0xd7, 0xf4, 0xdf, 0x01, 0x23, 0xd0, 0xc5, 0x36, 0x82, 0x6a, 0x17, 0x74, 0x1a, 0xbb, 0xc0, 0x83,
0x4e, 0x21, 0x78, 0x34, 0xc7, 0xd4, 0xeb, 0xca, 0xe9, 0x52, 0x42, 0xa9, 0x91, 0x7d, 0xa4, 0x96,
0x80, 0xc3, 0x4a, 0x58, 0xf5, 0x05, 0x34, 0xfa, 0xe2, 0x23, 0xbd, 0x2f, 0x7a, 0xd2, 0x23, 0x6f,
0x3d, 0x2d, 0xd7, 0xd7, 0xc4, 0x7f, 0x37, 0xd3, 0xff, 0x36, 0xc0, 0xae, 0x9a, 0xea, 0x70, 0xbd,
0xa9, 0x0e, 0xeb, 0xa6, 0x1a, 0x1d, 0x94, 0x4d, 0x35, 0x3a, 0x20, 0xcc, 0x4e, 0xca, 0xa6, 0x62,
0x27, 0x54, 0xac, 0x07, 0x22, 0xce, 0x93, 0x83, 0x42, 0x55, 0xd5, 0x61, 0x15, 0x26, 0x26, 0x7e,
0xbb, 0x40, 0xa1, 0x53, 0xed, 0x30, 0x8d, 0x88, 0xb7, 0xc7, 0x72, 0xe0, 0xa8, 0xe4, 0x2a, 0xe0,
0xbe, 0x07, 0x36, 0xa3, 0xe4, 0xc9, 0x0c, 0xaf, 0xd5, 0x45, 0x8a, 0x99, 0xd2, 0x92, 0x51, 0xf5,
0x9d, 0xa8, 0x09, 0x5c, 0x7e, 0x35, 0x7e, 0x08, 0xed, 0xf1, 0x22, 0x98, 0x65, 0xe5, 0xd6, 0x7d,
0xbd, 0x31, 0xb0, 0x82, 0x25, 0x4a, 0x1d, 0xd3, 0x57, 0xfc, 0xa7, 0xe0, 0x54, 0xc2, 0xda, 0x1d,
0xa3, 0xe9, 0x8e, 0x0b, 0xd6, 0x59, 0x14, 0x64, 0x65, 0xeb, 0xd2, 0x99, 0x82, 0x7d, 0x9a, 0xf3,
0x28, 0x0b, 0xb2, 0xa2, 0x6c, 0xdd, 0x12, 0xfb, 0xf7, 0xb4, 0xfb, 0x64, 0xee, 0x2c, 0x49, 0x50,
0xe8, 0x31, 0xa0, 0x80, 0x7c, 0x24, 0xbe, 0x42, 0x35, 0xc1, 0x4d, 0xa6, 0x80, 0xff, 0x1d, 0x38,
0xc3, 0x10, 0x45, 0xc6, 0xf2, 0x10, 0xb7, 0x6d, 0xd6, 0x6f, 0xc6, 0x4f, 0x1e, 0x97, 0x1e, 0xd0,
0xb9, 0x6e, 0x79, 0xf3, 0x5a, 0xcb, 0x3f, 0xe4, 0x09, 0x3f, 0x1a, 0x49, 0x9e, 0x9b, 0x4c, 0x23,
0xff, 0x67, 0x03, 0x2c, 0x9a, 0x2d, 0x0d, 0xd3, 0xd6, 0xcb, 0xe6, 0xd2, 0x89, 0x88, 0x2f, 0x83,
0x29, 0x8a, 0x32, 0xb8, 0x12, 0xcb, 0xa4, 0x4f, 0x16, 0x58, 0x2d, 0x70, 0x8d, 0x88, 0x6b, 0xf4,
0x51, 0x59, 0xf6, 0x52, 0x83, 0x6b, 0x24, 0x66, 0x4a, 0xe9, 0xbe, 0x0d, 0x30, 0xce, 0x13, 0x14,
0xc3, 0xe9, 0x32, 0x88, 0x64, 0xd1, 0xbb, 0xac, 0x21, 0xf1, 0xef, 0xab, 0xcf, 0xd4, 0x8d, 0x09,
0x65, 0x6c, 0xff, 0xa4, 0xbd, 0xee, 0xb9, 0xff, 0x8b, 0x01, 0x9d, 0x47, 0x3c, 0x49, 0x82, 0x68,
0xbe, 0x16, 0x85, 0xf1, 0xc2, 0x28, 0x5a, 0x6b, 0x51, 0xec, 0xc3, 0x6e, 0x79, 0x67, 0xed, 0x7d,
0x95, 0x85, 0xad, 0x3a, 0x9d, 0x51, 0xab, 0x2a, 0xd6, 0xab, 0x7c, 0xc3, 0x9e, 0xae, 0xdf, 0xd9,
0x56, 0xf0, 0x8d, 0xaa, 0xf4, 0xa1, 0xa7, 0xff, 0x7b, 0xc8, 0x2f, 0x79, 0x3d, 0x54, 0x1b, 0x22,
0x7f, 0x1f, 0xda, 0x87, 0x71, 0x34, 0x0b, 0xe6, 0xee, 0x00, 0xac, 0x61, 0x9e, 0x2d, 0xa4, 0xc5,
0xde, 0xfe, 0x6e, 0xa3, 0xf1, 0xf3, 0x6c, 0xa1, 0xee, 0x30, 0x79, 0xc3, 0xff, 0x02, 0xa0, 0x96,
0xd1, 0x1f, 0x97, 0xba, 0x1a, 0x8f, 0xf1, 0x8a, 0x28, 0x93, 0x4a, 0x2b, 0x5d, 0xb6, 0x45, 0xe3,
0x7f, 0x09, 0xce, 0x41, 0x1e, 0x84, 0xd3, 0xa3, 0x68, 0x16, 0xd3, 0xe8, 0x38, 0x47, 0x91, 0xd6,
0xf5, 0x2a, 0x21, 0xa5, 0x9b, 0xa6, 0x48, 0xd5, 0x43, 0x1a, 0x5d, 0xb4, 0xe5, 0x7f, 0xbf, 0x7b,
0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xfe, 0xe9, 0xd1, 0x8f, 0x0d, 0x0e, 0x00, 0x00,
}

View File

@ -36,6 +36,7 @@ message DashboardCell {
string ID = 8; // id is the unique id of the dashboard. MIGRATED FIELD added in 1.2.0-beta6
map<string, Axis> axes = 9; // Axes represent the graphical viewport for a cell's visualizations
repeated Color colors = 10; // Colors represent encoding data values to color
Legend legend = 11; // Legend is summary information for a cell
}
message Color {
@ -46,6 +47,11 @@ message Color {
string Value = 5; // Value is the data value mapped to this color
}
message Legend {
string Type = 1; // Type is how the legend is used
string Orientation = 2; // Orientation is the location of the legend on the cell
}
message Axis {
repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds.
@ -153,15 +159,22 @@ message User {
}
message Role {
string Organization = 1; // Organization is the ID of the organization that this user has a role in
string Name = 2; // Name is the name of the role of this user in the respective organization
string Organization = 1; // Organization is the ID of the organization that this user has a role in
string Name = 2; // Name is the name of the role of this user in the respective organization
}
message Mapping {
string Provider = 1; // Provider is the provider that certifies and issues this user's authentication, e.g. GitHub
string Scheme = 2; // Scheme is the scheme used to perform this user's authentication, e.g. OAuth2 or LDAP
string ProviderOrganization = 3; // ProviderOrganization is the group or organizations that you are a part of in an auth provider
string ID = 4; // ID is the unique ID for the mapping
string Organization = 5; // Organization is the organization ID that resource belongs to
}
message Organization {
string ID = 1; // ID is the unique ID of the organization
string Name = 2; // Name is the organization's name
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
bool Public = 4; // Public specifies that users must be explicitly added to the organization
string ID = 1; // ID is the unique ID of the organization
string Name = 2; // Name is the organization's name
string DefaultRole = 3; // DefaultRole is the name of the role that is the default for any users added to the organization
}
message Config {
@ -173,8 +186,8 @@ message AuthConfig {
}
message BuildInfo {
string Version = 1; // Version is a descriptive git SHA identifier
string Commit = 2; // Commit is an abbreviated SHA
string Version = 1; // Version is a descriptive git SHA identifier
string Commit = 2; // Commit is an abbreviated SHA
}
// The following is a vim modeline, it autoconfigures vim to have the

View File

@ -251,6 +251,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
Type: "line",
},
},
@ -301,6 +305,10 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
Type: "line",
},
},

128
bolt/mapping.go Normal file
View File

@ -0,0 +1,128 @@
package bolt
import (
"context"
"fmt"
"github.com/boltdb/bolt"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt/internal"
)
// Ensure MappingsStore implements chronograf.MappingsStore.
var _ chronograf.MappingsStore = &MappingsStore{}
var (
// MappingsBucket is the bucket where organizations are stored.
MappingsBucket = []byte("MappingsV1")
)
// MappingsStore uses bolt to store and retrieve Mappings
type MappingsStore struct {
client *Client
}
// Migrate sets the default organization at runtime
func (s *MappingsStore) Migrate(ctx context.Context) error {
return nil
}
// Add creates a new Mapping in the MappingsStore
func (s *MappingsStore) Add(ctx context.Context, o *chronograf.Mapping) (*chronograf.Mapping, error) {
err := s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(MappingsBucket)
seq, err := b.NextSequence()
if err != nil {
return err
}
o.ID = fmt.Sprintf("%d", seq)
v, err := internal.MarshalMapping(o)
if err != nil {
return err
}
return b.Put([]byte(o.ID), v)
})
if err != nil {
return nil, err
}
return o, nil
}
// All returns all known organizations
func (s *MappingsStore) All(ctx context.Context) ([]chronograf.Mapping, error) {
var mappings []chronograf.Mapping
err := s.each(ctx, func(m *chronograf.Mapping) {
mappings = append(mappings, *m)
})
if err != nil {
return nil, err
}
return mappings, nil
}
// Delete the organization from MappingsStore
func (s *MappingsStore) Delete(ctx context.Context, o *chronograf.Mapping) error {
_, err := s.get(ctx, o.ID)
if err != nil {
return err
}
if err := s.client.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket(MappingsBucket).Delete([]byte(o.ID))
}); err != nil {
return err
}
return nil
}
func (s *MappingsStore) get(ctx context.Context, id string) (*chronograf.Mapping, error) {
var o chronograf.Mapping
err := s.client.db.View(func(tx *bolt.Tx) error {
v := tx.Bucket(MappingsBucket).Get([]byte(id))
if v == nil {
return chronograf.ErrMappingNotFound
}
return internal.UnmarshalMapping(v, &o)
})
if err != nil {
return nil, err
}
return &o, nil
}
func (s *MappingsStore) each(ctx context.Context, fn func(*chronograf.Mapping)) error {
return s.client.db.View(func(tx *bolt.Tx) error {
return tx.Bucket(MappingsBucket).ForEach(func(k, v []byte) error {
var m chronograf.Mapping
if err := internal.UnmarshalMapping(v, &m); err != nil {
return err
}
fn(&m)
return nil
})
})
}
// Get returns a Mapping if the id exists.
func (s *MappingsStore) Get(ctx context.Context, id string) (*chronograf.Mapping, error) {
return s.get(ctx, id)
}
// Update the organization in MappingsStore
func (s *MappingsStore) Update(ctx context.Context, o *chronograf.Mapping) error {
return s.client.db.Update(func(tx *bolt.Tx) error {
if v, err := internal.MarshalMapping(o); err != nil {
return err
} else if err := tx.Bucket(MappingsBucket).Put([]byte(o.ID), v); err != nil {
return err
}
return nil
})
}

483
bolt/mapping_test.go Normal file
View File

@ -0,0 +1,483 @@
package bolt_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/influxdata/chronograf"
)
var mappingCmpOptions = cmp.Options{
cmpopts.IgnoreFields(chronograf.Mapping{}, "ID"),
cmpopts.EquateEmpty(),
}
func TestMappingStore_Add(t *testing.T) {
type fields struct {
mappings []*chronograf.Mapping
}
type args struct {
mapping *chronograf.Mapping
}
type wants struct {
mapping *chronograf.Mapping
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "default with wildcards",
args: args{
mapping: &chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
},
wants: wants{
mapping: &chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
},
},
{
name: "simple",
args: args{
mapping: &chronograf.Mapping{
Organization: "default",
Provider: "github",
Scheme: "oauth2",
ProviderOrganization: "idk",
},
},
wants: wants{
mapping: &chronograf.Mapping{
Organization: "default",
Provider: "github",
Scheme: "oauth2",
ProviderOrganization: "idk",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.MappingsStore
ctx := context.Background()
for _, mapping := range tt.fields.mappings {
// YOLO database prepopulation
_, _ = s.Add(ctx, mapping)
}
tt.args.mapping, err = s.Add(ctx, tt.args.mapping)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("MappingsStore.Add() error = %v, want error %v", err, tt.wants.err)
return
}
got, err := s.Get(ctx, tt.args.mapping.ID)
if err != nil {
t.Fatalf("failed to get mapping: %v", err)
return
}
if diff := cmp.Diff(got, tt.wants.mapping, mappingCmpOptions...); diff != "" {
t.Errorf("MappingStore.Add():\n-got/+want\ndiff %s", diff)
return
}
})
}
}
func TestMappingStore_All(t *testing.T) {
type fields struct {
mappings []*chronograf.Mapping
}
type args struct {
}
type wants struct {
mappings []chronograf.Mapping
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "simple",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
wants: wants{
mappings: []chronograf.Mapping{
chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.MappingsStore
ctx := context.Background()
for _, mapping := range tt.fields.mappings {
// YOLO database prepopulation
_, _ = s.Add(ctx, mapping)
}
got, err := s.All(ctx)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("MappingsStore.All() error = %v, want error %v", err, tt.wants.err)
return
}
if diff := cmp.Diff(got, tt.wants.mappings, mappingCmpOptions...); diff != "" {
t.Errorf("MappingStore.All():\n-got/+want\ndiff %s", diff)
return
}
})
}
}
func TestMappingStore_Delete(t *testing.T) {
type fields struct {
mappings []*chronograf.Mapping
}
type args struct {
mapping *chronograf.Mapping
}
type wants struct {
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "simple",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
args: args{
mapping: &chronograf.Mapping{
ID: "1",
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
},
wants: wants{
err: nil,
},
},
{
name: "mapping not found",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
args: args{
mapping: &chronograf.Mapping{
ID: "0",
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
},
wants: wants{
err: chronograf.ErrMappingNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.MappingsStore
ctx := context.Background()
for _, mapping := range tt.fields.mappings {
// YOLO database prepopulation
_, _ = s.Add(ctx, mapping)
}
err = s.Delete(ctx, tt.args.mapping)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("MappingsStore.Delete() error = %v, want error %v", err, tt.wants.err)
return
}
})
}
}
func TestMappingStore_Get(t *testing.T) {
type fields struct {
mappings []*chronograf.Mapping
}
type args struct {
mappingID string
}
type wants struct {
mapping *chronograf.Mapping
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "simple",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
args: args{
mappingID: "1",
},
wants: wants{
mapping: &chronograf.Mapping{
ID: "1",
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
err: nil,
},
},
{
name: "mapping not found",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
args: args{
mappingID: "0",
},
wants: wants{
err: chronograf.ErrMappingNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.MappingsStore
ctx := context.Background()
for _, mapping := range tt.fields.mappings {
// YOLO database prepopulation
_, _ = s.Add(ctx, mapping)
}
got, err := s.Get(ctx, tt.args.mappingID)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("MappingsStore.Get() error = %v, want error %v", err, tt.wants.err)
return
}
if diff := cmp.Diff(got, tt.wants.mapping, mappingCmpOptions...); diff != "" {
t.Errorf("MappingStore.Get():\n-got/+want\ndiff %s", diff)
return
}
})
}
}
func TestMappingStore_Update(t *testing.T) {
type fields struct {
mappings []*chronograf.Mapping
}
type args struct {
mapping *chronograf.Mapping
}
type wants struct {
mapping *chronograf.Mapping
err error
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "simple",
fields: fields{
mappings: []*chronograf.Mapping{
&chronograf.Mapping{
Organization: "default",
Provider: "*",
Scheme: "*",
ProviderOrganization: "*",
},
&chronograf.Mapping{
Organization: "0",
Provider: "google",
Scheme: "ldap",
ProviderOrganization: "*",
},
},
},
args: args{
mapping: &chronograf.Mapping{
ID: "1",
Organization: "default",
Provider: "cool",
Scheme: "it",
ProviderOrganization: "works",
},
},
wants: wants{
mapping: &chronograf.Mapping{
ID: "1",
Organization: "default",
Provider: "cool",
Scheme: "it",
ProviderOrganization: "works",
},
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewTestClient()
if err != nil {
t.Fatal(err)
}
defer client.Close()
s := client.MappingsStore
ctx := context.Background()
for _, mapping := range tt.fields.mappings {
// YOLO database prepopulation
_, _ = s.Add(ctx, mapping)
}
err = s.Update(ctx, tt.args.mapping)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("MappingsStore.Update() error = %v, want error %v", err, tt.wants.err)
return
}
if diff := cmp.Diff(tt.args.mapping, tt.wants.mapping, mappingCmpOptions...); diff != "" {
t.Errorf("MappingStore.Update():\n-got/+want\ndiff %s", diff)
return
}
})
}
}

View File

@ -25,8 +25,6 @@ const (
DefaultOrganizationName string = "Default"
// DefaultOrganizationRole is the DefaultRole for the Default organization
DefaultOrganizationRole string = "member"
// DefaultOrganizationPublic is the Public setting for the Default organization.
DefaultOrganizationPublic bool = true
)
// OrganizationsStore uses bolt to store and retrieve Organizations
@ -45,7 +43,14 @@ func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
ID: string(DefaultOrganizationID),
Name: DefaultOrganizationName,
DefaultRole: DefaultOrganizationRole,
Public: DefaultOrganizationPublic,
}
m := chronograf.Mapping{
ID: string(DefaultOrganizationID),
Organization: string(DefaultOrganizationID),
Provider: chronograf.MappingWildcard,
Scheme: chronograf.MappingWildcard,
ProviderOrganization: chronograf.MappingWildcard,
}
return s.client.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(OrganizationsBucket)
@ -59,6 +64,17 @@ func (s *OrganizationsStore) CreateDefault(ctx context.Context) error {
return err
}
b = tx.Bucket(MappingsBucket)
v = b.Get(DefaultOrganizationID)
if v != nil {
return nil
}
if v, err := internal.MarshalMapping(&m); err != nil {
return err
} else if err := b.Put(DefaultOrganizationID, v); err != nil {
return err
}
return nil
})
}
@ -189,6 +205,18 @@ func (s *OrganizationsStore) Delete(ctx context.Context, o *chronograf.Organizat
}
}
mappings, err := s.client.MappingsStore.All(ctx)
if err != nil {
return err
}
for _, mapping := range mappings {
if mapping.Organization == o.ID {
if err := s.client.MappingsStore.Delete(ctx, &mapping); err != nil {
return err
}
}
}
return nil
}

View File

@ -170,12 +170,10 @@ func TestOrganizationsStore_All(t *testing.T) {
{
Name: "EE - Evil Empire",
DefaultRole: roles.MemberRoleName,
Public: true,
},
{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
},
},
@ -183,17 +181,14 @@ func TestOrganizationsStore_All(t *testing.T) {
{
Name: "EE - Evil Empire",
DefaultRole: roles.MemberRoleName,
Public: true,
},
{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
{
Name: bolt.DefaultOrganizationName,
DefaultRole: bolt.DefaultOrganizationRole,
Public: bolt.DefaultOrganizationPublic,
},
},
addFirst: true,
@ -316,52 +311,63 @@ func TestOrganizationsStore_Update(t *testing.T) {
addFirst: true,
},
{
name: "Update organization name, role, public",
name: "Update organization name, role",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: false,
},
updates: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
DefaultRole: roles.AdminRoleName,
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
DefaultRole: roles.AdminRoleName,
},
addFirst: true,
},
{
name: "Update organization name and public",
name: "Update organization name",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
DefaultRole: roles.EditorRoleName,
Public: false,
},
updates: &chronograf.Organization{
Name: "The Bad Place",
Public: true,
Name: "The Bad Place",
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
DefaultRole: roles.EditorRoleName,
Public: true,
},
addFirst: true,
},
{
name: "Update organization name - organization already exists",
name: "Update organization name",
fields: fields{},
args: args{
ctx: context.Background(),
initial: &chronograf.Organization{
Name: "The Good Place",
},
updates: &chronograf.Organization{
Name: "The Bad Place",
},
},
want: &chronograf.Organization{
Name: "The Bad Place",
},
addFirst: true,
},
{
name: "Update organization name - name already taken",
fields: fields{
orgs: []chronograf.Organization{
{
@ -409,10 +415,6 @@ func TestOrganizationsStore_Update(t *testing.T) {
tt.args.initial.DefaultRole = tt.args.updates.DefaultRole
}
if tt.args.updates.Public != tt.args.initial.Public {
tt.args.initial.Public = tt.args.updates.Public
}
if err := s.Update(tt.args.ctx, tt.args.initial); (err != nil) != tt.wantErr {
t.Errorf("%q. OrganizationsStore.Update() error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
@ -618,7 +620,6 @@ func TestOrganizationsStore_DefaultOrganization(t *testing.T) {
ID: string(bolt.DefaultOrganizationID),
Name: bolt.DefaultOrganizationName,
DefaultRole: bolt.DefaultOrganizationRole,
Public: bolt.DefaultOrganizationPublic,
},
wantErr: false,
},

View File

@ -25,8 +25,12 @@ const (
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'")
ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'")
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
ErrInvalidLegend = Error("Invalid legend. Both type and orientation must be set")
ErrInvalidLegendType = Error("Invalid legend type. Valid legend type is 'static'")
ErrInvalidLegendOrient = Error("Invalid orientation type. Valid orientation types are 'top', 'bottom', 'right', 'left'")
ErrUserAlreadyExists = Error("user already exists")
ErrOrganizationNotFound = Error("organization not found")
ErrMappingNotFound = Error("mapping not found")
ErrOrganizationAlreadyExists = Error("organization already exists")
ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization")
ErrConfigNotFound = Error("cannot find configuration")
@ -398,7 +402,7 @@ type User struct {
Name string `json:"name"`
Passwd string `json:"password,omitempty"`
Permissions Permissions `json:"permissions,omitempty"`
Roles []Role `json:"roles,omitempty"`
Roles []Role `json:"roles"`
Provider string `json:"provider,omitempty"`
Scheme string `json:"scheme,omitempty"`
SuperAdmin bool `json:"superAdmin,omitempty"`
@ -499,6 +503,12 @@ type CellColor struct {
Value string `json:"value"` // Value is the data value mapped to this color
}
// Legend represents the encoding of data into a legend
type Legend struct {
Type string `json:"type,omitempty"`
Orientation string `json:"orientation,omitempty"`
}
// DashboardCell holds visual and query information for a cell
type DashboardCell struct {
ID string `json:"i"`
@ -511,6 +521,7 @@ type DashboardCell struct {
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
CellColors []CellColor `json:"colors"`
Legend Legend `json:"legend"`
}
// DashboardsStore is the storage and retrieval of dashboards
@ -563,15 +574,52 @@ type LayoutsStore interface {
Update(context.Context, Layout) error
}
// MappingWildcard is the wildcard value for mappings
const MappingWildcard string = "*"
// A Mapping is the structure that is used to determine a users
// role within an organization. The high level idea is to grant
// certain roles to certain users without them having to be given
// explicit role within the organization.
//
// One can think of a mapping like so:
// Provider:Scheme:Group -> Organization
// github:oauth2:influxdata -> Happy
// beyondcorp:ldap:influxdata -> TheBillHilliettas
//
// Any of Provider, Scheme, or Group may be provided as a wildcard *
// github:oauth2:* -> MyOrg
// *:*:* -> AllOrg
type Mapping struct {
ID string `json:"id"`
Organization string `json:"organizationId"`
Provider string `json:"provider"`
Scheme string `json:"scheme"`
ProviderOrganization string `json:"providerOrganization"`
}
// MappingsStore is the storage and retrieval of Mappings
type MappingsStore interface {
// Add creates a new Mapping.
// The Created mapping is returned back to the user with the
// ID field populated.
Add(context.Context, *Mapping) (*Mapping, error)
// All lists all Mapping in the MappingsStore
All(context.Context) ([]Mapping, error)
// Delete removes an Mapping from the MappingsStore
Delete(context.Context, *Mapping) error
// Get retrieves an Mapping from the MappingsStore
Get(context.Context, string) (*Mapping, error)
// Update updates an Mapping in the MappingsStore
Update(context.Context, *Mapping) error
}
// Organization is a group of resources under a common name
type Organization struct {
ID string `json:"id"`
Name string `json:"name"`
// DefaultRole is the name of the role that is the default for any users added to the organization
DefaultRole string `json:"defaultRole,omitempty"`
// Public specifies whether users must be explicitly added to the organization.
// It is currently only used by the default organization, but that may change in the future.
Public bool `json:"public"`
}
// OrganizationQuery represents the attributes that a organization may be retrieved by.
@ -610,7 +658,6 @@ type OrganizationsStore interface {
}
// AuthConfig is the global application config section for auth parameters
type AuthConfig struct {
// SuperAdminNewUsers should be true by default to give a seamless upgrade to
// 1.4.0 for legacy users. It means that all new users will by default receive
@ -648,7 +695,7 @@ type BuildStore interface {
Update(context.Context, BuildInfo) error
}
// Environement is the set of front-end exposed environment variables
// Environment is the set of front-end exposed environment variables
// that were set on the server
type Environment struct {
TelegrafSystemInterval time.Duration `json:"telegrafSystemInterval"`

View File

@ -3,7 +3,7 @@ machine:
services:
- docker
environment:
DOCKER_TAG: chronograf-20171027
DOCKER_TAG: chronograf-20180207
dependencies:
override:
@ -31,6 +31,7 @@ deployment:
--upload
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
@ -52,6 +53,7 @@ deployment:
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
@ -75,6 +77,7 @@ deployment:
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}

120
cmd/chronoctl/add.go Normal file
View File

@ -0,0 +1,120 @@
package main
import (
"context"
"strings"
"github.com/influxdata/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)
}

41
cmd/chronoctl/list.go Normal file
View File

@ -0,0 +1,41 @@
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)
}

27
cmd/chronoctl/main.go Normal file
View File

@ -0,0 +1,27 @@
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)
}
}
}

44
cmd/chronoctl/util.go Normal file
View File

@ -0,0 +1,44 @@
package main
import (
"context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt"
"github.com/influxdata/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, ","))
}

View File

@ -3,7 +3,7 @@
VERSION ?= $(shell git describe --always --tags)
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
GDM := $(shell command -v gdm 2> /dev/null)
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
GOBINDATA := $(shell go list -f {{.Root}} github.com/kevinburke/go-bindata 2> /dev/null)
YARN := $(shell command -v yarn 2> /dev/null)
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go')
@ -63,7 +63,7 @@ ifndef GDM
endif
ifndef GOBINDATA
@echo "Installing go-bindata"
go get -u github.com/jteeuwen/go-bindata/...
go get -u github.com/kevinburke/go-bindata/...
endif
gdm restore
@touch .godep

View File

@ -51,13 +51,13 @@ type Client struct {
}
// NewClientWithTimeSeries initializes a Client with a known set of TimeSeries.
func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.Authorizer, tls bool, series ...chronograf.TimeSeries) (*Client, error) {
func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.Authorizer, tls, insecure bool, series ...chronograf.TimeSeries) (*Client, error) {
metaURL, err := parseMetaURL(mu, tls)
if err != nil {
return nil, err
}
ctrl := NewMetaClient(metaURL, authorizer)
ctrl := NewMetaClient(metaURL, insecure, authorizer)
c := &Client{
Ctrl: ctrl,
UsersStore: &UserStore{
@ -85,13 +85,13 @@ func NewClientWithTimeSeries(lg chronograf.Logger, mu string, authorizer influx.
// varieties. TLS is used when the URL contains "https" or when the TLS
// parameter is set. authorizer will add the correct `Authorization` headers
// on the out-bound request.
func NewClientWithURL(mu string, authorizer influx.Authorizer, tls bool, lg chronograf.Logger) (*Client, error) {
func NewClientWithURL(mu string, authorizer influx.Authorizer, tls bool, insecure bool, lg chronograf.Logger) (*Client, error) {
metaURL, err := parseMetaURL(mu, tls)
if err != nil {
return nil, err
}
ctrl := NewMetaClient(metaURL, authorizer)
ctrl := NewMetaClient(metaURL, insecure, authorizer)
return &Client{
Ctrl: ctrl,
UsersStore: &UserStore{

View File

@ -84,6 +84,7 @@ func Test_Enterprise_AdvancesDataNodes(t *testing.T) {
Password: "thelake",
},
false,
false,
chronograf.TimeSeries(m1),
chronograf.TimeSeries(m2))
if err != nil {
@ -114,23 +115,53 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
t.Parallel()
urls := []struct {
url string
username string
password string
tls bool
shouldErr bool
name string
url string
username string
password string
tls bool
insecureSkipVerify bool
wantErr bool
}{
{"http://localhost:8086", "", "", false, false},
{"https://localhost:8086", "", "", false, false},
{"http://localhost:8086", "username", "password", false, false},
{"http://localhost:8086", "", "", true, false},
{"https://localhost:8086", "", "", true, false},
{"localhost:8086", "", "", false, false},
{"localhost:8086", "", "", true, false},
{":http", "", "", false, true},
{
name: "no tls should have no error",
url: "http://localhost:8086",
},
{
name: "tls sholuld have no error",
url: "https://localhost:8086",
},
{
name: "no tls but with basic auth",
url: "http://localhost:8086",
username: "username",
password: "password",
},
{
name: "tls request but url is not tls should not error",
url: "http://localhost:8086",
tls: true,
},
{
name: "https with tls and with insecureSkipVerify should not error",
url: "https://localhost:8086",
tls: true,
insecureSkipVerify: true,
},
{
name: "URL does not require http or https",
url: "localhost:8086",
},
{
name: "URL with TLS request should not error",
url: "localhost:8086",
tls: true,
},
{
name: "invalid URL causes error",
url: ":http",
wantErr: true,
},
}
for _, testURL := range urls {
@ -141,10 +172,11 @@ func Test_Enterprise_NewClientWithURL(t *testing.T) {
Password: testURL.password,
},
testURL.tls,
testURL.insecureSkipVerify,
log.New(log.DebugLevel))
if err != nil && !testURL.shouldErr {
if err != nil && !testURL.wantErr {
t.Errorf("Unexpected error creating Client with URL %s and TLS preference %t. err: %s", testURL.url, testURL.tls, err.Error())
} else if err == nil && testURL.shouldErr {
} else if err == nil && testURL.wantErr {
t.Errorf("Expected error creating Client with URL %s and TLS preference %t", testURL.url, testURL.tls)
}
}
@ -159,7 +191,7 @@ func Test_Enterprise_ComplainsIfNotOpened(t *testing.T) {
Username: "docbrown",
Password: "1.21 gigawatts",
},
false, chronograf.TimeSeries(m1))
false, false, chronograf.TimeSeries(m1))
if err != nil {
t.Error("Expected ErrUnitialized, but was this err:", err)
}

View File

@ -3,6 +3,7 @@ package enterprise
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@ -14,6 +15,14 @@ import (
"github.com/influxdata/chronograf/influx"
)
// Shared transports for all clients to prevent leaking connections
var (
skipVerifyTransport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
defaultTransport = &http.Transport{}
)
type client interface {
Do(URL *url.URL, path, method string, authorizer influx.Authorizer, params map[string]string, body io.Reader) (*http.Response, error)
}
@ -26,10 +35,12 @@ type MetaClient struct {
}
// NewMetaClient represents a meta node in an Influx Enterprise cluster
func NewMetaClient(url *url.URL, authorizer influx.Authorizer) *MetaClient {
func NewMetaClient(url *url.URL, InsecureSkipVerify bool, authorizer influx.Authorizer) *MetaClient {
return &MetaClient{
URL: url,
client: &defaultClient{},
URL: url,
client: &defaultClient{
InsecureSkipVerify: InsecureSkipVerify,
},
authorizer: authorizer,
}
}
@ -399,7 +410,8 @@ func (m *MetaClient) Post(ctx context.Context, path string, action interface{},
}
type defaultClient struct {
Leader string
Leader string
InsecureSkipVerify bool
}
// Do is a helper function to interface with Influx Enterprise's Meta API
@ -438,6 +450,12 @@ func (d *defaultClient) Do(URL *url.URL, path, method string, authorizer influx.
CheckRedirect: d.AuthedCheckRedirect,
}
if d.InsecureSkipVerify {
client.Transport = skipVerifyTransport
} else {
client.Transport = defaultTransport
}
res, err := client.Do(req)
if err != nil {
return nil, err

View File

@ -37,7 +37,7 @@ func (c *UserStore) Delete(ctx context.Context, u *chronograf.User) error {
return c.Ctrl.DeleteUser(ctx, u.Name)
}
// Number of users in Influx
// Num of users in Influx
func (c *UserStore) Num(ctx context.Context) (int, error) {
all, err := c.All(ctx)
if err != nil {

View File

@ -12,13 +12,15 @@ RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y \
ruby-dev \
rpm \
zip \
python-pip
python-pip \
autoconf \
libtool
RUN pip install boto requests python-jose --upgrade
RUN gem install fpm
# Install node
ENV NODE_VERSION v6.11.5
ENV NODE_VERSION v6.12.3
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \
tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \
rm -f node-${NODE_VERSION}-linux-x64.tar.gz
@ -35,7 +37,7 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
# Install go
ENV GOPATH /root/go
ENV GO_VERSION 1.9.2
ENV GO_VERSION 1.9.4
ENV GO_ARCH amd64
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \

View File

@ -10,3 +10,6 @@ After updating the Dockerfile_build run
and push to quay with:
`docker push quay.io/influxdb/builder:chronograf-$(date "+%Y%m%d")`
### Update circle
Update DOCKER_TAG in circle.yml to the new container.

View File

@ -24,6 +24,7 @@ DATA_DIR = "/var/lib/chronograf"
SCRIPT_DIR = "/usr/lib/chronograf/scripts"
LOGROTATE_DIR = "/etc/logrotate.d"
CANNED_DIR = "/usr/share/chronograf/canned"
RESOURCES_DIR = "/usr/share/chronograf/resources"
INIT_SCRIPT = "etc/scripts/init.sh"
SYSTEMD_SCRIPT = "etc/scripts/chronograf.service"
@ -74,6 +75,7 @@ for f in CONFIGURATION_FILES:
targets = {
'chronograf' : './cmd/chronograf',
'chronoctl' : './cmd/chronoctl',
}
supported_builds = {
@ -115,7 +117,8 @@ def create_package_fs(build_root):
DATA_DIR[1:],
SCRIPT_DIR[1:],
LOGROTATE_DIR[1:],
CANNED_DIR[1:]
CANNED_DIR[1:],
RESOURCES_DIR[1:]
]
for d in dirs:
os.makedirs(os.path.join(build_root, d))

View File

@ -8,8 +8,12 @@ After=network-online.target
[Service]
User=chronograf
Group=chronograf
Environment="HOST=0.0.0.0"
Environment="PORT=8888"
Environment="BOLT_PATH=/var/lib/chronograf/chronograf-v1.db"
Environment="CANNED_PATH=/usr/share/chronograf/canned"
EnvironmentFile=-/etc/default/chronograf
ExecStart=/usr/bin/chronograf --host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned $CHRONOGRAF_OPTS
ExecStart=/usr/bin/chronograf $CHRONOGRAF_OPTS
KillMode=control-group
Restart=on-failure

View File

@ -12,9 +12,13 @@
# Script to execute when starting
SCRIPT="/usr/bin/chronograf"
export HOST="0.0.0.0"
export PORT="8888"
export BOLT_PATH="/var/lib/chronograf/chronograf-v1.db"
export CANNED_PATH="/usr/share/chronograf/canned"
# Options to pass to the script on startup
. /etc/default/chronograf
SCRIPT_OPTS="--host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned ${CHRONOGRAF_OPTS}"
SCRIPT_OPTS="${CHRONOGRAF_OPTS}"
# User to run the process under
RUNAS=chronograf

File diff suppressed because it is too large Load Diff

View File

@ -86,7 +86,11 @@
"name": "comet",
"value": "100"
}
]
],
"legend": {
"type": "static",
"orientation": "bottom"
}
}
],
"templates": [

View File

@ -1293,7 +1293,157 @@ func TestClient_Create(t *testing.T) {
createTaskOptions *client.CreateTaskOptions
}{
{
name: "create alert rule",
name: "create alert rule with tags",
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
ID: &MockID{
ID: "howdy",
},
},
args: args{
ctx: context.Background(),
rule: chronograf.AlertRule{
ID: "howdy",
Name: "myname's",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
Measurement: "meas",
GroupBy: chronograf.GroupBy{
Tags: []string{
"tag1",
"tag2",
},
},
},
Trigger: Deadman,
TriggerValues: chronograf.TriggerValues{
Period: "1d",
},
},
},
resTask: client.Task{
ID: "chronograf-v1-howdy",
Status: client.Enabled,
Type: client.StreamTask,
DBRPs: []client.DBRP{
{
Database: "db",
RetentionPolicy: "rp",
},
},
Link: client.Link{
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
},
},
createTaskOptions: &client.CreateTaskOptions{
TICKscript: `var db = 'db'
var rp = 'rp'
var measurement = 'meas'
var groupBy = ['tag1', 'tag2']
var whereFilter = lambda: TRUE
var period = 1d
var name = 'myname\'s'
var idVar = name + ':{{.Group}}'
var message = ''
var idTag = 'alertID'
var levelTag = 'level'
var messageField = 'message'
var durationField = 'duration'
var outputDB = 'chronograf'
var outputRP = 'autogen'
var outputMeasurement = 'alerts'
var triggerType = 'deadman'
var threshold = 0.0
var data = stream
|from()
.database(db)
.retentionPolicy(rp)
.measurement(measurement)
.groupBy(groupBy)
.where(whereFilter)
var trigger = data
|deadman(threshold, period)
.stateChangesOnly()
.message(message)
.id(idVar)
.idTag(idTag)
.levelTag(levelTag)
.messageField(messageField)
.durationField(durationField)
trigger
|eval(lambda: "emitted")
.as('value')
.keep('value', messageField, durationField)
|eval(lambda: float("value"))
.as('value')
.keep()
|influxDBOut()
.create()
.database(outputDB)
.retentionPolicy(outputRP)
.measurement(outputMeasurement)
.tag('alertName', name)
.tag('triggerType', triggerType)
trigger
|httpOut('output')
`,
ID: "chronograf-v1-howdy",
Type: client.StreamTask,
Status: client.Enabled,
DBRPs: []client.DBRP{
{
Database: "db",
RetentionPolicy: "rp",
},
},
},
want: &Task{
ID: "chronograf-v1-howdy",
Href: "/kapacitor/v1/tasks/chronograf-v1-howdy",
HrefOutput: "/kapacitor/v1/tasks/chronograf-v1-howdy/output",
Rule: chronograf.AlertRule{
Type: "stream",
DBRPs: []chronograf.DBRP{
{
DB: "db",
RP: "rp",
},
},
Status: "enabled",
ID: "chronograf-v1-howdy",
Name: "chronograf-v1-howdy",
},
},
},
{
name: "create alert rule with no tags",
fields: fields{
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
@ -1348,7 +1498,7 @@ var period = 1d
var name = 'myname\'s'
var idVar = name + ':{{.Group}}'
var idVar = name
var message = ''

View File

@ -123,7 +123,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
%s
var name = '%s'
var idVar = name + ':{{.Group}}'
var idVar = %s
var message = '%s'
var idTag = '%s'
var levelTag = '%s'
@ -143,6 +143,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
whereFilter(rule.Query),
wind,
Escape(rule.Name),
idVar(rule.Query),
Escape(rule.Message),
IDTag,
LevelTag,
@ -197,6 +198,13 @@ func groupBy(q *chronograf.QueryConfig) string {
return "[" + strings.Join(groups, ",") + "]"
}
func idVar(q *chronograf.QueryConfig) string {
if len(q.GroupBy.Tags) > 0 {
return `name + ':{{.Group}}'`
}
return "name"
}
func field(q *chronograf.QueryConfig) (string, error) {
if q == nil {
return "", fmt.Errorf("No fields set in query")

35
mocks/mapping.go Normal file
View File

@ -0,0 +1,35 @@
package mocks
import (
"context"
"github.com/influxdata/chronograf"
)
type MappingsStore struct {
AddF func(context.Context, *chronograf.Mapping) (*chronograf.Mapping, error)
AllF func(context.Context) ([]chronograf.Mapping, error)
DeleteF func(context.Context, *chronograf.Mapping) error
UpdateF func(context.Context, *chronograf.Mapping) error
GetF func(context.Context, string) (*chronograf.Mapping, error)
}
func (s *MappingsStore) Add(ctx context.Context, m *chronograf.Mapping) (*chronograf.Mapping, error) {
return s.AddF(ctx, m)
}
func (s *MappingsStore) All(ctx context.Context) ([]chronograf.Mapping, error) {
return s.AllF(ctx)
}
func (s *MappingsStore) Delete(ctx context.Context, m *chronograf.Mapping) error {
return s.DeleteF(ctx, m)
}
func (s *MappingsStore) Get(ctx context.Context, id string) (*chronograf.Mapping, error) {
return s.GetF(ctx, id)
}
func (s *MappingsStore) Update(ctx context.Context, m *chronograf.Mapping) error {
return s.UpdateF(ctx, m)
}

View File

@ -9,6 +9,7 @@ import (
// Store is a server.DataStore
type Store struct {
SourcesStore chronograf.SourcesStore
MappingsStore chronograf.MappingsStore
ServersStore chronograf.ServersStore
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
@ -36,6 +37,9 @@ func (s *Store) Users(ctx context.Context) chronograf.UsersStore {
func (s *Store) Organizations(ctx context.Context) chronograf.OrganizationsStore {
return s.OrganizationsStore
}
func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
return s.MappingsStore
}
func (s *Store) Dashboards(ctx context.Context) chronograf.DashboardsStore {
return s.DashboardsStore

33
noop/mappings.go Normal file
View File

@ -0,0 +1,33 @@
package noop
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// ensure MappingsStore implements chronograf.MappingsStore
var _ chronograf.MappingsStore = &MappingsStore{}
type MappingsStore struct{}
func (s *MappingsStore) All(context.Context) ([]chronograf.Mapping, error) {
return nil, fmt.Errorf("no mappings found")
}
func (s *MappingsStore) Add(context.Context, *chronograf.Mapping) (*chronograf.Mapping, error) {
return nil, fmt.Errorf("failed to add mapping")
}
func (s *MappingsStore) Delete(context.Context, *chronograf.Mapping) error {
return fmt.Errorf("failed to delete mapping")
}
func (s *MappingsStore) Get(ctx context.Context, ID string) (*chronograf.Mapping, error) {
return nil, chronograf.ErrMappingNotFound
}
func (s *MappingsStore) Update(context.Context, *chronograf.Mapping) error {
return fmt.Errorf("failed to update mapping")
}

View File

@ -8,6 +8,8 @@ import (
"github.com/influxdata/chronograf"
)
var _ Provider = &Auth0{}
type Auth0 struct {
Generic
Organizations map[string]bool // the set of allowed organizations users may belong to
@ -41,6 +43,26 @@ func (a *Auth0) PrincipalID(provider *http.Client) (string, error) {
return act.Email, nil
}
func (a *Auth0) Group(provider *http.Client) (string, error) {
type Account struct {
Email string `json:"email"`
Organization string `json:"organization"`
}
resp, err := provider.Get(a.Generic.APIURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
act := Account{}
if err = json.NewDecoder(resp.Body).Decode(&act); err != nil {
return "", err
}
return act.Organization, nil
}
func NewAuth0(auth0Domain, clientID, clientSecret, redirectURL string, organizations []string, logger chronograf.Logger) (Auth0, error) {
domain, err := url.Parse(auth0Domain)
if err != nil {

View File

@ -12,11 +12,12 @@ import (
"golang.org/x/oauth2"
)
// Provider interface with optional methods
// ExtendedProvider extendts the base Provider interface with optional methods
type ExtendedProvider interface {
Provider
// get PrincipalID from id_token
PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error)
GroupFromClaims(claims gojwt.MapClaims) (string, error)
}
var _ ExtendedProvider = &Generic{}
@ -119,6 +120,31 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
return email, nil
}
// Group returns the domain that a user belongs to in the
// the generic OAuth.
func (g *Generic) Group(provider *http.Client) (string, error) {
res := struct {
Email string `json:"email"`
}{}
r, err := provider.Get(g.APIURL)
if err != nil {
return "", err
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&res); err != nil {
return "", err
}
email := strings.Split(res.Email, "@")
if len(email) != 2 {
return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", res.Email)
}
return email[1], nil
}
// UserEmail represents user's email address
type UserEmail struct {
Email *string `json:"email,omitempty"`
@ -168,10 +194,25 @@ func ofDomain(requiredDomains []string, email string) bool {
return false
}
// verify optional id_token and extract email address of the user
// PrincipalIDFromClaims verifies an optional id_token and extracts email address of the user
func (g *Generic) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error) {
if id, ok := claims[g.APIKey].(string); ok {
return id, nil
}
return "", fmt.Errorf("no claim for %s", g.APIKey)
}
// GroupFromClaims verifies an optional id_token, extracts the email address of the user and splits off the domain part
func (g *Generic) GroupFromClaims(claims gojwt.MapClaims) (string, error) {
if id, ok := claims[g.APIKey].(string); ok {
email := strings.Split(id, "@")
if len(email) != 2 {
g.Logger.Error("malformed email address, expected %q to contain @ symbol", id)
return "DEFAULT", nil
}
return email[1], nil
}
return "", fmt.Errorf("no claim for %s", g.APIKey)
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"io"
"net/http"
"strings"
"github.com/google/go-github/github"
"github.com/influxdata/chronograf"
@ -44,9 +45,9 @@ func (g *Github) Secret() string {
// we are filtering by organizations.
func (g *Github) Scopes() []string {
scopes := []string{"user:email"}
if len(g.Orgs) > 0 {
scopes = append(scopes, "read:org")
}
// In order to access a users orgs, we need the "read:org" scope
// even if g.Orgs == 0
scopes = append(scopes, "read:org")
return scopes
}
@ -84,6 +85,26 @@ func (g *Github) PrincipalID(provider *http.Client) (string, error) {
return email, nil
}
// Group returns a comma delimited string of Github organizations
// that a user belongs to in Github
func (g *Github) Group(provider *http.Client) (string, error) {
client := github.NewClient(provider)
orgs, err := getOrganizations(client, g.Logger)
if err != nil {
return "", err
}
groups := []string{}
for _, org := range orgs {
if org.Login != nil {
groups = append(groups, *org.Login)
continue
}
}
return strings.Join(groups, ","), nil
}
func randomString(length int) string {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {

View File

@ -88,3 +88,19 @@ func (g *Google) PrincipalID(provider *http.Client) (string, error) {
g.Logger.Error("Domain '", info.Hd, "' is not a member of required Google domain(s): ", g.Domains)
return "", fmt.Errorf("Not in required domain")
}
// Group returns the string of domain a user belongs to in Google
func (g *Google) Group(provider *http.Client) (string, error) {
srv, err := goauth2.New(provider)
if err != nil {
g.Logger.Error("Unable to communicate with Google ", err.Error())
return "", err
}
info, err := srv.Userinfo.Get().Do()
if err != nil {
g.Logger.Error("Unable to retrieve Google email ", err.Error())
return "", err
}
return info.Hd, nil
}

View File

@ -88,6 +88,34 @@ func (h *Heroku) PrincipalID(provider *http.Client) (string, error) {
return account.Email, nil
}
// Group returns the Heroku organization that user belongs to.
func (h *Heroku) Group(provider *http.Client) (string, error) {
type DefaultOrg struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Account struct {
Email string `json:"email"`
DefaultOrganization DefaultOrg `json:"default_organization"`
}
resp, err := provider.Get(HerokuAccountRoute)
if err != nil {
h.Logger.Error("Unable to communicate with Heroku. err:", err)
return "", err
}
defer resp.Body.Close()
d := json.NewDecoder(resp.Body)
var account Account
if err := d.Decode(&account); err != nil {
h.Logger.Error("Unable to decode response from Heroku. err:", err)
return "", err
}
return account.DefaultOrganization.Name, nil
}
// Scopes for heroku is "identity" which grants access to user account
// information. This will grant us access to the user's email address which is
// used as the Principal's identifier.

View File

@ -40,9 +40,18 @@ var _ gojwt.Claims = &Claims{}
// Claims extends jwt.StandardClaims' Valid to make sure claims has a subject.
type Claims struct {
gojwt.StandardClaims
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtmldd
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
// that felt appropriate for Organization. As a result, we added a custom `org` field.
Organization string `json:"org,omitempty"`
// We were unable to find a standard claim at https://www.iana.org/assignments/jwt/jwt.xhtml
// that felt appropriate for a users Group(s). As a result we added a custom `grp` field.
// Multiple groups may be specified by comma delimiting the various group.
//
// The singlular `grp` was chosen over the `grps` to keep consistent with the JWT naming
// convention (it is common for singlularly named values to actually be arrays, see `given_name`,
// `family_name`, and `middle_name` in the iana link provided above). I should add the discalimer
// I'm currently sick, so this thought process might be off.
Group string `json:"grp,omitempty"`
}
// Valid adds an empty subject test to the StandardClaims checks.
@ -67,7 +76,7 @@ func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.
return j.ValidClaims(jwtToken, lifespan, alg)
}
// key verification for HMAC an RSA/RS256
// KeyFunc verifies HMAC or RSA/RS256 signatures
func (j *JWT) KeyFunc(token *gojwt.Token) (interface{}, error) {
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); ok {
return []byte(j.Secret), nil
@ -86,7 +95,7 @@ func (j *JWT) KeyFunc(token *gojwt.Token) (interface{}, error) {
// OpenID Provider Configuration Information at /.well-known/openid-configuration
// implements rfc7517 section 4.7 "x5c" (X.509 Certificate Chain) Parameter
// JWKS nested struct
// JWK defines a JSON Web KEy nested struct
type JWK struct {
Kty string `json:"kty"`
Use string `json:"use"`
@ -98,11 +107,12 @@ type JWK struct {
X5c []string `json:"x5c"`
}
// JWKS defines a JKW[]
type JWKS struct {
Keys []JWK `json:"keys"`
}
// for RS256 signed JWT tokens, lookup the signing key in the key discovery service
// KeyFuncRS256 verifies RS256 signed JWT tokens, it looks up the signing key in the key discovery service
func (j *JWT) KeyFuncRS256(token *gojwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*gojwt.SigningMethodRSA); !ok {
@ -131,22 +141,22 @@ func (j *JWT) KeyFuncRS256(token *gojwt.Token) (interface{}, error) {
}
// extract cert when kid and alg match
var cert_pkix []byte
var certPkix []byte
for _, jwk := range jwks.Keys {
if token.Header["kid"] == jwk.Kid && token.Header["alg"] == jwk.Alg {
// FIXME: optionally walk the key chain, see rfc7517 section 4.7
cert_pkix, err = base64.StdEncoding.DecodeString(jwk.X5c[0])
certPkix, err = base64.StdEncoding.DecodeString(jwk.X5c[0])
if err != nil {
return nil, fmt.Errorf("base64 decode error for JWK kid %v", token.Header["kid"])
}
}
}
if cert_pkix == nil {
if certPkix == nil {
return nil, fmt.Errorf("no signing key found for kid %v", token.Header["kid"])
}
// parse certificate (from PKIX format) and return signing key
cert, err := x509.ParseCertificate(cert_pkix)
cert, err := x509.ParseCertificate(certPkix)
if err != nil {
return nil, err
}
@ -189,12 +199,13 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
Subject: claims.Subject,
Issuer: claims.Issuer,
Organization: claims.Organization,
Group: claims.Group,
ExpiresAt: exp,
IssuedAt: iat,
}, nil
}
// get claims from id_token
// GetClaims extracts claims from id_token
func (j *JWT) GetClaims(tokenString string) (gojwt.MapClaims, error) {
var claims gojwt.MapClaims
@ -229,6 +240,7 @@ func (j *JWT) Create(ctx context.Context, user Principal) (Token, error) {
NotBefore: user.IssuedAt.Unix(),
},
Organization: user.Organization,
Group: user.Group,
}
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
// Sign and get the complete encoded token as a string using the secret

View File

@ -118,6 +118,7 @@ func (j *AuthMux) Callback() http.Handler {
// if we received an extra id_token, inspect it
var id string
group := "DEFAULT"
if token.Extra("id_token") != "" {
log.Debug("token provides extra id_token")
if provider, ok := j.Provider.(ExtendedProvider); ok {
@ -140,6 +141,11 @@ func (j *AuthMux) Callback() http.Handler {
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
if group, err = provider.GroupFromClaims(claims); err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
} else {
log.Debug("provider does not implement PrincipalIDFromClaims()")
}
@ -155,11 +161,18 @@ func (j *AuthMux) Callback() http.Handler {
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
}
p := Principal{
Subject: id,
Issuer: j.Provider.Name(),
Group: group,
}
ctx := r.Context()
err = j.Auth.Authorize(ctx, w, p)

View File

@ -31,7 +31,11 @@ func setupMuxTest(selector func(*AuthMux) http.Handler, body string) (*http.Clie
now := func() time.Time {
return testTime
}
mp := &MockProvider{"biff@example.com", provider.URL}
mp := &MockProvider{
Email: "biff@example.com",
ProviderURL: provider.URL,
Orgs: "",
}
mt := &YesManTokenizer{}
auth := &cookie{
Name: DefaultCookieName,
@ -164,7 +168,7 @@ func Test_AuthMux_Callback_SetsCookie(t *testing.T) {
}
func Test_AuthMux_Callback_HandlesIdToken(t *testing.T) {
// body taken from ADFS4
// body taken from ADFS4
body := `{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJ1cm46bWljcm9zb2Z0OnVzZXJpbmZvIiwiaXNzIjoiaHR0cDovL2RzdGNpbWFhZDFwLmRzdC1pdHMuZGUvYWRmcy9zZXJ2aWNlcy90cnVzdCIsImlhdCI6MTUxNTcwMDU2NSwiZXhwIjoxNTE1NzA0MTY1LCJhcHB0eXBlIjoiQ29uZmlkZW50aWFsIiwiYXBwaWQiOiJjaHJvbm9ncmFmIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXV0aF90aW1lIjoiMjAxOC0wMS0xMVQxOTo1MToyNS44MDZaIiwidmVyIjoiMS4wIiwic2NwIjoib3BlbmlkIiwic3ViIjoiZVlWM2pkbGROeUZZMXFGRkg0b0FkQnZERmZiVmZudUcyOUhpSGtTdWp3az0ifQ.sf1qJys9LMUp2S232IRK2aTXiPCE93O-cUdYQQz7kg2woyD46KLwwKIYJVqMaqLspTn3OmaIhKtgx5ZXyAEtihODB1GOBK7DBNRBYCS1iqY_v2-Qwjf7hgaNaCqBjs0DZJspfp5G9MTykvD1FOtQNjPOcBW-i2bblG9L9jlmMbOZ3F7wrZMrroTSkiSn_gRiw2SnN8K7w8WrMEXNK2_jg9ZJ7aSHeUSBwkRNFRds2QNho3HWHg-zcsZFdZ4UGSt-6Az_0LY3yENMLj5us5Rl6Qzk_Re2dhFrlnlXlY1v1DEp3icCvvjkv6AeZWjTfW4qETZaCXUKtSyZ7d5_V1CRDQ","token_type":"bearer","expires_in":3600,"resource":"urn:microsoft:userinfo","refresh_token":"X9ZGO4H1bMk2bFeOfpv18BzAuFBzUPKQNfOEfdp60FkAAQAALPEBfj23FPEzajle-hm4DrDXp8-Kj53OqoVGalyZeuR-lfJzxpQXQhRAXZOUTuuQ8AQycByh9AylQYDA0jdMFW4FL4WL_6JhNh2JrtXCv2HQ9ozbUq9F7u_O0cY7u0P2pfNujQfk3ckYn-CMVjXbuwJTve6bXUR0JDp5c195bAVA5eFWyI-2uh432t7viyaIjAVbWxQF4fvimcpF1Et9cGodZHVsrZzGxKRnzwjYkWHsqm9go4KOeSKN6MlcWbjvS1UdMjQXSvoqSI00JnSMC3hxJZFn5JcmAPB1AMnJf4VvXZ5b-aOnwdX09YT8KayWkWekAsuZqTAsFwhZPVCRGWAFAADy0e2fTe6l-U6Cj_2bWsq6Snm1QEpWHXuwOJKWZJH-9yQn8KK3KzRowSzRuACzEIpZS5skrqXs_-2aOaZibNpjCEVyw8fF8GTw3VRLufsSrMQ5pD0KL7TppTGFpaqgwIH1yq6T8aRY4DeyoJkNpnO9cw1wuqnY7oGF-J25sfZ4XNWhk6o5e9A45PXhTilClyDKDLqTfdoIsG1Koc2ywqTIb-XI_EbWR3e4ijy8Kmlehw1kU9_xAG0MmmD2HTyGHZCBRgrskYCcHd-UNgCMrNAb5dZQ8NwpKtEL46qIq4R0lheTRRK8sOWzzuJXmvDEoJiIxqSR3Ma4MOISi-vsIsAuiEL9G1aMOkDRj-kDVmqrdKRAwYnN78AWY5EFfkQJyVBbiG882wBh9S0q3HUUCxzFerOvl4eDlVn6m18rRMz7CVZYBBltGtHRhEOQ4gumICR5JRrXAC50aBmUlhDiiMdbEIwJrvWrkhKE0oAJznqC7gleP0E4EOEh9r6CEGZ7Oj8X9Cdzjbuq2G1JGBm_yUvkhAcV61DjOiIQl35BpOfshveNZf_caUtNMa2i07BBmezve17-2kWGzRunr1BD1vMTz41z-H62fy4McR47WJjdDJnuy4DH5AZYQ6ooVxWCtEqeqRPYpzO0XdOdJGXFqXs9JzDKVXTgnHU443hZBC5H-BJkZDuuJ_ZWNKXf03JhouWkxXcdaMbuaQYOZJsUySVyJ5X4usrBFjW4udZAzy7mua-nJncbvcwoyVXiFlRfZiySXolQ9865N7XUnEk_2PijMLoVDATDbA09XuRySvngNsdsQ27p21dPxChXdtpD5ofNqKJ2FBzFKmxCkuX7L01N1nDpWQTuxhHF0JfxSKG5m3jcTx8Bd7Un94mTuAB7RuglDqkdQB9o4X9NHNGSdqGQaK-xeKoNCFWevk3VZoDoY9w2NqSNV2VIuqhy7SxtDSMjZKC5kiQi5EfGeTYZAvTwMYwaXb7K4WWtscy_ZE15EOCVeYi0hM1Ma8iFFTANkSRyX83Ju4SRphxRKnpKcJ2pPYH784I5HOm5sclhUL3aLeAA161QgxRBSa9YVIZfyXHyWQTcbNucNdhmdUZnKfRv1xtXcS9VAx2yAkoKFehZivEINX0Y500-WZ1eT_RXp0BfCKmJQ8Fu50oTaI-c5h2Q3Gp_LTSODNnMrjJiJxCLD_LD1fd1e8jTYDV3NroGlpWTuTdjMUm-Z1SMXaaJzQGEnNT6F8b6un9228L6YrDC_3MJ5J80VAHL5EO1GesdEWblugCL7AQDtFjNXq0lK8Aoo8X9_hlvDwgfdR16l8QALPT1HJVzlHPG8G3dRe50TKZnl3obU0WXN1KYG1EC4Qa3LyaVCIuGJYOeFqjMINrf7PoM368nS9yhrY08nnoHZbQ7IeA1KsNq2kANeH1doCNfWrXDwn8KxjYxZPEnzvlQ5M1RIzArOqzWL8NbftW1q2yCZZ4RVg0vOTVXsqWFnQIvWK-mkELa7bvByFzbtVHOJpc_2EKBKBNv6IYUENRCu2TOf6w7u42yvng7ccoXRTiUFUlKgVmswf9FzISxFd-YKgrzp3bMhC3gReGqcJuqEwnXPvOAY_BAkVMSd_ZaCFuyclRjFvUxrAg1T_cqOvRIlJ2Qq7z4u7W3BAo9BtFdj8QNLKJXtvvzXTprglRPDNP_QEPAkwZ_Uxa13vdYFcG18WCx4GbWQXchl5B7DnISobcdCH34M-I0xDZN98VWQVmLAfPniDUD30C8pfiYF7tW_EVy958Eg_JWVy0SstYEhV-y-adrJ1Oimjv0ptsWv-yErKBUD14aex9A_QqdnTXZUg.tqMb72eWAkAIvInuLp57NDyGxfYvms3NnhN-mllkYb7Xpd8gVbQFc2mYdzOOhtnfGuakyXYF4rZdJonQwzBO6C9KYuARciUU1Ms4bWPC-aeNO5t-aO_bDZbwC9qMPmq5ZuxG633BARGaw26fr0Z7qhcJMiou_EuaIehYTKkPB-mxtRAhxxyX91qqe0-PJnCHWoxizC4hDCUwp9Jb54tNf34BG3vtkXFX-kUARNfGucgKUkh6RYkhWiMBsMVoyWmkFXB5fYxmCAH5c5wDW6srKdyIDEWZInliuKbYR0p66vg1FfoSi4bBfrsm5NtCtLKG9V6Q0FEIA6tRRgHmKUGpkw","refresh_token_expires_in":28519,"scope":"openid","id_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTU3MDA1NjUsImV4cCI6MTUxNTcwNDE2NSwiYXV0aF90aW1lIjoxNTE1NzAwMjg1LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.XD873K6NVRTJY1700NsflLJGZKFHJfNBjB81SlADVdAHbhnq7wkAZbGEEm8wFqvTKKysUl9EALzmDa2tR9nzohVvmHftIYBO0E-wPBzdzWWX0coEgpVAc-SysP-eIQWLsj8EaodaMkCgKO0FbTWOf4GaGIBZGklrr9EEk8VRSdbXbm6Sv9WVphezEzxq6JJBRBlCVibCnZjR5OYh1Vw_7E7P38ESPbpLY3hYYl2hz4y6dQJqCwGr7YP8KrDlYtbosZYgT7ayxokEJI1udEbX5PbAq5G6mj5rLfSOl85rMg-psZiivoM8dn9lEl2P7oT8rAvMWvQp-FIRQQHwqf9cxw"}`
hc, ts, prov := setupMuxTest(func(j *AuthMux) http.Handler {
return j.Callback()

View File

@ -34,6 +34,7 @@ type Principal struct {
Subject string
Issuer string
Organization string
Group string
ExpiresAt time.Time
IssuedAt time.Time
}
@ -54,6 +55,10 @@ type Provider interface {
PrincipalID(provider *http.Client) (string, error)
// Name is the name of the Provider
Name() string
// Group is a comma delimited list of groups and organizations for a provider
// TODO: This will break if there are any group names that contain commas.
// I think this is okay, but I'm not 100% certain.
Group(provider *http.Client) (string, error)
}
// Mux is a collection of handlers responsible for servicing an Oauth2 interaction between a browser and a provider

View File

@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"time"
goauth "golang.org/x/oauth2"
@ -17,6 +18,7 @@ var _ Provider = &MockProvider{}
type MockProvider struct {
Email string
Orgs string
ProviderURL string
}
@ -49,6 +51,20 @@ func (mp *MockProvider) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, e
return mp.Email, nil
}
func (mp *MockProvider) GroupFromClaims(claims gojwt.MapClaims) (string, error) {
email := strings.Split(mp.Email, "@")
if len(email) != 2 {
//g.Logger.Error("malformed email address, expected %q to contain @ symbol", id)
return "DEFAULT", nil
}
return email[1], nil
}
func (mp *MockProvider) Group(provider *http.Client) (string, error) {
return mp.Orgs, nil
}
func (mp *MockProvider) Scopes() []string {
return []string{}
}
@ -76,7 +92,7 @@ func (y *YesManTokenizer) ExtendedPrincipal(ctx context.Context, p Principal, ex
return p, nil
}
func (m *YesManTokenizer) GetClaims(tokenString string) (gojwt.MapClaims, error) {
func (y *YesManTokenizer) GetClaims(tokenString string) (gojwt.MapClaims, error) {
return gojwt.MapClaims{}, nil
}

View File

@ -38,6 +38,9 @@ const (
EditorRoleName = "editor"
AdminRoleName = "admin"
SuperAdminStatus = "superadmin"
// Indicatior that the server should retrieve the default role for the organization.
WildcardRoleName = "*"
)
var (

View File

@ -11,6 +11,13 @@ import (
"github.com/influxdata/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.
@ -49,6 +56,33 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
})
}
// 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
@ -181,6 +215,13 @@ func hasAuthorizedRole(u *chronograf.User, role string) bool {
}
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 {

View File

@ -108,6 +108,230 @@ func TestAuthorizedUser(t *testing.T) {
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: clog.New(clog.DebugLevel),
},
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: clog.New(clog.DebugLevel),
},
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: clog.New(clog.DebugLevel),
},
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: clog.New(clog.DebugLevel),
},
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{
@ -1606,3 +1830,118 @@ func TestAuthorizedUser(t *testing.T) {
})
}
}
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: clog.New(clog.DebugLevel),
},
args: args{
serverContext: true,
},
wants: wants{
authorized: true,
hasServerContext: true,
},
},
{
name: "user on context is a SuperAdmin",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
args: args{
user: &chronograf.User{
SuperAdmin: true,
},
},
wants: wants{
authorized: true,
hasServerContext: true,
},
},
{
name: "user on context is a not SuperAdmin",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
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)
}
})
}
}

View File

@ -28,37 +28,31 @@ type dashboardCellResponse struct {
func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse {
base := "/chronograf/v1/dashboards"
newCell := chronograf.DashboardCell{}
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
copy(newCell.Queries, cell.Queries)
newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors))
copy(newCell.CellColors, cell.CellColors)
// ensure x, y, and y2 axes always returned
labels := []string{"x", "y", "y2"}
newCell.Axes = make(map[string]chronograf.Axis, len(labels))
newCell.X = cell.X
newCell.Y = cell.Y
newCell.W = cell.W
newCell.H = cell.H
newCell.Name = cell.Name
newCell.ID = cell.ID
newCell.Type = cell.Type
for _, lbl := range labels {
if axis, found := cell.Axes[lbl]; !found {
newCell.Axes[lbl] = chronograf.Axis{
Bounds: []string{},
}
} else {
newCell.Axes[lbl] = axis
}
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: newCell,
DashboardCell: cell,
Links: dashboardCellLinks{
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
},
@ -91,7 +85,10 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
if err != nil {
return err
}
return HasCorrectColors(c)
if err = HasCorrectColors(c); err != nil {
return err
}
return HasCorrectLegend(c)
}
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
@ -126,6 +123,27 @@ func HasCorrectColors(c *chronograf.DashboardCell) error {
return nil
}
// HasCorrectLegend verifies that the format of the legend is correct
func HasCorrectLegend(c *chronograf.DashboardCell) error {
// No legend set
if c.Legend.Type == "" && c.Legend.Orientation == "" {
return nil
}
if c.Legend.Type == "" || c.Legend.Orientation == "" {
return chronograf.ErrInvalidLegend
}
if !oneOf(c.Legend.Orientation, "top", "bottom", "right", "left") {
return chronograf.ErrInvalidLegendOrient
}
// Remember! if we add other types, update ErrInvalidLegendType
if !oneOf(c.Legend.Type, "static") {
return chronograf.ErrInvalidLegendType
}
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 {

View File

@ -532,7 +532,7 @@ func TestService_ReplaceDashboardCell(t *testing.T) {
}
}
`))),
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"}],"links":{"self":"/chronograf/v1/dashboards/1/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"}],"legend":{},"links":{"self":"/chronograf/v1/dashboards/1/cells/3c5c4102-fa40-4585-a8f9-917c77e37192"}}
`,
},
{
@ -695,7 +695,7 @@ func Test_newCellResponses(t *testing.T) {
want []dashboardCellResponse
}{
{
name: "foo",
name: "all fields set",
dID: chronograf.DashboardID(1),
dcells: []chronograf.DashboardCell{
chronograf.DashboardCell{
@ -752,6 +752,10 @@ func Test_newCellResponses(t *testing.T) {
chronograf.CellColor{ID: "0", Type: "min", Hex: "#00C9FF", Name: "laser", Value: "0"},
chronograf.CellColor{ID: "1", Type: "max", Hex: "#9394FF", Name: "comet", Value: "100"},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
},
want: []dashboardCellResponse{
@ -817,6 +821,50 @@ func Test_newCellResponses(t *testing.T) {
Value: "100",
},
},
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
Links: dashboardCellLinks{
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
},
},
},
{
name: "nothing set",
dID: chronograf.DashboardID(1),
dcells: []chronograf.DashboardCell{
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": chronograf.Axis{
Bounds: []string{},
},
"y": chronograf.Axis{
Bounds: []string{},
},
"y2": chronograf.Axis{
Bounds: []string{},
},
},
CellColors: []chronograf.CellColor{},
Legend: chronograf.Legend{},
},
Links: dashboardCellLinks{
Self: "/chronograf/v1/dashboards/1/cells/445f8dc0-4d73-4168-8477-f628690d18a3"},
@ -832,3 +880,97 @@ func Test_newCellResponses(t *testing.T) {
})
}
}
func TestHasCorrectLegend(t *testing.T) {
tests := []struct {
name string
c *chronograf.DashboardCell
wantErr bool
}{
{
name: "empty legend is ok",
c: &chronograf.DashboardCell{},
},
{
name: "must have both an orientation and type",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
},
},
wantErr: true,
},
{
name: "must have both a type and orientation",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Orientation: "bottom",
},
},
wantErr: true,
},
{
name: "invalid types",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "no such type",
Orientation: "bottom",
},
},
wantErr: true,
},
{
name: "invalid orientation",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "no such orientation",
},
},
wantErr: true,
},
{
name: "orientation bottom valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "bottom",
},
},
},
{
name: "orientation top valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "top",
},
},
},
{
name: "orientation right valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "right",
},
},
},
{
name: "orientation left valid",
c: &chronograf.DashboardCell{
Legend: chronograf.Legend{
Type: "static",
Orientation: "left",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := HasCorrectLegend(tt.c); (err != nil) != tt.wantErr {
t.Errorf("HasCorrectLegend() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

250
server/mapping.go Normal file
View File

@ -0,0 +1,250 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
)
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 retrives 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
}
fmt.Printf("mappings: %#v\n", mappings)
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
}

360
server/mapping_test.go Normal file
View File

@ -0,0 +1,360 @@
package server
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/chronograf/roles"
)
func TestMappings_All(t *testing.T) {
type fields struct {
MappingsStore chronograf.MappingsStore
}
type args struct {
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
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: log.New(log.DebugLevel),
}
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: log.New(log.DebugLevel),
}
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: log.New(log.DebugLevel),
}
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: log.New(log.DebugLevel),
}
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)
}
})
}
}

View File

@ -20,16 +20,29 @@ type meLinks struct {
type meResponse struct {
*chronograf.User
Links meLinks `json:"links"`
Organizations []chronograf.Organization `json:"organizations,omitempty"`
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) meResponse {
base := "/chronograf/v1/users"
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))
}
@ -181,7 +194,7 @@ 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 := newMeResponse(nil)
res := newNoAuthMeResponse()
encodeJSON(w, http.StatusOK, res, s.Logger)
return
}
@ -200,12 +213,13 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
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 == "" {
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
p.Organization = defaultOrg.ID
}
@ -219,35 +233,8 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
return
}
defaultOrg, err := s.Store.Organizations(serverCtx).DefaultOrganization(serverCtx)
if err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
// user exists
if usr != nil {
if defaultOrg.Public || usr.SuperAdmin == true {
// If the default organization is public, or the user is a super admin
// they will always have a role in the default organization
defaultOrgID := defaultOrg.ID
if !hasRoleInDefaultOrganization(usr, defaultOrgID) {
usr.Roles = append(usr.Roles, chronograf.Role{
Organization: defaultOrgID,
Name: defaultOrg.DefaultRole,
})
if err := s.Store.Users(serverCtx).Update(serverCtx, usr); err != nil {
unknownErrorWithMessage(w, err, s.Logger)
return
}
}
}
// If the default org is private and the user has no roles, they should not have access
if !defaultOrg.Public && len(usr.Roles) == 0 {
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", 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
@ -264,20 +251,14 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
unknownErrorWithMessage(w, err, s.Logger)
return
}
res := newMeResponse(usr)
res := newMeResponse(usr, currentOrg.ID)
res.Organizations = orgs
res.CurrentOrganization = currentOrg
encodeJSON(w, http.StatusOK, res, s.Logger)
return
}
// If users must be explicitly added to the default organization, respond with 403
// forbidden
if !defaultOrg.Public {
Error(w, http.StatusForbidden, "This organization is private. To gain access, you must be explicitly added by an administrator.", s.Logger)
return
}
// Because we didnt find a user, making a new one
user := &chronograf.User{
Name: p.Subject,
@ -286,17 +267,23 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
// support OAuth2. This hard-coding should be removed whenever we add
// support for other authentication schemes.
Scheme: scheme,
Roles: []chronograf.Role{
{
Name: defaultOrg.DefaultRole,
// This is the ID of the default organization
Organization: defaultOrg.ID,
},
},
// TODO(desa): this needs a better name
SuperAdmin: s.newUsersAreSuperAdmin(),
}
roles, err := s.mapPrincipalToRoles(serverCtx, p)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), s.Logger)
return
}
if 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
}
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)
@ -314,7 +301,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
unknownErrorWithMessage(w, err, s.Logger)
return
}
res := newMeResponse(newUser)
res := newMeResponse(newUser, currentOrg.ID)
res.Organizations = orgs
res.CurrentOrganization = currentOrg
encodeJSON(w, http.StatusOK, res, s.Logger)

View File

@ -23,6 +23,7 @@ func TestService_Me(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
MappingsStore chronograf.MappingsStore
ConfigStore chronograf.ConfigStore
Logger chronograf.Logger
UseAuth bool
@ -56,13 +57,24 @@ func TestService_Me(t *testing.T) {
},
},
},
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,
Public: false,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -72,13 +84,11 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.ViewerRoleName,
Public: false,
}, nil
case "1":
return &chronograf.Organization{
ID: "1",
Name: "The Bad Place",
Public: false,
ID: "1",
Name: "The Bad Place",
}, nil
}
return nil, nil
@ -108,12 +118,12 @@ func TestService_Me(t *testing.T) {
Subject: "me",
Issuer: "github",
},
wantStatus: http.StatusForbidden,
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
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 user - private default org and user is a super admin",
name: "Existing superadmin - not member of any organization",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
@ -121,13 +131,17 @@ func TestService_Me(t *testing.T) {
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
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,
Public: false,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -137,13 +151,11 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
case "1":
return &chronograf.Organization{
ID: "1",
Name: "The Bad Place",
Public: true,
ID: "1",
Name: "The Bad Place",
}, nil
}
return nil, nil
@ -176,137 +188,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
},
{
name: "Existing user - private default org",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "Default",
DefaultRole: roles.ViewerRoleName,
Public: false,
}, 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,
Public: true,
}, nil
case "1":
return &chronograf.Organization{
ID: "1",
Name: "The Bad Place",
Public: true,
}, 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.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
},
{
name: "Existing user - default org public",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
},
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
Name: "Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, 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,
Public: true,
}, nil
case "1":
return &chronograf.Organization{
ID: "1",
Name: "The Bad Place",
Public: true,
}, 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":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
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",
@ -317,13 +199,17 @@ func TestService_Me(t *testing.T) {
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
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,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -333,7 +219,6 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
}
return nil, chronograf.ErrOrganizationNotFound
@ -365,7 +250,7 @@ func TestService_Me(t *testing.T) {
wantBody: `{"code":403,"message":"user's current organization was not found"}`,
},
{
name: "new user - default org is public",
name: "default mapping applies to new user",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest("GET", "http://example.com/foo", nil),
@ -380,13 +265,24 @@ func TestService_Me(t *testing.T) {
},
},
},
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,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -394,7 +290,15 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
},
}, nil
},
},
@ -423,8 +327,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
`,
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",
@ -442,13 +345,24 @@ func TestService_Me(t *testing.T) {
},
},
},
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,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -456,7 +370,15 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
},
}, nil
},
},
@ -485,8 +407,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
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",
@ -504,13 +425,24 @@ func TestService_Me(t *testing.T) {
},
},
},
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,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -518,7 +450,15 @@ func TestService_Me(t *testing.T) {
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: "0",
Name: "The Gnarly Default",
DefaultRole: roles.ViewerRoleName,
},
}, nil
},
},
@ -547,8 +487,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"secret","superAdmin":true,"roles":[{"name":"viewer","organization":"0"}],"provider":"auth0","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
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",
@ -565,18 +504,31 @@ func TestService_Me(t *testing.T) {
},
},
},
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",
Public: true,
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",
Public: true,
ID: "0",
Name: "The Bad Place",
}, nil
},
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: "0",
Name: "The Bad Place",
DefaultRole: roles.ViewerRoleName,
},
}, nil
},
},
@ -601,9 +553,9 @@ func TestService_Me(t *testing.T) {
Subject: "secret",
Issuer: "heroku",
},
wantStatus: http.StatusInternalServerError,
wantStatus: http.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":500,"message":"Unknown error: error storing user secret: Why Heavy?"}`,
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
},
{
name: "No Auth",
@ -624,8 +576,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/users/me"}}
`,
wantBody: `{"links":{"self":"/chronograf/v1/me"}}`,
},
{
name: "Empty Principal",
@ -659,13 +610,24 @@ func TestService_Me(t *testing.T) {
fields: fields{
UseAuth: true,
Logger: log.New(log.DebugLevel),
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,
Public: false,
}, nil
},
},
@ -694,7 +656,7 @@ func TestService_Me(t *testing.T) {
},
wantStatus: http.StatusForbidden,
wantContentType: "application/json",
wantBody: `{"code":403,"message":"This organization is private. To gain access, you must be explicitly added by an administrator."}`,
wantBody: `{"code":403,"message":"This Chronograf is private. To gain access, you must be explicitly added by an administrator."}`,
},
}
for _, tt := range tests {
@ -703,12 +665,14 @@ func TestService_Me(t *testing.T) {
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,
}
fmt.Println(tt.name)
s.Me(tt.args.w, tt.args.r)
resp := tt.args.w.Result()
@ -792,7 +756,6 @@ func TestService_UpdateMe(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.AdminRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -805,13 +768,11 @@ func TestService_UpdateMe(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.AdminRoleName,
Public: true,
}, nil
case "1337":
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
Public: true,
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
}
return nil, nil
@ -824,7 +785,7 @@ func TestService_UpdateMe(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"admin","public":true},{"id":"1337","name":"The ShillBillThrilliettas","public":true}],"currentOrganization":{"id":"1337","name":"The ShillBillThrilliettas","public":true}}`,
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",
@ -866,7 +827,6 @@ func TestService_UpdateMe(t *testing.T) {
ID: "0",
Name: "Default",
DefaultRole: roles.EditorRoleName,
Public: true,
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
@ -876,16 +836,14 @@ func TestService_UpdateMe(t *testing.T) {
switch *q.ID {
case "1337":
return &chronograf.Organization{
ID: "1337",
Name: "The ThrillShilliettos",
Public: false,
ID: "1337",
Name: "The ThrillShilliettos",
}, nil
case "0":
return &chronograf.Organization{
ID: "0",
Name: "Default",
DefaultRole: roles.EditorRoleName,
Public: true,
}, nil
}
return nil, nil
@ -899,7 +857,7 @@ func TestService_UpdateMe(t *testing.T) {
},
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"name":"me","roles":[{"name":"admin","organization":"1337"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"editor","public":true},{"id":"1337","name":"The ThrillShilliettos","public":false}],"currentOrganization":{"id":"1337","name":"The ThrillShilliettos","public":false}}`,
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",
@ -946,9 +904,8 @@ func TestService_UpdateMe(t *testing.T) {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
Public: true,
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
},
},

57
server/middle.go Normal file
View File

@ -0,0 +1,57 @@
package server
import (
"net/http"
"github.com/bouk/httprouter"
"github.com/influxdata/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)
}
}

196
server/middle_test.go Normal file
View File

@ -0,0 +1,196 @@
package server
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/mocks"
"github.com/influxdata/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: log.New(log.DebugLevel),
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: log.New(log.DebugLevel),
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: log.New(log.DebugLevel),
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: log.New(log.DebugLevel),
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)
}
})
}
}

View File

@ -68,6 +68,16 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
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,
@ -105,6 +115,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
)
}
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"))
@ -114,9 +137,16 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/organizations", EnsureAdmin(service.Organizations))
router.POST("/chronograf/v1/organizations", EnsureSuperAdmin(service.NewOrganization))
router.GET("/chronograf/v1/organizations/:id", EnsureAdmin(service.OrganizationID))
router.PATCH("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.UpdateOrganization))
router.DELETE("/chronograf/v1/organizations/:id", EnsureSuperAdmin(service.RemoveOrganization))
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))
// Sources
router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources))
@ -194,12 +224,19 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
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/users", EnsureAdmin(service.Users))
router.POST("/chronograf/v1/users", EnsureAdmin(service.NewUser))
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/users/:id", EnsureAdmin(service.UserID))
router.DELETE("/chronograf/v1/users/:id", EnsureAdmin(service.RemoveUser))
router.PATCH("/chronograf/v1/users/:id", EnsureAdmin(service.UpdateUser))
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))
@ -250,6 +287,11 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
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

View File

@ -15,7 +15,6 @@ import (
type organizationRequest struct {
Name string `json:"name"`
DefaultRole string `json:"defaultRole"`
Public *bool `json:"public"`
}
func (r *organizationRequest) ValidCreate() error {
@ -27,7 +26,7 @@ func (r *organizationRequest) ValidCreate() error {
}
func (r *organizationRequest) ValidUpdate() error {
if r.Name == "" && r.DefaultRole == "" && r.Public == nil {
if r.Name == "" && r.DefaultRole == "" {
return fmt.Errorf("No fields to update")
}
@ -119,10 +118,6 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
DefaultRole: req.DefaultRole,
}
if req.Public != nil {
org.Public = *req.Public
}
res, err := s.Store.Organizations(ctx).Add(ctx, org)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
@ -165,7 +160,7 @@ func (s *Service) NewOrganization(w http.ResponseWriter, r *http.Request) {
func (s *Service) OrganizationID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
id := httprouter.GetParamFromContext(ctx, "oid")
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
@ -191,7 +186,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
}
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
id := httprouter.GetParamFromContext(ctx, "oid")
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {
@ -207,10 +202,6 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
org.DefaultRole = req.DefaultRole
}
if req.Public != nil {
org.Public = *req.Public
}
err = s.Store.Organizations(ctx).Update(ctx, org)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
@ -226,7 +217,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
// 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, "id")
id := httprouter.GetParamFromContext(ctx, "oid")
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {

View File

@ -52,9 +52,8 @@ func TestService_OrganizationID(t *testing.T) {
switch *q.ID {
case "1337":
return &chronograf.Organization{
ID: "1337",
Name: "The Good Place",
Public: false,
ID: "1337",
Name: "The Good Place",
}, nil
default:
return nil, fmt.Errorf("Organization with ID %s not found", *q.ID)
@ -65,7 +64,38 @@ func TestService_OrganizationID(t *testing.T) {
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","public":false}`,
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: log.New(log.DebugLevel),
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"}}`,
},
}
@ -82,7 +112,7 @@ func TestService_OrganizationID(t *testing.T) {
context.Background(),
httprouter.Params{
{
Key: "id",
Key: "oid",
Value: tt.id,
},
}))
@ -124,7 +154,7 @@ func TestService_Organizations(t *testing.T) {
wantBody string
}{
{
name: "Get Single Organization",
name: "Get Organizations",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
@ -139,14 +169,12 @@ func TestService_Organizations(t *testing.T) {
AllF: func(ctx context.Context) ([]chronograf.Organization, error) {
return []chronograf.Organization{
chronograf.Organization{
ID: "1337",
Name: "The Good Place",
Public: false,
ID: "1337",
Name: "The Good Place",
},
chronograf.Organization{
ID: "100",
Name: "The Bad Place",
Public: false,
ID: "100",
Name: "The Bad Place",
},
}, nil
},
@ -154,7 +182,7 @@ func TestService_Organizations(t *testing.T) {
},
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","public":false},{"links":{"self":"/chronograf/v1/organizations/100"},"id":"100","name":"The Bad Place","public":false}]}`,
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"}]}`,
},
}
@ -195,7 +223,6 @@ func TestService_UpdateOrganization(t *testing.T) {
w *httptest.ResponseRecorder
r *http.Request
org *organizationRequest
public bool
setPtr bool
}
tests := []struct {
@ -231,7 +258,6 @@ func TestService_UpdateOrganization(t *testing.T) {
ID: "1337",
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: false,
}, nil
},
},
@ -239,41 +265,7 @@ func TestService_UpdateOrganization(t *testing.T) {
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"},"public":false}`,
},
{
name: "Update Organization public",
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{},
public: false,
setPtr: true,
},
fields: fields{
Logger: log.New(log.DebugLevel),
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: "0",
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
},
},
id: "0",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"id":"0","name":"The Good Place","defaultRole":"viewer","public":false,"links":{"self":"/chronograf/v1/organizations/0"}}`,
wantBody: `{"id":"1337","name":"The Bad Place","defaultRole":"viewer","links":{"self":"/chronograf/v1/organizations/1337"}}`,
},
{
name: "Update Organization - nothing to update",
@ -297,7 +289,6 @@ func TestService_UpdateOrganization(t *testing.T) {
ID: "1337",
Name: "The Good Place",
DefaultRole: roles.ViewerRoleName,
Public: true,
}, nil
},
},
@ -331,7 +322,6 @@ func TestService_UpdateOrganization(t *testing.T) {
ID: "1337",
Name: "The Good Place",
DefaultRole: roles.MemberRoleName,
Public: false,
}, nil
},
},
@ -339,7 +329,7 @@ func TestService_UpdateOrganization(t *testing.T) {
id: "1337",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer","public":false}`,
wantBody: `{"links":{"self":"/chronograf/v1/organizations/1337"},"id":"1337","name":"The Good Place","defaultRole":"viewer"}`,
},
{
name: "Update Organization - invalid update",
@ -411,15 +401,11 @@ func TestService_UpdateOrganization(t *testing.T) {
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
httprouter.Params{
{
Key: "id",
Key: "oid",
Value: tt.id,
},
}))
if tt.args.setPtr {
tt.args.org.Public = &tt.args.public
}
buf, _ := json.Marshal(tt.args.org)
tt.args.r.Body = ioutil.NopCloser(bytes.NewReader(buf))
s.UpdateOrganization(tt.args.w, tt.args.r)
@ -503,7 +489,7 @@ func TestService_RemoveOrganization(t *testing.T) {
tt.args.r = tt.args.r.WithContext(httprouter.WithParams(context.Background(),
httprouter.Params{
{
Key: "id",
Key: "oid",
Value: tt.id,
},
}))
@ -573,16 +559,54 @@ func TestService_NewOrganization(t *testing.T) {
OrganizationsStore: &mocks.OrganizationsStore{
AddF: func(ctx context.Context, o *chronograf.Organization) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "1337",
Name: "The Good Place",
Public: false,
ID: "1337",
Name: "The Good Place",
}, nil
},
},
},
wantStatus: http.StatusCreated,
wantContentType: "application/json",
wantBody: `{"id":"1337","public":false,"name":"The Good Place","links":{"self":"/chronograf/v1/organizations/1337"}}`,
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: log.New(log.DebugLevel),
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",

View File

@ -1,9 +1,11 @@
package server
import (
"fmt"
"net/http"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
)
// AuthRoute are the routes for each type of OAuth2 provider
@ -31,6 +33,7 @@ func (r *AuthRoutes) Lookup(provider string) (AuthRoute, bool) {
type getRoutesResponse struct {
Layouts string `json:"layouts"` // Location of the layouts endpoint
Users string `json:"users"` // Location of the users endpoint
AllUsers string `json:"allUsers"` // Location of the raw users endpoint
Organizations string `json:"organizations"` // Location of the organizations endpoint
Mappings string `json:"mappings"` // Location of the application mappings endpoint
Sources string `json:"sources"` // Location of the sources endpoint
@ -47,14 +50,15 @@ type getRoutesResponse struct {
// external links for the client to know about, such as for JSON feeds or custom side nav buttons.
// Optionally, routes for authentication can be returned.
type AllRoutes struct {
AuthRoutes []AuthRoute // Location of all auth routes. If no auth, this can be empty.
LogoutLink string // Location of the logout route for all auth routes. If no auth, this can be empty.
StatusFeed string // External link to the JSON Feed for the News Feed on the client's Status Page
CustomLinks map[string]string // Custom external links for client's User menu, as passed in via CLI/ENV
Logger chronograf.Logger
GetPrincipal func(r *http.Request) oauth2.Principal // GetPrincipal is used to retrieve the principal on http request.
AuthRoutes []AuthRoute // Location of all auth routes. If no auth, this can be empty.
LogoutLink string // Location of the logout route for all auth routes. If no auth, this can be empty.
StatusFeed string // External link to the JSON Feed for the News Feed on the client's Status Page
CustomLinks map[string]string // Custom external links for client's User menu, as passed in via CLI/ENV
Logger chronograf.Logger
}
// ServeHTTP returns all top level routes and external links within chronograf
// serveHTTP returns all top level routes and external links within chronograf
func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
customLinks, err := NewCustomLinks(a.CustomLinks)
if err != nil {
@ -62,10 +66,20 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
org := "default"
if a.GetPrincipal != nil {
// If there is a principal, use the organization to populate the users routes
// otherwise use the default organization
if p := a.GetPrincipal(r); p.Organization != "" {
org = p.Organization
}
}
routes := getRoutesResponse{
Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts",
Users: "/chronograf/v1/users",
Users: fmt.Sprintf("/chronograf/v1/organizations/%s/users", org),
AllUsers: "/chronograf/v1/users",
Organizations: "/chronograf/v1/organizations",
Me: "/chronograf/v1/me",
Environment: "/chronograf/v1/env",

View File

@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutes not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/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","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
want := `{"layouts":"/chronograf/v1/layouts","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","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}}
`
if want != string(body) {
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/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","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":""}}
want := `{"layouts":"/chronograf/v1/layouts","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","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":""}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/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","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
want := `{"layouts":"/chronograf/v1/layouts","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","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))

View File

@ -485,6 +485,7 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s
OrganizationsStore: organizations,
UsersStore: db.UsersStore,
ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore,
},
Logger: logger,
UseAuth: useAuth,

View File

@ -48,7 +48,8 @@ func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chr
}
if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" {
tls := strings.Contains(src.MetaURL, "https")
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, client)
insecure := src.InsecureSkipVerify
return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, insecure, client)
}
return client, nil
}

View File

@ -88,6 +88,7 @@ type DataStore interface {
Layouts(ctx context.Context) chronograf.LayoutsStore
Users(ctx context.Context) chronograf.UsersStore
Organizations(ctx context.Context) chronograf.OrganizationsStore
Mappings(ctx context.Context) chronograf.MappingsStore
Dashboards(ctx context.Context) chronograf.DashboardsStore
Config(ctx context.Context) chronograf.ConfigStore
}
@ -102,6 +103,7 @@ type Store struct {
LayoutsStore chronograf.LayoutsStore
UsersStore chronograf.UsersStore
DashboardsStore chronograf.DashboardsStore
MappingsStore chronograf.MappingsStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
}
@ -191,3 +193,14 @@ func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
}
return &noop.ConfigStore{}
}
// Mappings returns the underlying MappingsStore.
func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
if isServer := hasServerContext(ctx); isServer {
return s.MappingsStore
}
if isSuperAdmin := hasSuperAdminContext(ctx); isSuperAdmin {
return s.MappingsStore
}
return &noop.MappingsStore{}
}

View File

@ -3,7 +3,7 @@
"info": {
"title": "Chronograf",
"description": "API endpoints for Chronograf",
"version": "1.4.0.0"
"version": "1.4.1.3"
},
"schemes": ["http"],
"basePath": "/chronograf/v1",
@ -550,7 +550,8 @@
"patch": {
"tags": ["sources", "users"],
"summary": "Update user configuration",
"description": "Update one parameter at a time (one of password, permissions or roles)",
"description":
"Update one parameter at a time (one of password, permissions or roles)",
"parameters": [
{
"name": "id",
@ -3964,6 +3965,24 @@
"$ref": "#/definitions/DashboardColor"
}
},
"legend": {
"description":
"Legend define encoding of the data into a cell's legend",
"type": "object",
"properties": {
"type": {
"description": "type is the style of the legend",
"type": "string",
"enum": ["static"]
},
"orientation": {
"description":
"orientation is the location of the legend with respect to the cell graph",
"type": "string",
"enum": ["top", "bottom", "left", "right"]
}
}
},
"links": {
"type": "object",
"properties": {

View File

@ -41,15 +41,15 @@ func (r *userRequest) ValidCreate() error {
}
func (r *userRequest) ValidUpdate() error {
if len(r.Roles) == 0 {
if r.Roles == nil {
return fmt.Errorf("No Roles to update")
}
return r.ValidRoles()
}
func (r *userRequest) ValidRoles() error {
orgs := map[string]bool{}
if len(r.Roles) > 0 {
orgs := map[string]bool{}
for _, r := range r.Roles {
if r.Organization == "" {
return fmt.Errorf("no organization was provided")
@ -59,10 +59,10 @@ func (r *userRequest) ValidRoles() error {
}
orgs[r.Organization] = true
switch r.Name {
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName, roles.WildcardRoleName:
continue
default:
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', and 'admin'", r.Name)
return fmt.Errorf("Unknown role %s. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'", r.Name)
}
}
}
@ -79,13 +79,19 @@ type userResponse struct {
Roles []chronograf.Role `json:"roles"`
}
func newUserResponse(u *chronograf.User) *userResponse {
func newUserResponse(u *chronograf.User, org string) *userResponse {
// This ensures that any user response with no roles returns an empty array instead of
// null when marshaled into JSON. That way, JavaScript doesn't need any guard on the
// key existing and it can simply be iterated over.
if u.Roles == nil {
u.Roles = []chronograf.Role{}
}
var selfLink string
if org != "" {
selfLink = fmt.Sprintf("/chronograf/v1/organizations/%s/users/%d", org, u.ID)
} else {
selfLink = fmt.Sprintf("/chronograf/v1/users/%d", u.ID)
}
return &userResponse{
ID: u.ID,
Name: u.Name,
@ -94,7 +100,7 @@ func newUserResponse(u *chronograf.User) *userResponse {
Roles: u.Roles,
SuperAdmin: u.SuperAdmin,
Links: selfLinks{
Self: fmt.Sprintf("/chronograf/v1/users/%d", u.ID),
Self: selfLink,
},
}
}
@ -104,18 +110,25 @@ type usersResponse struct {
Users []*userResponse `json:"users"`
}
func newUsersResponse(users []chronograf.User) *usersResponse {
func newUsersResponse(users []chronograf.User, org string) *usersResponse {
usersResp := make([]*userResponse, len(users))
for i, user := range users {
usersResp[i] = newUserResponse(&user)
usersResp[i] = newUserResponse(&user, org)
}
sort.Slice(usersResp, func(i, j int) bool {
return usersResp[i].ID < usersResp[j].ID
})
var selfLink string
if org != "" {
selfLink = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org)
} else {
selfLink = "/chronograf/v1/users"
}
return &usersResponse{
Users: usersResp,
Links: selfLinks{
Self: "/chronograf/v1/users",
Self: selfLink,
},
}
}
@ -136,7 +149,9 @@ func (s *Service) UserID(w http.ResponseWriter, r *http.Request) {
return
}
res := newUserResponse(user)
orgID := httprouter.GetParamFromContext(ctx, "oid")
res := newUserResponse(user, orgID)
location(w, res.Links.Self)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
@ -162,6 +177,11 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
return
}
if err := s.validRoles(serverCtx, req.Roles); err != nil {
invalidData(w, err, s.Logger)
return
}
user := &chronograf.User{
Name: req.Name,
Provider: req.Provider,
@ -184,7 +204,8 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
return
}
cu := newUserResponse(res)
orgID := httprouter.GetParamFromContext(ctx, "oid")
cu := newUserResponse(res, orgID)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusCreated, cu, s.Logger)
}
@ -204,15 +225,6 @@ func (s *Service) RemoveUser(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
ctxUser, ok := hasUserContext(ctx)
if !ok {
Error(w, http.StatusBadRequest, "failed to retrieve user from context", s.Logger)
return
}
if ctxUser.ID == u.ID {
Error(w, http.StatusForbidden, "user cannot delete themselves", s.Logger)
return
}
if err := s.Store.Users(ctx).Delete(ctx, u); err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
@ -248,6 +260,12 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
serverCtx := serverContext(ctx)
if err := s.validRoles(serverCtx, req.Roles); err != nil {
invalidData(w, err, s.Logger)
return
}
// ValidUpdate should ensure that req.Roles is not nil
u.Roles = req.Roles
@ -299,7 +317,8 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
cu := newUserResponse(u)
orgID := httprouter.GetParamFromContext(ctx, "oid")
cu := newUserResponse(u, orgID)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusOK, cu, s.Logger)
}
@ -314,7 +333,8 @@ func (s *Service) Users(w http.ResponseWriter, r *http.Request) {
return
}
res := newUsersResponse(users)
orgID := httprouter.GetParamFromContext(ctx, "oid")
res := newUsersResponse(users, orgID)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
@ -341,3 +361,19 @@ func setSuperAdmin(ctx context.Context, req userRequest, user *chronograf.User)
return nil
}
func (s *Service) validRoles(ctx context.Context, rs []chronograf.Role) error {
for i, role := range rs {
// verify that the organization exists
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &role.Organization})
if err != nil {
return err
}
if role.Name == roles.WildcardRoleName {
role.Name = org.DefaultRole
rs[i] = role
}
}
return nil
}

View File

@ -112,9 +112,10 @@ func TestService_UserID(t *testing.T) {
func TestService_NewUser(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
ConfigStore chronograf.ConfigStore
Logger chronograf.Logger
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
@ -204,6 +205,25 @@ func TestService_NewUser(t *testing.T) {
},
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
case "2":
return &chronograf.Organization{
ID: "2",
Name: "another",
DefaultRole: roles.MemberRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
@ -427,14 +447,93 @@ func TestService_NewUser(t *testing.T) {
wantContentType: "application/json",
wantBody: `{"id":"1338","superAdmin":true,"name":"bob","provider":"github","scheme":"oauth2","roles":[],"links":{"self":"/chronograf/v1/users/1338"}}`,
},
{
name: "Create a new Chronograf User with multiple roles with wildcard default role",
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"POST",
"http://any.url",
nil,
),
user: &userRequest{
Name: "bob",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1",
},
{
Name: roles.WildcardRoleName,
Organization: "2",
},
},
},
},
fields: fields{
Logger: log.New(log.DebugLevel),
ConfigStore: &mocks.ConfigStore{
Config: &chronograf.Config{
Auth: chronograf.AuthConfig{
SuperAdminNewUsers: false,
},
},
},
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
case "2":
return &chronograf.Organization{
ID: "2",
Name: "another",
DefaultRole: roles.MemberRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
AddF: func(ctx context.Context, user *chronograf.User) (*chronograf.User, error) {
return &chronograf.User{
ID: 1338,
Name: "bob",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1",
},
{
Name: roles.MemberRoleName,
Organization: "2",
},
},
}, nil
},
},
},
wantStatus: http.StatusCreated,
wantContentType: "application/json",
wantBody: `{"id":"1338","superAdmin":false,"name":"bob","provider":"github","scheme":"oauth2","roles":[{"name":"admin","organization":"1"},{"name":"member","organization":"2"}],"links":{"self":"/chronograf/v1/users/1338"}}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
UsersStore: tt.fields.UsersStore,
ConfigStore: tt.fields.ConfigStore,
UsersStore: tt.fields.UsersStore,
ConfigStore: tt.fields.ConfigStore,
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
@ -564,8 +663,8 @@ func TestService_RemoveUser(t *testing.T) {
},
id: "1339",
},
wantStatus: http.StatusForbidden,
wantBody: `{"code":403,"message":"user cannot delete themselves"}`,
wantStatus: http.StatusNoContent,
wantBody: ``,
},
}
for _, tt := range tests {
@ -613,8 +712,9 @@ func TestService_RemoveUser(t *testing.T) {
func TestService_UpdateUser(t *testing.T) {
type fields struct {
UsersStore chronograf.UsersStore
Logger chronograf.Logger
UsersStore chronograf.UsersStore
OrganizationsStore chronograf.OrganizationsStore
Logger chronograf.Logger
}
type args struct {
w *httptest.ResponseRecorder
@ -631,10 +731,76 @@ func TestService_UpdateUser(t *testing.T) {
wantContentType string
wantBody string
}{
{
name: "Update a Chronograf user - no roles",
fields: fields{
Logger: log.New(log.DebugLevel),
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
},
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
switch *q.ID {
case 1336:
return &chronograf.User{
ID: 1336,
Name: "bobbetta",
Provider: "github",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.EditorRoleName,
Organization: "1",
},
},
}, nil
default:
return nil, fmt.Errorf("User with ID %d not found", *q.ID)
}
},
},
},
args: args{
w: httptest.NewRecorder(),
r: httptest.NewRequest(
"PATCH",
"http://any.url",
nil,
),
userKeyUser: &chronograf.User{
ID: 0,
Name: "coolUser",
Provider: "github",
Scheme: "oauth2",
SuperAdmin: false,
},
user: &userRequest{
ID: 1336,
Roles: []chronograf.Role{},
},
},
id: "1336",
wantStatus: http.StatusOK,
wantContentType: "application/json",
wantBody: `{"id":"1336","superAdmin":false,"name":"bobbetta","provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/users/1336"},"roles":[]}`,
},
{
name: "Update a Chronograf user",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -693,6 +859,25 @@ func TestService_UpdateUser(t *testing.T) {
name: "Update a Chronograf user roles different orgs",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
case "2":
return &chronograf.Organization{
ID: "2",
Name: "another",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -804,6 +989,19 @@ func TestService_UpdateUser(t *testing.T) {
name: "SuperAdmin modifying their own SuperAdmin Status - user missing from context",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -857,6 +1055,19 @@ func TestService_UpdateUser(t *testing.T) {
name: "SuperAdmin modifying their own SuperAdmin Status",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -917,6 +1128,19 @@ func TestService_UpdateUser(t *testing.T) {
name: "Update a SuperAdmin's Roles - without super admin context",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -977,6 +1201,19 @@ func TestService_UpdateUser(t *testing.T) {
name: "Update a Chronograf user to super admin - without super admin context",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -1033,6 +1270,19 @@ func TestService_UpdateUser(t *testing.T) {
name: "Update a Chronograf user to super admin - with super admin context",
fields: fields{
Logger: log.New(log.DebugLevel),
OrganizationsStore: &mocks.OrganizationsStore{
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
switch *q.ID {
case "1":
return &chronograf.Organization{
ID: "1",
Name: "org",
DefaultRole: roles.ViewerRoleName,
}, nil
}
return nil, fmt.Errorf("org not found")
},
},
UsersStore: &mocks.UsersStore{
UpdateF: func(ctx context.Context, user *chronograf.User) error {
return nil
@ -1090,7 +1340,8 @@ func TestService_UpdateUser(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
UsersStore: tt.fields.UsersStore,
UsersStore: tt.fields.UsersStore,
OrganizationsStore: tt.fields.OrganizationsStore,
},
Logger: tt.fields.Logger,
}
@ -1354,7 +1605,7 @@ func TestUserRequest_ValidCreate(t *testing.T) {
},
},
wantErr: true,
err: fmt.Errorf("Unknown role BilliettaSpecialRole. Valid roles are 'member', 'viewer', 'editor', and 'admin'"),
err: fmt.Errorf("Unknown role BilliettaSpecialRole. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
},
{
name: "Invalid roles - missing organization",
@ -1444,7 +1695,39 @@ func TestUserRequest_ValidUpdate(t *testing.T) {
},
},
wantErr: true,
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', and 'admin'"),
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
},
{
name: "Valid roles empty",
args: args{
u: &userRequest{
ID: 1337,
Name: "billietta",
Provider: "auth0",
Scheme: "oauth2",
Roles: []chronograf.Role{},
},
},
wantErr: false,
},
{
name: "Invalid - bad role name",
args: args{
u: &userRequest{
ID: 1337,
Name: "billietta",
Provider: "auth0",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: "BillietaSpecialOrg",
Organization: "0",
},
},
},
},
wantErr: true,
err: fmt.Errorf("Unknown role BillietaSpecialOrg. Valid roles are 'member', 'viewer', 'editor', 'admin', and '*'"),
},
{
name: "Invalid - duplicate organization",

View File

@ -1,6 +1,6 @@
{
"name": "chronograf-ui",
"version": "1.4.0-0",
"version": "1.4.1-3",
"private": false,
"license": "AGPL-3.0",
"description": "",

View File

@ -2,7 +2,7 @@ import _ from 'lodash'
import linksReducer from 'shared/reducers/links'
import {linksReceived} from 'shared/actions/links'
import {linksGetCompleted} from 'shared/actions/links'
import {noop} from 'shared/actions/app'
const links = {
@ -25,11 +25,10 @@ const links = {
}
describe('Shared.Reducers.linksReducer', () => {
it('can handle LINKS_RECEIVED', () => {
it('can handle LINKS_GET_COMPLETED', () => {
const initial = linksReducer(undefined, noop())
const actual = linksReducer(initial, linksReceived(links))
const actual = linksReducer(initial, linksGetCompleted(links))
const expected = links
expect(_.isEqual(actual, expected)).to.equal(true)
})
})

View File

@ -36,13 +36,15 @@ class CheckSources extends Component {
}
async componentWillMount() {
const {auth: {isUsingAuth, me}} = this.props
const {router, auth: {isUsingAuth, me}} = this.props
if (!isUsingAuth || isUserAuthorized(me.role, VIEWER_ROLE)) {
await this.props.getSources()
this.setState({isFetching: false})
} else {
router.push('/purgatory')
return
}
this.setState({isFetching: false})
}
shouldComponentUpdate(nextProps) {
@ -66,7 +68,7 @@ class CheckSources extends Component {
params,
errorThrown,
sources,
auth: {isUsingAuth, me, me: {organizations, currentOrganization}},
auth: {isUsingAuth, me, me: {organizations = [], currentOrganization}},
notify,
getSources,
} = nextProps
@ -81,6 +83,14 @@ class CheckSources extends Component {
return router.push('/')
}
if (!isFetching && isUsingAuth && !organizations.length) {
notify(
'error',
'You have been removed from all organizations. Please contact your administrator.'
)
return router.push('/purgatory')
}
if (
me.superAdmin &&
!organizations.find(o => o.id === currentOrganization.id)

View File

@ -10,6 +10,10 @@ import {
createOrganization as createOrganizationAJAX,
updateOrganization as updateOrganizationAJAX,
deleteOrganization as deleteOrganizationAJAX,
getMappings as getMappingsAJAX,
createMapping as createMappingAJAX,
updateMapping as updateMappingAJAX,
deleteMapping as deleteMappingAJAX,
} from 'src/admin/apis/chronograf'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
@ -94,6 +98,35 @@ export const removeOrganization = organization => ({
},
})
export const loadMappings = ({mappings}) => ({
type: 'CHRONOGRAF_LOAD_MAPPINGS',
payload: {
mappings,
},
})
export const updateMapping = (staleMapping, updatedMapping) => ({
type: 'CHRONOGRAF_UPDATE_MAPPING',
payload: {
staleMapping,
updatedMapping,
},
})
export const addMapping = mapping => ({
type: 'CHRONOGRAF_ADD_MAPPING',
payload: {
mapping,
},
})
export const removeMapping = mapping => ({
type: 'CHRONOGRAF_REMOVE_MAPPING',
payload: {
mapping,
},
})
// async actions (thunks)
export const loadUsersAsync = url => async dispatch => {
try {
@ -113,6 +146,62 @@ export const loadOrganizationsAsync = url => async dispatch => {
}
}
export const loadMappingsAsync = () => async dispatch => {
try {
const {data} = await getMappingsAJAX()
dispatch(loadMappings(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const createMappingAsync = (url, mapping) => async dispatch => {
const mappingWithTempId = {...mapping, _tempID: uuid.v4()}
dispatch(addMapping(mappingWithTempId))
try {
const {data} = await createMappingAJAX(url, mapping)
dispatch(updateMapping(mappingWithTempId, data))
} catch (error) {
const message = `${_.upperFirst(
_.toLower(error.data.message)
)}: Scheme: ${mapping.scheme} Provider: ${mapping.provider}`
dispatch(errorThrown(error, message))
setTimeout(
() => dispatch(removeMapping(mappingWithTempId)),
REVERT_STATE_DELAY
)
}
}
export const deleteMappingAsync = mapping => async dispatch => {
dispatch(removeMapping(mapping))
try {
await deleteMappingAJAX(mapping)
dispatch(
publishAutoDismissingNotification(
'success',
`Mapping deleted: ${mapping.id} ${mapping.scheme}`
)
)
} catch (error) {
dispatch(errorThrown(error))
dispatch(addMapping(mapping))
}
}
export const updateMappingAsync = (
staleMapping,
updatedMapping
) => async dispatch => {
dispatch(updateMapping(staleMapping, updatedMapping))
try {
await updateMappingAJAX(updatedMapping)
} catch (error) {
dispatch(errorThrown(error))
dispatch(updateMapping(updatedMapping, staleMapping))
}
}
export const createUserAsync = (url, user) => async dispatch => {
// temp uuid is added to be able to disambiguate a created user that has the
// same scheme, provider, and name as an existing user
@ -131,7 +220,11 @@ export const createUserAsync = (url, user) => async dispatch => {
}
}
export const updateUserAsync = (user, updatedUser) => async dispatch => {
export const updateUserAsync = (
user,
updatedUser,
successMessage
) => async dispatch => {
dispatch(updateUser(user, updatedUser))
try {
// currently the request will be rejected if name, provider, or scheme, or
@ -145,12 +238,7 @@ export const updateUserAsync = (user, updatedUser) => async dispatch => {
provider: null,
scheme: null,
})
dispatch(
publishAutoDismissingNotification(
'success',
`User updated: ${user.scheme}::${user.provider}::${user.name}`
)
)
dispatch(publishAutoDismissingNotification('success', successMessage))
// it's not necessary to syncUser again but it's useful for good
// measure and for the clarity of insight in the redux story
dispatch(syncUser(user, data))
@ -160,14 +248,19 @@ export const updateUserAsync = (user, updatedUser) => async dispatch => {
}
}
export const deleteUserAsync = user => async dispatch => {
export const deleteUserAsync = (
user,
{isAbsoluteDelete} = {}
) => async dispatch => {
dispatch(removeUser(user))
try {
await deleteUserAJAX(user)
dispatch(
publishAutoDismissingNotification(
'success',
`User removed from organization: ${user.scheme}::${user.provider}::${user.name}`
`${user.name} has been removed from ${isAbsoluteDelete
? 'all organizations and deleted'
: 'the current organization'}`
)
)
} catch (error) {

View File

@ -102,3 +102,54 @@ export const deleteOrganization = async organization => {
throw error
}
}
// Mappings
export const createMapping = async (url, mapping) => {
try {
return await AJAX({
method: 'POST',
resource: 'mappings',
data: mapping,
})
} catch (error) {
console.error(error)
throw error
}
}
export const getMappings = async () => {
try {
return await AJAX({
method: 'GET',
resource: 'mappings',
})
} catch (error) {
console.error(error)
throw error
}
}
export const updateMapping = async mapping => {
try {
return await AJAX({
method: 'PUT',
url: mapping.links.self,
data: mapping,
})
} catch (error) {
console.error(error)
throw error
}
}
export const deleteMapping = async mapping => {
try {
return await AJAX({
method: 'DELETE',
url: mapping.links.self,
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -9,14 +9,30 @@ import {
import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs'
import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage'
import UsersPage from 'src/admin/containers/chronograf/UsersPage'
import ProvidersPage from 'src/admin/containers/ProvidersPage'
import AllUsersPage from 'src/admin/containers/chronograf/AllUsersPage'
const ORGANIZATIONS_TAB_NAME = 'Organizations'
const USERS_TAB_NAME = 'Users'
const ORGANIZATIONS_TAB_NAME = 'All Orgs'
const PROVIDERS_TAB_NAME = 'Org Mappings'
const CURRENT_ORG_USERS_TAB_NAME = 'Current Org'
const ALL_USERS_TAB_NAME = 'All Users'
const AdminTabs = ({
me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID},
}) => {
const tabs = [
{
requiredRole: ADMIN_ROLE,
type: CURRENT_ORG_USERS_TAB_NAME,
component: (
<UsersPage meID={meID} meCurrentOrganization={meCurrentOrganization} />
),
},
{
requiredRole: SUPERADMIN_ROLE,
type: ALL_USERS_TAB_NAME,
component: <AllUsersPage meID={meID} />,
},
{
requiredRole: SUPERADMIN_ROLE,
type: ORGANIZATIONS_TAB_NAME,
@ -25,11 +41,9 @@ const AdminTabs = ({
),
},
{
requiredRole: ADMIN_ROLE,
type: USERS_TAB_NAME,
component: (
<UsersPage meID={meID} meCurrentOrganization={meCurrentOrganization} />
),
requiredRole: SUPERADMIN_ROLE,
type: PROVIDERS_TAB_NAME,
component: <ProvidersPage />,
},
].filter(t => isUserAuthorized(meRole, t.requiredRole))

View File

@ -0,0 +1,215 @@
import React, {Component, PropTypes} from 'react'
import uuid from 'node-uuid'
import AllUsersTableHeader from 'src/admin/components/chronograf/AllUsersTableHeader'
import AllUsersTableRowNew from 'src/admin/components/chronograf/AllUsersTableRowNew'
import AllUsersTableRow from 'src/admin/components/chronograf/AllUsersTableRow'
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
const {
colOrganizations,
colProvider,
colScheme,
colSuperAdmin,
colActions,
} = ALL_USERS_TABLE
class AllUsersTable extends Component {
constructor(props) {
super(props)
this.state = {
isCreatingUser: false,
}
}
handleUpdateAuthConfig = fieldName => updatedValue => {
const {
actionsConfig: {updateAuthConfigAsync},
authConfig,
links,
} = this.props
const updatedAuthConfig = {
...authConfig,
[fieldName]: updatedValue,
}
updateAuthConfigAsync(links.config.auth, authConfig, updatedAuthConfig)
}
handleAddToOrganization = user => organization => {
// '*' tells the server to fill in the current defaultRole of that org
const newRoles = user.roles.concat({
organization: organization.id,
name: '*',
})
this.props.onUpdateUserRoles(
user,
newRoles,
`${user.name} has been added to ${organization.name}`
)
}
handleRemoveFromOrganization = user => role => {
const newRoles = user.roles.filter(
r => r.organization !== role.organization
)
const {name} = this.props.organizations.find(
o => o.id === role.organization
)
this.props.onUpdateUserRoles(
user,
newRoles,
`${user.name} has been removed from ${name}`
)
}
handleChangeSuperAdmin = user => newStatus => {
this.props.onUpdateUserSuperAdmin(user, newStatus)
}
handleClickCreateUser = () => {
this.setState({isCreatingUser: true})
}
handleBlurCreateUserRow = () => {
this.setState({isCreatingUser: false})
}
render() {
const {
users,
organizations,
onCreateUser,
authConfig,
meID,
notify,
onDeleteUser,
isLoading,
} = this.props
const {isCreatingUser} = this.state
if (isLoading) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="page-spinner" />
</div>
</div>
)
}
return (
<div className="panel panel-default">
<AllUsersTableHeader
numUsers={users.length}
numOrganizations={organizations.length}
onClickCreateUser={this.handleClickCreateUser}
isCreatingUser={isCreatingUser}
authConfig={authConfig}
onChangeAuthConfig={this.handleUpdateAuthConfig}
/>
<div className="panel-body">
<table className="table table-highlight v-center chronograf-admin-table">
<thead>
<tr>
<th>Username</th>
<th
style={{width: colOrganizations}}
className="align-with-col-text"
>
Organizations
</th>
<th style={{width: colProvider}}>Provider</th>
<th style={{width: colScheme}}>Scheme</th>
<th style={{width: colSuperAdmin}} className="text-center">
SuperAdmin
</th>
<th className="text-right" style={{width: colActions}} />
</tr>
</thead>
<tbody>
{users.length
? users.map(user =>
<AllUsersTableRow
user={user}
key={uuid.v4()}
organizations={organizations}
onAddToOrganization={this.handleAddToOrganization}
onRemoveFromOrganization={
this.handleRemoveFromOrganization
}
onChangeSuperAdmin={this.handleChangeSuperAdmin}
onDelete={onDeleteUser}
meID={meID}
/>
)
: <tr className="table-empty-state">
<th colSpan="6">
<p>No Users to display</p>
</th>
</tr>}
{isCreatingUser
? <AllUsersTableRowNew
organizations={organizations}
onBlur={this.handleBlurCreateUserRow}
onCreateUser={onCreateUser}
notify={notify}
/>
: null}
</tbody>
</table>
</div>
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
AllUsersTable.propTypes = {
links: shape({
config: shape({
auth: string.isRequired,
}).isRequired,
}).isRequired,
users: arrayOf(
shape({
id: string,
links: shape({
self: string.isRequired,
}),
name: string.isRequired,
provider: string.isRequired,
roles: arrayOf(
shape({
name: string.isRequired,
organization: string.isRequired,
})
),
scheme: string.isRequired,
superAdmin: bool,
})
).isRequired,
organizations: arrayOf(
shape({
name: string.isRequired,
id: string.isRequired,
})
),
onCreateUser: func.isRequired,
onUpdateUserRoles: func.isRequired,
onUpdateUserSuperAdmin: func.isRequired,
onDeleteUser: func.isRequired,
actionsConfig: shape({
getAuthConfigAsync: func.isRequired,
updateAuthConfigAsync: func.isRequired,
}),
authConfig: shape({
superAdminNewUsers: bool,
}),
meID: string.isRequired,
notify: func.isRequired,
isLoading: bool.isRequired,
}
export default AllUsersTable

View File

@ -0,0 +1,65 @@
import React, {PropTypes} from 'react'
import SlideToggle from 'shared/components/SlideToggle'
const AllUsersTableHeader = ({
numUsers,
numOrganizations,
onClickCreateUser,
isCreatingUser,
authConfig: {superAdminNewUsers},
onChangeAuthConfig,
}) => {
const numUsersString = `${numUsers} User${numUsers === 1 ? '' : 's'}`
const numOrganizationsString = `${numOrganizations} Org${numOrganizations ===
1
? ''
: 's'}`
return (
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{numUsersString} across {numOrganizationsString}
</h2>
<div style={{display: 'flex', alignItems: 'center'}}>
<div className="all-users-admin-toggle">
<SlideToggle
size="xs"
active={superAdminNewUsers}
onToggle={onChangeAuthConfig('superAdminNewUsers')}
/>
<span>All new users are SuperAdmins</span>
</div>
<button
className="btn btn-primary btn-sm"
onClick={onClickCreateUser}
disabled={isCreatingUser || !onClickCreateUser}
>
<span className="icon plus" />
Add User
</button>
</div>
</div>
)
}
const {bool, func, number, shape} = PropTypes
AllUsersTableHeader.defaultProps = {
numUsers: 0,
numOrganizations: 0,
isCreatingUser: false,
}
AllUsersTableHeader.propTypes = {
numUsers: number.isRequired,
numOrganizations: number.isRequired,
onClickCreateUser: func,
isCreatingUser: bool.isRequired,
onChangeAuthConfig: func.isRequired,
authConfig: shape({
superAdminNewUsers: bool,
}),
}
export default AllUsersTableHeader

View File

@ -0,0 +1,114 @@
import React, {PropTypes} from 'react'
import Tags from 'shared/components/Tags'
import SlideToggle from 'shared/components/SlideToggle'
import ConfirmButton from 'shared/components/ConfirmButton'
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
const {
colOrganizations,
colProvider,
colScheme,
colSuperAdmin,
colActions,
} = ALL_USERS_TABLE
const AllUsersTableRow = ({
organizations,
user,
onAddToOrganization,
onRemoveFromOrganization,
onChangeSuperAdmin,
onDelete,
meID,
}) => {
const dropdownOrganizationsItems = organizations
.filter(o => !user.roles.find(role => role.organization === o.id))
.map(o => ({
...o,
text: o.name,
}))
const userIsMe = user.id === meID
const userOrganizations = user.roles.map(r => ({
...r,
name: organizations.find(o => r.organization === o.id).name,
}))
const wrappedDelete = () => onDelete(user)
const removeWarning = userIsMe
? 'Delete your user record\nand log yourself out?'
: 'Delete this user?'
return (
<tr className={'chronograf-admin-table--user'}>
<td>
{userIsMe
? <strong className="chronograf-user--me">
<span className="icon user" />
{user.name}
</strong>
: <strong>
{user.name}
</strong>}
</td>
<td style={{width: colOrganizations}}>
<Tags
tags={userOrganizations}
onDeleteTag={onRemoveFromOrganization(user)}
emptyStateText="None"
addMenuItems={dropdownOrganizationsItems}
addMenuChoose={onAddToOrganization(user)}
/>
</td>
<td style={{width: colProvider}}>
{user.provider}
</td>
<td style={{width: colScheme}}>
{user.scheme}
</td>
<td style={{width: colSuperAdmin}} className="text-center">
<SlideToggle
active={user.superAdmin}
onToggle={onChangeSuperAdmin(user)}
size="xs"
disabled={userIsMe}
/>
</td>
<td style={{textAlign: 'right', width: colActions}}>
<ConfirmButton
confirmText={removeWarning}
confirmAction={wrappedDelete}
size="btn-xs"
text="Remove"
customClass="table--show-on-row-hover"
/>
</td>
</tr>
)
}
const {arrayOf, func, shape, string} = PropTypes
AllUsersTableRow.propTypes = {
user: shape(),
organization: shape({
name: string.isRequired,
id: string.isRequired,
}),
onAddToOrganization: func.isRequired,
onRemoveFromOrganization: func.isRequired,
onChangeSuperAdmin: func.isRequired,
onDelete: func.isRequired,
meID: string.isRequired,
organizations: arrayOf(
shape({
id: string.isRequired,
name: string.isRequired,
})
),
}
export default AllUsersTableRow

View File

@ -0,0 +1,183 @@
import React, {Component, PropTypes} from 'react'
import Dropdown from 'shared/components/Dropdown'
import {ALL_USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
const {
colOrganizations,
colProvider,
colScheme,
colSuperAdmin,
colActions,
} = ALL_USERS_TABLE
const nullOrganization = {id: undefined, name: 'None'}
const nullRole = {name: '*', organization: undefined}
class AllUsersTableRowNew extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
provider: '',
scheme: 'oauth2',
role: {
...nullRole,
},
}
}
handleInputChange = fieldName => e => {
this.setState({[fieldName]: e.target.value.trim()})
}
handleConfirmCreateUser = () => {
const {onBlur, onCreateUser} = this.props
const {name, provider, scheme, role, superAdmin} = this.state
const newUser = {
name,
provider,
scheme,
superAdmin,
// since you can only choose one organization, there is only one role in a new row
// if no organization is selected ie the "None" organization,
// then set roles to an empty array
roles: role.organization === undefined ? [] : [role],
}
onCreateUser(newUser)
onBlur()
}
handleInputFocus = e => {
e.target.select()
}
handleSelectOrganization = newOrganization => {
// if "None" was selected for organization, create a "null role" from the predefined null role
// else create a new role with the organization as the newOrganization's id
const newRole =
newOrganization.id === undefined
? {
...nullRole,
}
: {
organization: newOrganization.id,
name: '*', // '*' causes the server to determine the current defaultRole of the selected organization
}
this.setState({role: newRole})
}
handleKeyDown = e => {
const {name, provider} = this.state
const preventCreate = !name || !provider
if (e.key === 'Escape') {
this.props.onBlur()
}
if (e.key === 'Enter') {
if (preventCreate) {
return this.props.notify(
'warning',
'User must have a name and provider'
)
}
this.handleConfirmCreateUser()
}
}
render() {
const {organizations, onBlur} = this.props
const {name, provider, scheme, role} = this.state
const dropdownOrganizationsItems = [
{...nullOrganization},
...organizations,
].map(o => ({
...o,
text: o.name,
}))
const selectedRole = dropdownOrganizationsItems.find(
o => role.organization === o.id
)
const preventCreate = !name || !provider
return (
<tr className="chronograf-admin-table--new-user">
<td>
<input
className="form-control input-xs"
type="text"
placeholder="OAuth Username..."
autoFocus={true}
value={name}
onChange={this.handleInputChange('name')}
onKeyDown={this.handleKeyDown}
/>
</td>
<td style={{width: colOrganizations}}>
<Dropdown
items={dropdownOrganizationsItems}
selected={selectedRole.text}
onChoose={this.handleSelectOrganization}
buttonColor="btn-primary"
buttonSize="btn-xs"
className="dropdown-stretch"
/>
</td>
<td style={{width: colProvider}}>
<input
className="form-control input-xs"
type="text"
placeholder="OAuth Provider..."
value={provider}
onChange={this.handleInputChange('provider')}
onKeyDown={this.handleKeyDown}
/>
</td>
<td style={{width: colScheme}}>
<input
className="form-control input-xs disabled"
type="text"
disabled={true}
placeholder="OAuth Scheme..."
value={scheme}
/>
</td>
<td style={{width: colSuperAdmin}} className="text-center">
&mdash;
</td>
<td className="text-right" style={{width: colActions}}>
<button className="btn btn-xs btn-square btn-info" onClick={onBlur}>
<span className="icon remove" />
</button>
<button
className="btn btn-xs btn-square btn-success"
disabled={preventCreate}
onClick={this.handleConfirmCreateUser}
>
<span className="icon checkmark" />
</button>
</td>
</tr>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
AllUsersTableRowNew.propTypes = {
organizations: arrayOf(
shape({
id: string.isRequired,
name: string.isRequired,
})
),
onBlur: func.isRequired,
onCreateUser: func.isRequired,
notify: func.isRequired,
}
export default AllUsersTableRowNew

View File

@ -1,46 +0,0 @@
import React from 'react'
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
const EmptyUsersTable = () => {
const {
colRole,
colSuperAdmin,
colProvider,
colScheme,
colActions,
} = USERS_TABLE
return (
<div className="panel panel-default">
<UsersTableHeader />
<div className="panel-body">
<table className="table table-highlight v-center chronograf-admin-table">
<thead>
<tr>
<th>Username</th>
<th style={{width: colRole}} className="align-with-col-text">
Role
</th>
<Authorized requiredRole={SUPERADMIN_ROLE}>
<th style={{width: colSuperAdmin}} className="text-center">
SuperAdmin
</th>
</Authorized>
<th style={{width: colProvider}}>Provider</th>
<th style={{width: colScheme}}>Scheme</th>
<th className="text-right" style={{width: colActions}} />
</tr>
</thead>
<tbody />
</table>
</div>
</div>
)
}
export default EmptyUsersTable

View File

@ -2,14 +2,8 @@ import React, {Component, PropTypes} from 'react'
import uuid from 'node-uuid'
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
import OrganizationsTableRowNew from 'src/admin/components/chronograf/OrganizationsTableRowNew'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
import SlideToggle from 'shared/components/SlideToggle'
import {PUBLIC_TOOLTIP} from 'src/admin/constants/index'
class OrganizationsTable extends Component {
constructor(props) {
@ -40,10 +34,7 @@ class OrganizationsTable extends Component {
onDeleteOrg,
onRenameOrg,
onChooseDefaultRole,
onTogglePublic,
currentOrganization,
authConfig: {superAdminNewUsers},
onChangeAuthConfig,
} = this.props
const {isCreatingOrganization} = this.state
@ -52,6 +43,15 @@ class OrganizationsTable extends Component {
? ''
: 's'}`
if (!organizations.length) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="page-spinner" />
</div>
</div>
)
}
return (
<div className="panel panel-default">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
@ -67,15 +67,13 @@ class OrganizationsTable extends Component {
</button>
</div>
<div className="panel-body">
<div className="orgs-table--org-labels">
<div className="orgs-table--active" />
<div className="orgs-table--name">Name</div>
<div className="orgs-table--public">
Public{' '}
<QuestionMarkTooltip tipID="public" tipContent={PUBLIC_TOOLTIP} />
<div className="fancytable--labels">
<div className="fancytable--th orgs-table--active" />
<div className="fancytable--th orgs-table--name">Name</div>
<div className="fancytable--th orgs-table--default-role">
Default Role
</div>
<div className="orgs-table--default-role">Default Role</div>
<div className="orgs-table--delete" />
<div className="fancytable--th orgs-table--delete" />
</div>
{isCreatingOrganization
? <OrganizationsTableRowNew
@ -87,42 +85,19 @@ class OrganizationsTable extends Component {
<OrganizationsTableRow
key={uuid.v4()}
organization={org}
onTogglePublic={onTogglePublic}
onDelete={onDeleteOrg}
onRename={onRenameOrg}
onChooseDefaultRole={onChooseDefaultRole}
currentOrganization={currentOrganization}
/>
)}
<Authorized requiredRole={SUPERADMIN_ROLE}>
<table className="table v-center superadmin-config">
<thead>
<tr>
<th style={{width: 70}}>Config</th>
<th />
</tr>
</thead>
<tbody>
<tr>
<td style={{width: 70}}>
<SlideToggle
size="xs"
active={superAdminNewUsers}
onToggle={onChangeAuthConfig('superAdminNewUsers')}
/>
</td>
<td>All new users are SuperAdmins</td>
</tr>
</tbody>
</table>
</Authorized>
</div>
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
const {arrayOf, func, shape, string} = PropTypes
OrganizationsTable.propTypes = {
organizations: arrayOf(
@ -138,11 +113,6 @@ OrganizationsTable.propTypes = {
onCreateOrg: func.isRequired,
onDeleteOrg: func.isRequired,
onRenameOrg: func.isRequired,
onTogglePublic: func.isRequired,
onChooseDefaultRole: func.isRequired,
onChangeAuthConfig: func.isRequired,
authConfig: shape({
superAdminNewUsers: bool,
}),
}
export default OrganizationsTable

View File

@ -3,9 +3,9 @@ import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {withRouter} from 'react-router'
import SlideToggle from 'shared/components/SlideToggle'
import ConfirmButtons from 'shared/components/ConfirmButtons'
import Dropdown from 'shared/components/Dropdown'
import InputClickToEdit from 'shared/components/InputClickToEdit'
import {meChangeOrganizationAsync} from 'shared/actions/auth'
@ -32,9 +32,7 @@ class OrganizationsTableRow extends Component {
super(props)
this.state = {
isEditing: false,
isDeleting: false,
workingName: this.props.organization.name,
}
}
@ -44,55 +42,10 @@ class OrganizationsTableRow extends Component {
await meChangeOrganization(links.me, {organization: organization.id})
router.push('')
}
handleNameClick = () => {
this.setState({isEditing: true})
handleUpdateOrgName = newName => {
const {organization, onRename} = this.props
onRename(organization, newName)
}
handleConfirmRename = () => {
const {onRename, organization} = this.props
const {workingName} = this.state
onRename(organization, workingName)
this.setState({workingName, isEditing: false})
}
handleCancelRename = () => {
const {organization} = this.props
this.setState({
workingName: organization.name,
isEditing: false,
})
}
handleInputChange = e => {
this.setState({workingName: e.target.value})
}
handleInputBlur = () => {
const {organization} = this.props
const {workingName} = this.state
if (organization.name === workingName) {
this.handleCancelRename()
} else {
this.handleConfirmRename()
}
}
handleKeyDown = e => {
if (e.key === 'Enter') {
this.handleInputBlur()
} else if (e.key === 'Escape') {
this.handleCancelRename()
}
}
handleFocus = e => {
e.target.select()
}
handleDeleteClick = () => {
this.setState({isDeleting: true})
}
@ -106,18 +59,13 @@ class OrganizationsTableRow extends Component {
onDelete(organization)
}
handleTogglePublic = () => {
const {organization, onTogglePublic} = this.props
onTogglePublic(organization)
}
handleChooseDefaultRole = role => {
const {organization, onChooseDefaultRole} = this.props
onChooseDefaultRole(organization, role.name)
}
render() {
const {workingName, isEditing, isDeleting} = this.state
const {isDeleting} = this.state
const {organization, currentOrganization} = this.props
const dropdownRolesItems = USER_ROLES.map(role => ({
@ -126,12 +74,12 @@ class OrganizationsTableRow extends Component {
}))
const defaultRoleClassName = isDeleting
? 'orgs-table--default-role editing'
: 'orgs-table--default-role'
? 'fancytable--td orgs-table--default-role deleting'
: 'fancytable--td orgs-table--default-role'
return (
<div className="orgs-table--org">
<div className="orgs-table--active">
<div className="fancytable--row">
<div className="fancytable--td orgs-table--active">
{organization.id === currentOrganization.id
? <button className="btn btn-sm btn-success">
<span className="icon checkmark" /> Current
@ -143,32 +91,11 @@ class OrganizationsTableRow extends Component {
<span className="icon shuffle" /> Switch to
</button>}
</div>
{isEditing
? <input
type="text"
className="form-control input-sm orgs-table--input"
defaultValue={workingName}
onChange={this.handleInputChange}
onBlur={this.handleInputBlur}
onKeyDown={this.handleKeyDown}
placeholder="Name this Organization..."
autoFocus={true}
onFocus={this.handleFocus}
ref={r => (this.inputRef = r)}
/>
: <div className="orgs-table--name" onClick={this.handleNameClick}>
{workingName}
<span className="icon pencil" />
</div>}
{organization.id === DEFAULT_ORG_ID
? <div className="orgs-table--public">
<SlideToggle
size="xs"
active={organization.public}
onToggle={this.handleTogglePublic}
/>
</div>
: <div className="orgs-table--public disabled">&mdash;</div>}
<InputClickToEdit
value={organization.name}
wrapperClass="fancytable--td orgs-table--name"
onUpdate={this.handleUpdateOrgName}
/>
<div className={defaultRoleClassName}>
<Dropdown
items={dropdownRolesItems}
@ -204,7 +131,6 @@ OrganizationsTableRow.propTypes = {
}).isRequired,
onDelete: func.isRequired,
onRename: func.isRequired,
onTogglePublic: func.isRequired,
onChooseDefaultRole: func.isRequired,
currentOrganization: shape({
name: string.isRequired,

View File

@ -58,20 +58,22 @@ class OrganizationsTableRowNew extends Component {
}))
return (
<div className="orgs-table--org orgs-table--new-org">
<div className="orgs-table--active">&mdash;</div>
<input
type="text"
className="form-control input-sm orgs-table--input"
value={name}
onKeyDown={this.handleKeyDown}
onChange={this.handleInputChange}
onFocus={this.handleInputFocus}
placeholder="Name this Organization..."
autoFocus={true}
ref={r => (this.inputRef = r)}
/>
<div className="orgs-table--default-role editing">
<div className="fancytable--row">
<div className="fancytable--td orgs-table--active">&mdash;</div>
<div className="fancytable--td orgs-table--name">
<input
type="text"
className="form-control input-sm"
value={name}
onKeyDown={this.handleKeyDown}
onChange={this.handleInputChange}
onFocus={this.handleInputFocus}
placeholder="Name this Organization..."
autoFocus={true}
ref={r => (this.inputRef = r)}
/>
</div>
<div className="fancytable--td orgs-table--default-role deleting">
<Dropdown
items={dropdownRolesItems}
onChoose={this.handleChooseDefaultRole}

View File

@ -0,0 +1,149 @@
import React, {Component, PropTypes} from 'react'
import uuid from 'node-uuid'
import ProvidersTableRow from 'src/admin/components/chronograf/ProvidersTableRow'
import ProvidersTableRowNew from 'src/admin/components/chronograf/ProvidersTableRowNew'
class ProvidersTable extends Component {
constructor(props) {
super(props)
this.state = {
isCreatingMap: false,
}
}
handleClickCreateMap = () => {
this.setState({isCreatingMap: true})
}
handleCancelCreateMap = () => {
this.setState({isCreatingMap: false})
}
handleCreateMap = newMap => {
this.props.onCreateMap(newMap)
this.setState({isCreatingMap: false})
}
render() {
const {
mappings = [],
organizations,
onUpdateMap,
onDeleteMap,
isLoading,
} = this.props
const {isCreatingMap} = this.state
const tableTitle =
mappings.length === 1 ? '1 Map' : `${mappings.length} Maps`
// define scheme options
const SCHEMES = [{text: '*'}, {text: 'oauth2'}]
if (isLoading) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="page-spinner" />
</div>
</div>
)
}
return (
<div className="panel panel-default">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{tableTitle}
</h2>
<button
className="btn btn-sm btn-primary"
onClick={this.handleClickCreateMap}
disabled={isCreatingMap}
>
<span className="icon plus" /> Create Mapping
</button>
</div>
{mappings.length || isCreatingMap
? <div className="panel-body">
<div className="fancytable--labels">
<div className="fancytable--th provider--scheme">Scheme</div>
<div className="fancytable--th provider--provider">
Provider
</div>
<div className="fancytable--th provider--providerorg">
Provider Org
</div>
<div className="fancytable--th provider--arrow" />
<div className="fancytable--th provider--redirect">
Organization
</div>
<div className="fancytable--th" />
<div className="fancytable--th provider--delete" />
</div>
{mappings.map((mapping, i) =>
<ProvidersTableRow
key={uuid.v4()}
mapping={mapping}
organizations={organizations}
schemes={SCHEMES}
onDelete={onDeleteMap}
onUpdate={onUpdateMap}
rowIndex={i + 1}
/>
)}
{isCreatingMap
? <ProvidersTableRowNew
organizations={organizations}
schemes={SCHEMES}
onCreate={this.handleCreateMap}
onCancel={this.handleCancelCreateMap}
rowIndex={mappings.length + 1}
/>
: null}
</div>
: <div className="panel-body">
<div className="generic-empty-state">
<h4 style={{margin: '50px 0'}}>
Looks like you have no mappings<br />
New users will not be able to sign up automatically
</h4>
<button
className="btn btn-sm btn-primary"
onClick={this.handleClickCreateMap}
disabled={isCreatingMap}
>
<span className="icon plus" /> Create Mapping
</button>
</div>
</div>}
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
ProvidersTable.propTypes = {
mappings: arrayOf(
shape({
id: string,
scheme: string,
provider: string,
providerOrganization: string,
organizationId: string,
})
).isRequired,
organizations: arrayOf(
shape({
id: string, // when optimistically created, organization will not have an id
name: string.isRequired,
})
).isRequired,
onCreateMap: func.isRequired,
onUpdateMap: func.isRequired,
onDeleteMap: func.isRequired,
isLoading: bool.isRequired,
}
export default ProvidersTable

View File

@ -0,0 +1,150 @@
import React, {Component, PropTypes} from 'react'
import ConfirmButtons from 'shared/components/ConfirmButtons'
import Dropdown from 'shared/components/Dropdown'
import InputClickToEdit from 'shared/components/InputClickToEdit'
import {DEFAULT_MAPPING_ID} from 'src/admin/constants/chronografAdmin'
class ProvidersTableRow extends Component {
constructor(props) {
super(props)
this.state = {
...this.props.mapping,
isDeleting: false,
}
}
handleDeleteClick = () => {
this.setState({isDeleting: true})
}
handleDismissDeleteConfirmation = () => {
this.setState({isDeleting: false})
}
handleDeleteMap = mapping => {
const {onDelete} = this.props
this.setState({isDeleting: false})
onDelete(mapping)
}
handleUpdateMapping = changes => {
const {onUpdate, mapping} = this.props
const newState = {...mapping, ...changes}
this.setState(newState)
onUpdate(mapping, newState)
}
handleChangeProvider = provider => this.handleUpdateMapping({provider})
handleChangeProviderOrg = providerOrganization =>
this.handleUpdateMapping({providerOrganization})
handleChooseOrganization = ({id: organizationId}) =>
this.handleUpdateMapping({organizationId})
handleChooseScheme = ({text: scheme}) => this.handleUpdateMapping({scheme})
render() {
const {
scheme,
provider,
providerOrganization,
organizationId,
isDeleting,
} = this.state
const {organizations, mapping, schemes, rowIndex} = this.props
const selectedOrg = organizations.find(o => o.id === organizationId)
const orgDropdownItems = organizations.map(role => ({
...role,
text: role.name,
}))
const organizationIdClassName = isDeleting
? 'fancytable--td provider--redirect deleting'
: 'fancytable--td provider--redirect'
const isDefaultMapping = DEFAULT_MAPPING_ID === mapping.id
return (
<div className="fancytable--row">
<Dropdown
items={schemes}
onChoose={this.handleChooseScheme}
selected={scheme}
className="fancytable--td provider--scheme"
disabled={isDefaultMapping}
/>
<InputClickToEdit
value={provider}
wrapperClass="fancytable--td provider--provider"
onUpdate={this.handleChangeProvider}
disabled={isDefaultMapping}
tabIndex={rowIndex}
/>
<InputClickToEdit
value={providerOrganization}
wrapperClass="fancytable--td provider--providerorg"
onUpdate={this.handleChangeProviderOrg}
disabled={isDefaultMapping}
tabIndex={rowIndex}
/>
<div className="fancytable--td provider--arrow">
<span />
</div>
<div className={organizationIdClassName}>
<Dropdown
items={orgDropdownItems}
onChoose={this.handleChooseOrganization}
selected={selectedOrg.name}
className="dropdown-stretch"
disabled={isDefaultMapping}
/>
</div>
{isDeleting
? <ConfirmButtons
item={mapping}
onCancel={this.handleDismissDeleteConfirmation}
onConfirm={this.handleDeleteMap}
onClickOutside={this.handleDismissDeleteConfirmation}
/>
: <button
className="btn btn-sm btn-default btn-square"
onClick={this.handleDeleteClick}
>
<span className="icon trash" />
</button>}
</div>
)
}
}
const {arrayOf, func, number, shape, string} = PropTypes
ProvidersTableRow.propTypes = {
mapping: shape({
id: string,
scheme: string,
provider: string,
providerOrganization: string,
organizationId: string,
}),
organizations: arrayOf(
shape({
id: string.isRequired,
name: string.isRequired,
})
),
schemes: arrayOf(
shape({
text: string.isRequired,
})
),
rowIndex: number,
onDelete: func.isRequired,
onUpdate: func.isRequired,
}
export default ProvidersTableRow

View File

@ -0,0 +1,116 @@
import React, {Component, PropTypes} from 'react'
import ConfirmButtons from 'shared/components/ConfirmButtons'
import Dropdown from 'shared/components/Dropdown'
import InputClickToEdit from 'shared/components/InputClickToEdit'
class ProvidersTableRowNew extends Component {
constructor(props) {
super(props)
this.state = {
scheme: '*',
provider: null,
providerOrganization: null,
organizationId: 'default',
}
}
handleChooseScheme = scheme => {
this.setState({scheme: scheme.text})
}
handleChangeProvider = provider => {
this.setState({provider})
}
handleChangeProviderOrg = providerOrganization => {
this.setState({providerOrganization})
}
handleChooseOrganization = org => {
this.setState({organizationId: org.id})
}
handleSaveNewMapping = () => {
const {onCreate} = this.props
onCreate(this.state)
}
render() {
const {scheme, provider, providerOrganization, organizationId} = this.state
const {organizations, onCancel, schemes, rowIndex} = this.props
const selectedOrg = organizations.find(o => o.id === organizationId)
const dropdownItems = organizations.map(role => ({
...role,
text: role.name,
}))
const preventCreate = !provider || !providerOrganization
return (
<div className="fancytable--row">
<Dropdown
items={schemes}
onChoose={this.handleChooseScheme}
selected={scheme}
className={'fancytable--td provider--scheme'}
/>
<InputClickToEdit
value={provider}
wrapperClass="fancytable--td provider--provider"
onUpdate={this.handleChangeProvider}
tabIndex={rowIndex}
placeholder="google"
/>
<InputClickToEdit
value={providerOrganization}
wrapperClass="fancytable--td provider--providerorg"
onUpdate={this.handleChangeProviderOrg}
tabIndex={rowIndex}
placeholder="*"
/>
<div className="fancytable--td provider--arrow">
<span />
</div>
<div className="fancytable--td provider--redirect deleting">
<Dropdown
items={dropdownItems}
onChoose={this.handleChooseOrganization}
selected={selectedOrg.name}
className="dropdown-stretch"
/>
</div>
<ConfirmButtons
onCancel={onCancel}
onConfirm={this.handleSaveNewMapping}
isDisabled={preventCreate}
/>
</div>
)
}
}
const {arrayOf, func, number, shape, string} = PropTypes
ProvidersTableRowNew.propTypes = {
organizations: arrayOf(
shape({
id: string.isRequired,
name: string.isRequired,
})
).isRequired,
schemes: arrayOf(
shape({
text: string.isRequired,
})
),
rowIndex: number,
onCreate: func.isRequired,
onCancel: func.isRequired,
}
export default ProvidersTableRowNew

View File

@ -2,8 +2,6 @@ import React, {Component, PropTypes} from 'react'
import uuid from 'node-uuid'
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
import UsersTableRowNew from 'src/admin/components/chronograf/UsersTableRowNew'
import UsersTableRow from 'src/admin/components/chronograf/UsersTableRow'
@ -23,10 +21,6 @@ class UsersTable extends Component {
this.props.onUpdateUserRole(user, currentRole, newRole)
}
handleChangeSuperAdmin = user => newStatus => {
this.props.onUpdateUserSuperAdmin(user, newStatus)
}
handleDeleteUser = user => {
this.props.onDeleteUser(user)
}
@ -40,17 +34,27 @@ class UsersTable extends Component {
}
render() {
const {organization, users, onCreateUser, meID, notify} = this.props
const {
organization,
users,
onCreateUser,
meID,
notify,
isLoading,
} = this.props
const {isCreatingUser} = this.state
const {
colRole,
colSuperAdmin,
colProvider,
colScheme,
colActions,
} = USERS_TABLE
const {colRole, colProvider, colScheme, colActions} = USERS_TABLE
if (isLoading) {
return (
<div className="panel panel-default">
<div className="panel-body">
<div className="page-spinner" />
</div>
</div>
)
}
return (
<div className="panel panel-default">
<UsersTableHeader
@ -67,11 +71,6 @@ class UsersTable extends Component {
<th style={{width: colRole}} className="align-with-col-text">
Role
</th>
<Authorized requiredRole={SUPERADMIN_ROLE}>
<th style={{width: colSuperAdmin}} className="text-center">
SuperAdmin
</th>
</Authorized>
<th style={{width: colProvider}}>Provider</th>
<th style={{width: colScheme}}>Scheme</th>
<th className="text-right" style={{width: colActions}} />
@ -86,31 +85,21 @@ class UsersTable extends Component {
notify={notify}
/>
: null}
{users.length || !isCreatingUser
{users.length
? users.map(user =>
<UsersTableRow
user={user}
key={uuid.v4()}
organization={organization}
onChangeUserRole={this.handleChangeUserRole}
onChangeSuperAdmin={this.handleChangeSuperAdmin}
onDelete={this.handleDeleteUser}
meID={meID}
/>
)
: <tr className="table-empty-state">
<Authorized
requiredRole={SUPERADMIN_ROLE}
replaceWithIfNotAuthorized={
<th colSpan="5">
<p>No Users to display</p>
</th>
}
>
<th colSpan="6">
<p>No Users to display</p>
</th>
</Authorized>
<th colSpan="5">
<p>No Users to display</p>
</th>
</tr>}
</tbody>
</table>
@ -138,7 +127,6 @@ UsersTable.propTypes = {
})
),
scheme: string.isRequired,
superAdmin: bool,
})
).isRequired,
organization: shape({
@ -147,10 +135,10 @@ UsersTable.propTypes = {
}),
onCreateUser: func.isRequired,
onUpdateUserRole: func.isRequired,
onUpdateUserSuperAdmin: func.isRequired,
onDeleteUser: func.isRequired,
meID: string.isRequired,
notify: func.isRequired,
isLoading: bool.isRequired,
}
export default UsersTable

View File

@ -26,7 +26,7 @@ class UsersTableHeader extends Component {
disabled={isCreatingUser || !onClickCreateUser}
>
<span className="icon plus" />
Create User
Add User
</button>
</div>
)

View File

@ -1,9 +1,6 @@
import React, {PropTypes} from 'react'
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import Dropdown from 'shared/components/Dropdown'
import SlideToggle from 'shared/components/SlideToggle'
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
@ -13,11 +10,10 @@ const UsersTableRow = ({
user,
organization,
onChangeUserRole,
onChangeSuperAdmin,
onDelete,
meID,
}) => {
const {colRole, colSuperAdmin, colProvider, colScheme} = USERS_TABLE
const {colRole, colProvider, colScheme} = USERS_TABLE
const dropdownRolesItems = USER_ROLES.map(r => ({
...r,
@ -53,16 +49,6 @@ const UsersTableRow = ({
/>
</span>
</td>
<Authorized requiredRole={SUPERADMIN_ROLE}>
<td style={{width: colSuperAdmin}} className="text-center">
<SlideToggle
active={user.superAdmin}
onToggle={onChangeSuperAdmin(user)}
size="xs"
disabled={userIsMe}
/>
</td>
</Authorized>
<td style={{width: colProvider}}>
{user.provider}
</td>
@ -89,7 +75,6 @@ UsersTableRow.propTypes = {
id: string.isRequired,
}),
onChangeUserRole: func.isRequired,
onChangeSuperAdmin: func.isRequired,
onDelete: func.isRequired,
meID: string.isRequired,
}

View File

@ -1,7 +1,5 @@
import React, {Component, PropTypes} from 'react'
import Authorized, {SUPERADMIN_ROLE} from 'src/auth/Authorized'
import Dropdown from 'shared/components/Dropdown'
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
@ -25,13 +23,12 @@ class UsersTableRowNew extends Component {
handleConfirmCreateUser = () => {
const {onBlur, onCreateUser, organization} = this.props
const {name, provider, scheme, role, superAdmin} = this.state
const {name, provider, scheme, role} = this.state
const newUser = {
name,
provider,
scheme,
superAdmin,
roles: [
{
name: role,
@ -72,13 +69,7 @@ class UsersTableRowNew extends Component {
}
render() {
const {
colRole,
colProvider,
colScheme,
colSuperAdmin,
colActions,
} = USERS_TABLE
const {colRole, colProvider, colScheme, colActions} = USERS_TABLE
const {onBlur} = this.props
const {name, provider, scheme, role} = this.state
@ -108,11 +99,6 @@ class UsersTableRowNew extends Component {
className="dropdown-stretch"
/>
</td>
<Authorized requiredRole={SUPERADMIN_ROLE}>
<td style={{width: colSuperAdmin}} className="text-center">
&mdash;
</td>
</Authorized>
<td style={{width: colProvider}}>
<input
className="form-control input-xs"

View File

@ -13,3 +13,4 @@ export const USER_ROLES = [
]
export const DEFAULT_ORG_ID = 'default'
export const DEFAULT_MAPPING_ID = 'default'

View File

@ -1,7 +1,16 @@
export const USERS_TABLE = {
colRole: 120,
colOrganizations: 200,
colSuperAdmin: 90,
colProvider: 170,
colScheme: 90,
colActions: 80,
}
export const ALL_USERS_TABLE = {
colOrganizations: 320,
colSuperAdmin: 90,
colProvider: 100,
colScheme: 90,
colActions: 50,
}

View File

@ -46,6 +46,3 @@ export const NEW_DEFAULT_DATABASE = {
isNew: true,
retentionPolicies: [NEW_DEFAULT_RP],
}
export const PUBLIC_TOOLTIP =
'If turned off, new users cannot<br/>authenticate unless an <strong>Admin</strong> explicitly<br/>adds them to the organization.'

View File

@ -0,0 +1,101 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import ProvidersTable from 'src/admin/components/chronograf/ProvidersTable'
class ProvidersPage extends Component {
constructor(props) {
super(props)
this.state = {isLoading: true}
}
async componentDidMount() {
const {
links,
actions: {loadOrganizationsAsync, loadMappingsAsync},
} = this.props
await Promise.all([
loadOrganizationsAsync(links.organizations),
loadMappingsAsync(links.mappings),
])
this.setState({isLoading: false})
}
handleCreateMap = mapping => {
this.props.actions.createMappingAsync(this.props.links.mappings, mapping)
}
handleUpdateMap = (staleMap, updatedMap) => {
this.props.actions.updateMappingAsync(staleMap, updatedMap)
}
handleDeleteMap = mapping => {
this.props.actions.deleteMappingAsync(mapping)
}
render() {
const {organizations, mappings = []} = this.props
const {isLoading} = this.state
return (
<ProvidersTable
mappings={mappings}
organizations={organizations}
onCreateMap={this.handleCreateMap}
onUpdateMap={this.handleUpdateMap}
onDeleteMap={this.handleDeleteMap}
isLoading={isLoading}
/>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
ProvidersPage.propTypes = {
links: shape({
organizations: string.isRequired,
}),
organizations: arrayOf(
shape({
id: string.isRequired,
name: string.isRequired,
})
),
mappings: arrayOf(
shape({
id: string,
scheme: string,
provider: string,
providerOrganization: string,
organizationId: string,
})
),
actions: shape({
loadOrganizationsAsync: func.isRequired,
}),
notify: func.isRequired,
}
const mapStateToProps = ({
links,
adminChronograf: {organizations, mappings},
}) => ({
links,
organizations,
mappings,
})
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(adminChronografActionCreators, dispatch),
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(ProvidersPage)

View File

@ -0,0 +1,143 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import * as adminChronografActionCreators from 'src/admin/actions/chronograf'
import * as configActionCreators from 'shared/actions/config'
import {publishAutoDismissingNotification} from 'shared/dispatchers'
import AllUsersTable from 'src/admin/components/chronograf/AllUsersTable'
class AllUsersPage extends Component {
constructor(props) {
super(props)
this.state = {
isLoading: true,
}
}
componentDidMount() {
const {links, actionsConfig: {getAuthConfigAsync}} = this.props
getAuthConfigAsync(links.config.auth)
}
handleCreateUser = user => {
const {links, actionsAdmin: {createUserAsync}} = this.props
createUserAsync(links.allUsers, user)
}
handleUpdateUserRoles = (user, roles, successMessage) => {
const {actionsAdmin: {updateUserAsync}} = this.props
const updatedUser = {...user, roles}
updateUserAsync(user, updatedUser, successMessage)
}
handleUpdateUserSuperAdmin = (user, superAdmin) => {
const {actionsAdmin: {updateUserAsync}} = this.props
const updatedUser = {...user, superAdmin}
updateUserAsync(
user,
updatedUser,
`${user.name}'s SuperAdmin status has been updated`
)
}
handleDeleteUser = user => {
const {actionsAdmin: {deleteUserAsync}} = this.props
deleteUserAsync(user, {isAbsoluteDelete: true})
}
async componentWillMount() {
const {
links,
actionsAdmin: {loadOrganizationsAsync, loadUsersAsync},
} = this.props
this.setState({isLoading: true})
await Promise.all([
loadOrganizationsAsync(links.organizations),
loadUsersAsync(links.allUsers),
])
this.setState({isLoading: false})
}
render() {
const {
organizations,
meID,
users,
authConfig,
actionsConfig,
links,
notify,
} = this.props
return (
<AllUsersTable
meID={meID}
users={users}
organizations={organizations}
onCreateUser={this.handleCreateUser}
onUpdateUserRoles={this.handleUpdateUserRoles}
onUpdateUserSuperAdmin={this.handleUpdateUserSuperAdmin}
onDeleteUser={this.handleDeleteUser}
links={links}
authConfig={authConfig}
actionsConfig={actionsConfig}
notify={notify}
isLoading={this.state.isLoading}
/>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
AllUsersPage.propTypes = {
links: shape({
users: string.isRequired,
config: shape({
auth: string.isRequired,
}).isRequired,
}),
meID: string.isRequired,
users: arrayOf(shape),
organizations: arrayOf(shape),
actionsAdmin: shape({
loadUsersAsync: func.isRequired,
loadOrganizationsAsync: func.isRequired,
createUserAsync: func.isRequired,
updateUserAsync: func.isRequired,
deleteUserAsync: func.isRequired,
}),
actionsConfig: shape({
getAuthConfigAsync: func.isRequired,
updateAuthConfigAsync: func.isRequired,
}),
authConfig: shape({
superAdminNewUsers: bool,
}),
notify: func.isRequired,
}
const mapStateToProps = ({
links,
adminChronograf: {organizations, users},
config: {auth: authConfig},
}) => ({
links,
organizations,
users,
authConfig,
})
const mapDispatchToProps = dispatch => ({
actionsAdmin: bindActionCreators(adminChronografActionCreators, dispatch),
actionsConfig: bindActionCreators(configActionCreators, dispatch),
notify: bindActionCreators(publishAutoDismissingNotification, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(AllUsersPage)

Some files were not shown because too many files have changed in this diff Show More