diff --git a/integrations/server_test.go b/integrations/server_test.go index 8188bf709..be3893ac3 100644 --- a/integrations/server_test.go +++ b/integrations/server_test.go @@ -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": [ { diff --git a/server/auth.go b/server/auth.go index 928b707f7..695a9b535 100644 --- a/server/auth.go +++ b/server/auth.go @@ -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 { diff --git a/server/auth_test.go b/server/auth_test.go index 7aba188ca..ff1afdf1f 100644 --- a/server/auth_test.go +++ b/server/auth_test.go @@ -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) } }) diff --git a/server/me.go b/server/me.go index 52a36c6ff..1ec50d262 100644 --- a/server/me.go +++ b/server/me.go @@ -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) diff --git a/server/me_test.go b/server/me_test.go index 6f90e7d32..6733c44c3 100644 --- a/server/me_test.go +++ b/server/me_test.go @@ -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", diff --git a/server/middle.go b/server/middle.go new file mode 100644 index 000000000..c8649b22a --- /dev/null +++ b/server/middle.go @@ -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) + } +} diff --git a/server/middle_test.go b/server/middle_test.go new file mode 100644 index 000000000..31ae2d862 --- /dev/null +++ b/server/middle_test.go @@ -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) + } + + }) + } +} diff --git a/server/mux.go b/server/mux.go index 7c5ff9a55..faa7d9786 100644 --- a/server/mux.go +++ b/server/mux.go @@ -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)) diff --git a/server/organizations.go b/server/organizations.go index 5b2227953..a4debaa15 100644 --- a/server/organizations.go +++ b/server/organizations.go @@ -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 { diff --git a/server/organizations_test.go b/server/organizations_test.go index 3315d981b..846f8ab81 100644 --- a/server/organizations_test.go +++ b/server/organizations_test.go @@ -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, }, })) diff --git a/server/routes.go b/server/routes.go index f2040705e..84b3aaf8e 100644 --- a/server/routes.go +++ b/server/routes.go @@ -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", diff --git a/server/routes_test.go b/server/routes_test.go index 264b05ca7..38ff7b8b3 100644 --- a/server/routes_test.go +++ b/server/routes_test.go @@ -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)) diff --git a/server/users.go b/server/users.go index 85aa3f9fa..8fc65b173 100644 --- a/server/users.go +++ b/server/users.go @@ -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) }