Nest user routes under organization

Add global user routes
pull/2733/head
Michael Desa 2018-01-16 16:45:58 -05:00
parent 4afb444579
commit 2d7828b602
13 changed files with 587 additions and 145 deletions

View File

@ -899,7 +899,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -937,7 +937,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -949,12 +949,12 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users"
"self": "/chronograf/v1/organizations/default/users"
},
"users": [
{
"links": {
"self": "/chronograf/v1/users/1"
"self": "/chronograf/v1/organizations/default/users/1"
},
"id": "1",
"name": "billibob",
@ -1011,7 +1011,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -1023,12 +1023,12 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users"
"self": "/chronograf/v1/organizations/default/users"
},
"users": [
{
"links": {
"self": "/chronograf/v1/users/1"
"self": "/chronograf/v1/organizations/default/users/1"
},
"id": "1",
"name": "billibob",
@ -1084,7 +1084,7 @@ func TestServer(t *testing.T) {
},
},
method: "POST",
path: "/chronograf/v1/users?raw=true",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -1096,7 +1096,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2?raw=true"
"self": "/chronograf/v1/users/2"
},
"id": "2",
"name": "user",
@ -1145,7 +1145,7 @@ func TestServer(t *testing.T) {
Roles: []chronograf.Role{},
},
method: "POST",
path: "/chronograf/v1/users?raw=true",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -1157,7 +1157,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2?raw=true"
"self": "/chronograf/v1/users/2"
},
"id": "2",
"name": "user",
@ -1215,7 +1215,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users?raw=true",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billibob",
@ -1227,12 +1227,12 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users?raw=true"
"self": "/chronograf/v1/users"
},
"users": [
{
"links": {
"self": "/chronograf/v1/users/1?raw=true"
"self": "/chronograf/v1/users/1"
},
"id": "1",
"name": "billibob",
@ -1248,7 +1248,7 @@ func TestServer(t *testing.T) {
},
{
"links": {
"self": "/chronograf/v1/users/2?raw=true"
"self": "/chronograf/v1/users/2"
},
"id": "2",
"name": "billietta",
@ -1317,7 +1317,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "GET",
path: "/chronograf/v1/users?raw=true",
path: "/chronograf/v1/users",
principal: oauth2.Principal{
Organization: "default",
Subject: "billieta",
@ -1365,7 +1365,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "POST",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
payload: &chronograf.User{
Name: "user",
Provider: "provider",
@ -1388,7 +1388,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2"
"self": "/chronograf/v1/organizations/default/users/2"
},
"id": "2",
"name": "user",
@ -1435,7 +1435,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "POST",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
payload: &chronograf.User{
Name: "user",
Provider: "provider",
@ -1458,7 +1458,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2"
"self": "/chronograf/v1/organizations/default/users/2"
},
"id": "2",
"name": "user",
@ -1505,7 +1505,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "POST",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
payload: &chronograf.User{
Name: "user",
Provider: "provider",
@ -1528,7 +1528,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2"
"self": "/chronograf/v1/organizations/default/users/2"
},
"id": "2",
"name": "user",
@ -1575,7 +1575,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "POST",
path: "/chronograf/v1/users",
path: "/chronograf/v1/organizations/default/users",
payload: &chronograf.User{
Name: "user",
Provider: "provider",
@ -1641,7 +1641,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "POST",
path: "/chronograf/v1/users?raw=true",
path: "/chronograf/v1/users",
payload: &chronograf.User{
Name: "user",
Provider: "provider",
@ -1668,7 +1668,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/2?raw=true"
"self": "/chronograf/v1/users/2"
},
"id": "2",
"name": "user",
@ -1726,7 +1726,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "PATCH",
path: "/chronograf/v1/users/1?raw=true",
path: "/chronograf/v1/users/1",
payload: map[string]interface{}{
"name": "billibob",
"provider": "github",
@ -1745,7 +1745,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/1?raw=true"
"self": "/chronograf/v1/users/1"
},
"id": "1",
"name": "billibob",
@ -1795,7 +1795,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "PATCH",
path: "/chronograf/v1/users/1?raw=true",
path: "/chronograf/v1/users/1",
payload: &chronograf.User{
Name: "billibob",
Provider: "github",
@ -1823,7 +1823,7 @@ func TestServer(t *testing.T) {
body: `
{
"links": {
"self": "/chronograf/v1/users/1?raw=true"
"self": "/chronograf/v1/users/1"
},
"id": "1",
"name": "billibob",
@ -1869,7 +1869,7 @@ func TestServer(t *testing.T) {
GithubClientSecret: "not empty",
},
method: "PATCH",
path: "/chronograf/v1/users/1",
path: "/chronograf/v1/organizations/default/users/1",
payload: map[string]interface{}{
"id": "1",
"superAdmin": false,
@ -1964,7 +1964,7 @@ func TestServer(t *testing.T) {
"scheme": "oauth2",
"superAdmin": true,
"links": {
"self": "/chronograf/v1/users/1"
"self": "/chronograf/v1/organizations/1/users/1"
},
"organizations": [
{

View File

@ -11,11 +11,6 @@ import (
"github.com/influxdata/chronograf/roles"
)
const (
rawQueryKey = "raw"
rawQueryValue = "true"
)
// 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.
@ -54,11 +49,8 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
})
}
// CheckRaw checks the query parameters on the HTTP request looking for
// the pair raw=true. If it finds a pair and the user is a super admin,
// then the user making the request will be given raw access to the data
// store (usually users are given a facade).
func CheckForRawQuery(logger chronograf.Logger, next http.HandlerFunc) http.HandlerFunc {
// 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 {
@ -67,20 +59,17 @@ func CheckForRawQuery(logger chronograf.Logger, next http.HandlerFunc) http.Hand
}
log := logger.
WithField("component", "raw_query").
WithField("component", "raw_store").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
v := r.URL.Query().Get(rawQueryKey)
if v == rawQueryValue {
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
}
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)
@ -219,6 +208,13 @@ func hasAuthorizedRole(u *chronograf.User, role string) bool {
}
switch role {
case roles.MemberRoleName:
for _, r := range u.Roles {
switch r.Name {
case roles.MemberRoleName, roles.ViewerRoleName, roles.EditorRoleName, roles.AdminRoleName:
return true
}
}
case roles.ViewerRoleName:
for _, r := range u.Roles {
switch r.Name {

View File

@ -108,6 +108,230 @@ func TestAuthorizedUser(t *testing.T) {
hasServerContext: true,
authorized: true,
},
{
name: "User with member role is member authorized",
fields: fields{
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
}
return &chronograf.User{
ID: 1337,
Name: "billysteve",
Provider: "google",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.MemberRoleName,
Organization: "1337",
},
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
},
},
Logger: clog.New(clog.DebugLevel),
},
args: args{
principal: &oauth2.Principal{
Subject: "billysteve",
Issuer: "google",
Organization: "1337",
},
scheme: "oauth2",
role: "member",
useAuth: true,
},
authorized: true,
hasOrganizationContext: true,
hasSuperAdminContext: false,
hasRoleContext: true,
hasServerContext: false,
},
{
name: "User with viewer role is member authorized",
fields: fields{
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
}
return &chronograf.User{
ID: 1337,
Name: "billysteve",
Provider: "google",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.ViewerRoleName,
Organization: "1337",
},
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
},
},
Logger: clog.New(clog.DebugLevel),
},
args: args{
principal: &oauth2.Principal{
Subject: "billysteve",
Issuer: "google",
Organization: "1337",
},
scheme: "oauth2",
role: "member",
useAuth: true,
},
authorized: true,
hasOrganizationContext: true,
hasSuperAdminContext: false,
hasRoleContext: true,
hasServerContext: false,
},
{
name: "User with editor role is member authorized",
fields: fields{
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
}
return &chronograf.User{
ID: 1337,
Name: "billysteve",
Provider: "google",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.EditorRoleName,
Organization: "1337",
},
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
},
},
Logger: clog.New(clog.DebugLevel),
},
args: args{
principal: &oauth2.Principal{
Subject: "billysteve",
Issuer: "google",
Organization: "1337",
},
scheme: "oauth2",
role: "member",
useAuth: true,
},
authorized: true,
hasOrganizationContext: true,
hasSuperAdminContext: false,
hasRoleContext: true,
hasServerContext: false,
},
{
name: "User with admin role is member authorized",
fields: fields{
UsersStore: &mocks.UsersStore{
GetF: func(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
if q.Name == nil || q.Provider == nil || q.Scheme == nil {
return nil, fmt.Errorf("Invalid user query: missing Name, Provider, and/or Scheme")
}
return &chronograf.User{
ID: 1337,
Name: "billysteve",
Provider: "google",
Scheme: "oauth2",
Roles: []chronograf.Role{
{
Name: roles.AdminRoleName,
Organization: "1337",
},
},
}, nil
},
},
OrganizationsStore: &mocks.OrganizationsStore{
DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) {
return &chronograf.Organization{
ID: "0",
}, nil
},
GetF: func(ctx context.Context, q chronograf.OrganizationQuery) (*chronograf.Organization, error) {
if q.ID == nil {
return nil, fmt.Errorf("Invalid organization query: missing ID")
}
return &chronograf.Organization{
ID: "1337",
Name: "The ShillBillThrilliettas",
}, nil
},
},
Logger: clog.New(clog.DebugLevel),
},
args: args{
principal: &oauth2.Principal{
Subject: "billysteve",
Issuer: "google",
Organization: "1337",
},
scheme: "oauth2",
role: "member",
useAuth: true,
},
authorized: true,
hasOrganizationContext: true,
hasSuperAdminContext: false,
hasRoleContext: true,
hasServerContext: false,
},
{
name: "User with viewer role is viewer authorized",
fields: fields{
@ -1607,7 +1831,7 @@ func TestAuthorizedUser(t *testing.T) {
}
}
func TestCheckForRawQuery(t *testing.T) {
func TestRawStoreAccess(t *testing.T) {
type fields struct {
Logger chronograf.Logger
}
@ -1615,7 +1839,6 @@ func TestCheckForRawQuery(t *testing.T) {
principal *oauth2.Principal
serverContext bool
user *chronograf.User
raw bool
}
type wants struct {
authorized bool
@ -1628,13 +1851,12 @@ func TestCheckForRawQuery(t *testing.T) {
wants wants
}{
{
name: "middleware already has server context with raw",
name: "middleware already has server context",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
args: args{
serverContext: true,
raw: true,
},
wants: wants{
authorized: true,
@ -1642,21 +1864,7 @@ func TestCheckForRawQuery(t *testing.T) {
},
},
{
name: "middleware already has server context without raw",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
args: args{
serverContext: true,
raw: false,
},
wants: wants{
authorized: true,
hasServerContext: true,
},
},
{
name: "user on context is a SuperAdmin with raw",
name: "user on context is a SuperAdmin",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
@ -1664,7 +1872,6 @@ func TestCheckForRawQuery(t *testing.T) {
user: &chronograf.User{
SuperAdmin: true,
},
raw: true,
},
wants: wants{
authorized: true,
@ -1672,7 +1879,7 @@ func TestCheckForRawQuery(t *testing.T) {
},
},
{
name: "user on context is a not SuperAdmin with raw",
name: "user on context is a not SuperAdmin",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
@ -1680,29 +1887,12 @@ func TestCheckForRawQuery(t *testing.T) {
user: &chronograf.User{
SuperAdmin: false,
},
raw: true,
},
wants: wants{
authorized: false,
hasServerContext: false,
},
},
{
name: "user on context is a SuperAdmin without raw",
fields: fields{
Logger: clog.New(clog.DebugLevel),
},
args: args{
user: &chronograf.User{
SuperAdmin: true,
},
raw: false,
},
wants: wants{
authorized: true,
hasServerContext: false,
},
},
}
for _, tt := range tests {
@ -1714,16 +1904,13 @@ func TestCheckForRawQuery(t *testing.T) {
hasServerCtx = hasServerContext(ctx)
authorized = true
}
fn := CheckForRawQuery(
fn := RawStoreAccess(
tt.fields.Logger,
next,
)
w := httptest.NewRecorder()
url := "http://any.url"
if tt.args.raw {
url = fmt.Sprintf("%s?raw=true", url)
}
r := httptest.NewRequest(
"GET",
url,
@ -1744,15 +1931,15 @@ func TestCheckForRawQuery(t *testing.T) {
fn(w, r)
if authorized != tt.wants.authorized {
t.Errorf("%q. CheckForRawQuery() = %v, expected %v", tt.name, 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. CheckForRawQuery() Status Code = %v, expected %v", tt.name, 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. CheckForRawQuery().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext)
t.Errorf("%q. RawStoreAccess().Context().Server = %v, expected %v", tt.name, hasServerCtx, tt.wants.hasServerContext)
}
})

View File

@ -26,10 +26,11 @@ type meResponse struct {
// 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 +182,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 := newMeResponse(nil, "")
encodeJSON(w, http.StatusOK, res, s.Logger)
return
}
@ -264,7 +265,7 @@ 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)
@ -314,7 +315,7 @@ func (s *Service) Me(w http.ResponseWriter, r *http.Request) {
unknownErrorWithMessage(w, err, s.Logger)
return
}
res := newMeResponse(newUser)
res := newMeResponse(newUser, currentOrg.ID)
res.Organizations = orgs
res.CurrentOrganization = currentOrg
encodeJSON(w, http.StatusOK, res, s.Logger)

View File

@ -176,7 +176,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}}`,
wantBody: `{"name":"me","roles":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","superAdmin":true,"links":{"self":"/chronograf/v1/organizations/0/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",
@ -306,7 +306,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","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":[{"name":"viewer","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/0/users/0"},"organizations":[{"id":"0","name":"Default","defaultRole":"viewer","public":true}],"currentOrganization":{"id":"0","name":"Default","defaultRole":"viewer","public":true}}`,
},
{
name: "Existing user - organization doesn't exist",
@ -423,7 +423,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","public":true}],"currentOrganization":{"id":"0","name":"The Gnarly Default","defaultRole":"viewer","public":true}}
`,
},
{
@ -485,7 +485,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","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
},
{
@ -547,7 +547,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","public":true,"defaultRole":"viewer"}],"currentOrganization":{"id":"0","name":"The Gnarly Default","public":true,"defaultRole":"viewer"}}
`,
},
{
@ -624,7 +624,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"}}
`,
},
{
@ -824,7 +824,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"},{"name":"admin","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/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}}`,
},
{
name: "Change the current User's organization",
@ -899,7 +899,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"},{"name":"editor","organization":"0"}],"provider":"github","scheme":"oauth2","links":{"self":"/chronograf/v1/organizations/1337/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}}`,
},
{
name: "Unable to find requested user in valid organization",

46
server/middle.go Normal file
View File

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

162
server/middle_test.go Normal file
View File

@ -0,0 +1,162 @@
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/oauth2"
)
func TestRouteMatchesPrincipal(t *testing.T) {
type fields struct {
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),
},
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),
},
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),
},
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),
},
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) {
var matches bool
next := func(w http.ResponseWriter, r *http.Request) {
matches = true
}
fn := RouteMatchesPrincipal(
tt.args.useAuth,
tt.fields.Logger,
next,
)
w := httptest.NewRecorder()
url := "http://any.url"
r := httptest.NewRequest(
"GET",
url,
nil,
)
if tt.args.routerParams != nil {
r = r.WithContext(httprouter.WithParams(r.Context(), *tt.args.routerParams))
}
if tt.args.principal == nil {
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, nil))
} else {
r = r.WithContext(context.WithValue(r.Context(), oauth2.PrincipalKey, *tt.args.principal))
}
fn(w, r)
if matches != tt.wants.matches {
t.Errorf("%q. RouteMatchesPrincipal() = %v, expected %v", tt.name, matches, tt.wants.matches)
}
if !matches && w.Code != http.StatusForbidden {
t.Errorf("%q. RouteMatchesPrincipal() Status Code = %v, expected %v", tt.name, w.Code, http.StatusForbidden)
}
})
}
}

View File

@ -68,6 +68,16 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound)
}
EnsureMember := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
opts.UseAuth,
roles.MemberRoleName,
opts.Logger,
next,
)
}
_ = EnsureMember
EnsureViewer := func(next http.HandlerFunc) http.HandlerFunc {
return AuthorizedUser(
service.Store,
@ -105,8 +115,16 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
)
}
checkForRawQuery := func(next http.HandlerFunc) http.HandlerFunc {
return CheckForRawQuery(opts.Logger, next)
rawStoreAccess := func(next http.HandlerFunc) http.HandlerFunc {
return RawStoreAccess(opts.Logger, next)
}
ensureOrgMatches := func(next http.HandlerFunc) http.HandlerFunc {
return RouteMatchesPrincipal(
opts.UseAuth,
opts.Logger,
next,
)
}
/* Documentation */
@ -118,9 +136,9 @@ 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))
// Sources
router.GET("/chronograf/v1/sources", EnsureViewer(service.Sources))
@ -198,12 +216,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(checkForRawQuery(service.Users)))
router.POST("/chronograf/v1/users", EnsureAdmin(checkForRawQuery(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(checkForRawQuery(service.UserID)))
router.DELETE("/chronograf/v1/users/:id", EnsureAdmin(checkForRawQuery(service.RemoveUser)))
router.PATCH("/chronograf/v1/users/:id", EnsureAdmin(checkForRawQuery(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))

View File

@ -165,7 +165,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 +191,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 {
@ -226,7 +226,7 @@ func (s *Service) UpdateOrganization(w http.ResponseWriter, r *http.Request) {
// RemoveOrganization removes an organization in the organizations store
func (s *Service) RemoveOrganization(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id := httprouter.GetParamFromContext(ctx, "id")
id := httprouter.GetParamFromContext(ctx, "oid")
org, err := s.Store.Organizations(ctx).Get(ctx, chronograf.OrganizationQuery{ID: &id})
if err != nil {

View File

@ -82,7 +82,7 @@ func TestService_OrganizationID(t *testing.T) {
context.Background(),
httprouter.Params{
{
Key: "id",
Key: "oid",
Value: tt.id,
},
}))
@ -411,7 +411,7 @@ 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,
},
}))
@ -503,7 +503,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,
},
}))

View File

@ -1,6 +1,7 @@
package server
import (
"fmt"
"net/http"
"github.com/influxdata/chronograf"
@ -31,7 +32,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
RawUsers string `json:"rawUsers"` // Location of the raw 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
@ -48,6 +49,7 @@ 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 {
Middleware func(http.HandlerFunc) http.HandlerFunc
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
@ -56,18 +58,32 @@ type AllRoutes struct {
}
// ServeHTTP returns all top level routes and external links within chronograf
func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (s *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.Middleware != nil {
s.Middleware(s.serveHTTP)(w, r)
return
}
s.serveHTTP(w, r)
}
// 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 {
Error(w, http.StatusInternalServerError, err.Error(), a.Logger)
return
}
org := "default"
if contextOrg, ok := hasOrganizationContext(r.Context()); ok {
org = contextOrg
}
routes := getRoutesResponse{
Sources: "/chronograf/v1/sources",
Layouts: "/chronograf/v1/layouts",
Users: "/chronograf/v1/users",
RawUsers: "/chronograf/v1/users?raw=true",
Users: fmt.Sprintf("/chronograf/v1/organizations/%s/users", org),
AllUsers: "/chronograf/v1/users",
Organizations: "/chronograf/v1/organizations",
Me: "/chronograf/v1/me",
Environment: "/chronograf/v1/env",

View File

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

View File

@ -79,16 +79,18 @@ type userResponse struct {
Roles []chronograf.Role `json:"roles"`
}
func newUserResponse(u *chronograf.User, raw bool) *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{}
}
selfLink := fmt.Sprintf("/chronograf/v1/users/%d", u.ID)
if raw {
selfLink = fmt.Sprintf("%s?raw=true", selfLink)
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,
@ -108,18 +110,20 @@ type usersResponse struct {
Users []*userResponse `json:"users"`
}
func newUsersResponse(users []chronograf.User, raw bool) *usersResponse {
func newUsersResponse(users []chronograf.User, org string) *usersResponse {
usersResp := make([]*userResponse, len(users))
for i, user := range users {
usersResp[i] = newUserResponse(&user, raw)
usersResp[i] = newUserResponse(&user, org)
}
sort.Slice(usersResp, func(i, j int) bool {
return usersResp[i].ID < usersResp[j].ID
})
selfLink := "/chronograf/v1/users"
if raw {
selfLink = fmt.Sprintf("%s?raw=true", selfLink)
var selfLink string
if org != "" {
selfLink = fmt.Sprintf("/chronograf/v1/organizations/%s/users", org)
} else {
selfLink = "/chronograf/v1/users"
}
return &usersResponse{
Users: usersResp,
@ -145,7 +149,9 @@ func (s *Service) UserID(w http.ResponseWriter, r *http.Request) {
return
}
res := newUserResponse(user, hasServerContext(ctx))
orgID := httprouter.GetParamFromContext(ctx, "oid")
res := newUserResponse(user, orgID)
location(w, res.Links.Self)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
@ -198,7 +204,8 @@ func (s *Service) NewUser(w http.ResponseWriter, r *http.Request) {
return
}
cu := newUserResponse(res, hasServerContext(ctx))
orgID := httprouter.GetParamFromContext(ctx, "oid")
cu := newUserResponse(res, orgID)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusCreated, cu, s.Logger)
}
@ -319,7 +326,8 @@ func (s *Service) UpdateUser(w http.ResponseWriter, r *http.Request) {
return
}
cu := newUserResponse(u, hasServerContext(ctx))
orgID := httprouter.GetParamFromContext(ctx, "oid")
cu := newUserResponse(u, orgID)
location(w, cu.Links.Self)
encodeJSON(w, http.StatusOK, cu, s.Logger)
}
@ -334,7 +342,8 @@ func (s *Service) Users(w http.ResponseWriter, r *http.Request) {
return
}
res := newUsersResponse(users, hasServerContext(ctx))
orgID := httprouter.GetParamFromContext(ctx, "oid")
res := newUsersResponse(users, orgID)
encodeJSON(w, http.StatusOK, res, s.Logger)
}