merged upstream, updated comments, added GroupFromClaims()
parent
9d931390ee
commit
227009723d
|
@ -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}
|
||||
|
|
|
@ -6,6 +6,7 @@ backup/
|
|||
|
||||
# Binaries
|
||||
/chronograf
|
||||
/chronoctl
|
||||
|
||||
# Dotfiles
|
||||
.pull-request
|
||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
6
Makefile
6
Makefile
|
@ -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
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestConfig_Get(t *testing.T) {
|
|||
wants: wants{
|
||||
config: &chronograf.Config{
|
||||
Auth: chronograf.AuthConfig{
|
||||
SuperAdminNewUsers: true,
|
||||
SuperAdminNewUsers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, ","))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ; \
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
@ -86,7 +86,11 @@
|
|||
"name": "comet",
|
||||
"value": "100"
|
||||
}
|
||||
]
|
||||
],
|
||||
"legend": {
|
||||
"type": "static",
|
||||
"orientation": "bottom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"templates": [
|
||||
|
|
|
@ -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 = ''
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
93
server/me.go
93
server/me.go
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chronograf-ui",
|
||||
"version": "1.4.0-0",
|
||||
"version": "1.4.1-3",
|
||||
"private": false,
|
||||
"license": "AGPL-3.0",
|
||||
"description": "",
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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">
|
||||
—
|
||||
</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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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">—</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,
|
||||
|
|
|
@ -58,20 +58,22 @@ class OrganizationsTableRowNew extends Component {
|
|||
}))
|
||||
|
||||
return (
|
||||
<div className="orgs-table--org orgs-table--new-org">
|
||||
<div className="orgs-table--active">—</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">—</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}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -26,7 +26,7 @@ class UsersTableHeader extends Component {
|
|||
disabled={isCreatingUser || !onClickCreateUser}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
Create User
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
—
|
||||
</td>
|
||||
</Authorized>
|
||||
<td style={{width: colProvider}}>
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
|
|
|
@ -13,3 +13,4 @@ export const USER_ROLES = [
|
|||
]
|
||||
|
||||
export const DEFAULT_ORG_ID = 'default'
|
||||
export const DEFAULT_MAPPING_ID = 'default'
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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)
|
|
@ -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
Loading…
Reference in New Issue