Security: Sync security changes on main (#45083)

* * Teams: Appropriately apply user id filter in /api/teams/:id and /api/teams/search
* Teams: Ensure that users searching for teams are only able see teams they have access to
* Teams: Require teamGuardian admin privileges to list team members
* Teams: Prevent org viewers from administering teams
* Teams: Add org_id condition to team count query
* Teams: clarify permission requirements in teams api docs
* Teams: expand scenarios for team search tests
* Teams: mock teamGuardian in tests

Co-authored-by: Dan Cech <dcech@grafana.com>

* remove duplicate WHERE statement

* Fix for CVE-2022-21702

(cherry picked from commit 202d7c190082c094bc1dc13f7fe9464746c37f9e)

* Lint and test fixes

(cherry picked from commit 3e6b67d5504abf4a1d7b8d621f04d062c048e981)

* check content type properly

(cherry picked from commit 70b4458892bf2f776302720c10d24c9ff34edd98)

* basic csrf origin check

(cherry picked from commit 3adaa5ff39832364f6390881fb5b42ad47df92e1)

* compare origin to host

(cherry picked from commit 5443892699e8ed42836bb2b9a44744ff3e970f42)

* simplify url parsing

(cherry picked from commit b2ffbc9513fed75468628370a48b929d30af2b1d)

* check csrf for GET requests, only compare origin

(cherry picked from commit 8b81dc12d8f8a1f07852809c5b4d44f0f0b1d709)

* parse content type properly

(cherry picked from commit 16f76f4902e6f2188bea9606c68b551af186bdc0)

* mentioned get in the comment

(cherry picked from commit a7e61811ef8ae558ce721e2e3fed04ce7a5a5345)

* add content-type: application/json to test HTTP requests

* fix pluginproxy test

* Fix linter when comparing errors

Co-authored-by: Kevin Minehart <kmineh0151@gmail.com>
Co-authored-by: Dan Cech <dcech@grafana.com>
Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
Co-authored-by: Serge Zaitsev <serge.zaitsev@grafana.com>
Co-authored-by: Vardan Torosyan <vardants@gmail.com>
pull/45139/merge
Dimitris Sotirakis 3 years ago committed by GitHub
parent d3d7411e36
commit 605d056136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      docs/sources/http_api/team.md
  2. 3
      pkg/api/admin_users_test.go
  3. 1
      pkg/api/alerting_test.go
  4. 4
      pkg/api/annotations_test.go
  5. 2
      pkg/api/api.go
  6. 6
      pkg/api/common_test.go
  7. 1
      pkg/api/dashboard_permission_test.go
  8. 4
      pkg/api/dashboard_test.go
  9. 1
      pkg/api/folder_permission_test.go
  10. 2
      pkg/api/folder_test.go
  11. 1
      pkg/api/frontend_logging_test.go
  12. 1
      pkg/api/http_server.go
  13. 1
      pkg/api/pluginproxy/ds_proxy.go
  14. 14
      pkg/api/pluginproxy/ds_proxy_test.go
  15. 8
      pkg/api/pluginproxy/pluginproxy.go
  16. 41
      pkg/api/pluginproxy/pluginproxy_test.go
  17. 3
      pkg/api/plugins.go
  18. 48
      pkg/api/plugins_test.go
  19. 1
      pkg/api/short_url_test.go
  20. 22
      pkg/api/team.go
  21. 3
      pkg/api/team_members.go
  22. 15
      pkg/api/team_members_test.go
  23. 3
      pkg/api/team_test.go
  24. 14
      pkg/middleware/auth.go
  25. 39
      pkg/middleware/csrf.go
  26. 4
      pkg/models/team.go
  27. 6
      pkg/services/dashboardimport/api/api_test.go
  28. 6
      pkg/services/libraryelements/libraryelements_test.go
  29. 5
      pkg/services/queryhistory/queryhistory_test.go
  30. 2
      pkg/services/serviceaccounts/api/token_test.go
  31. 49
      pkg/services/sqlstore/team.go
  32. 6
      pkg/util/proxyutil/proxyutil.go
  33. 10
      pkg/web/binding.go

@ -7,7 +7,13 @@ aliases = ["/docs/grafana/latest/http_api/team/"]
# Team API
This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization.
This API can be used to manage Teams and Team Memberships.
Access to these API endpoints is restricted as follows:
- All authenticated users are able to view details of teams they are a member of.
- Organization Admins are able to manage all teams and team members.
- If the `editors_can_admin` configuration flag is enabled, Organization Editors are able to view details of all teams and to manage teams that they are Admin members of.
## Team Search With Paging

@ -288,6 +288,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
@ -346,6 +347,7 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
sc.userAuthTokenService = fakeAuthTokenService
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
@ -464,6 +466,7 @@ func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID

@ -144,6 +144,7 @@ func postAlertScenario(t *testing.T, hs *HTTPServer, desc string, url string, ro
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID

@ -279,6 +279,7 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
@ -304,6 +305,7 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
@ -328,6 +330,7 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
@ -353,6 +356,7 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID

@ -27,7 +27,7 @@ func (hs *HTTPServer) registerRoutes() {
reqEditorRole := middleware.ReqEditorRole
reqOrgAdmin := middleware.ReqOrgAdmin
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
authorize := acmiddleware.Middleware(hs.AccessControl)

@ -102,6 +102,7 @@ func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
require.NoError(sc.t, err)
req.Header.Add("Content-Type", "application/json")
sc.req = req
return sc
@ -117,6 +118,8 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
panic(fmt.Sprintf("Making request failed: %s", err))
}
req.Header.Add("Content-Type", "application/json")
q := req.URL.Query()
for k, v := range queryParams {
q.Add(k, v)
@ -129,6 +132,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
func (sc *scenarioContext) fakeReqNoAssertions(method, url string) *scenarioContext {
sc.resp = httptest.NewRecorder()
req, _ := http.NewRequest(method, url, nil)
req.Header.Add("Content-Type", "application/json")
sc.req = req
return sc
@ -140,7 +144,7 @@ func (sc *scenarioContext) fakeReqNoAssertionsWithCookie(method, url string, coo
req, _ := http.NewRequest(method, url, nil)
req.Header = http.Header{"Cookie": sc.resp.Header()["Set-Cookie"]}
req.Header.Add("Content-Type", "application/json")
sc.req = req
return sc

@ -383,6 +383,7 @@ func updateDashboardPermissionScenario(t *testing.T, ctx updatePermissionContext
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(ctx.cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.OrgId = testOrgID
sc.context.UserId = testUserID

@ -36,6 +36,7 @@ import (
func TestGetHomeDashboard(t *testing.T) {
httpReq, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
httpReq.Header.Add("Content-Type", "application/json")
req := &models.ReqContext{SignedInUser: &models.SignedInUser{}, Context: &web.Context{Req: httpReq}}
cfg := setting.NewCfg()
cfg.StaticRootPath = "../../public/"
@ -1023,6 +1024,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
@ -1056,6 +1058,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{
OrgId: testOrgID,
@ -1095,6 +1098,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
sc.sqlStore = mockSQLStore
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{
OrgId: testOrgID,

@ -405,6 +405,7 @@ func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, h
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(ctx.cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.OrgId = testOrgID
sc.context.UserId = testUserID

@ -149,6 +149,7 @@ func createFolderScenario(t *testing.T, desc string, url string, routePattern st
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID}
@ -184,6 +185,7 @@ func updateFolderScenario(t *testing.T, desc string, url string, routePattern st
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID}

@ -92,6 +92,7 @@ func logSentryEventScenario(t *testing.T, desc string, event frontendlogging.Fro
handler := routing.Wrap(func(c *models.ReqContext) response.Response {
sc.context = c
c.Req.Body = mockRequestBody(event)
c.Req.Header.Add("Content-Type", "application/json")
return loggingHandler(c)
})

@ -444,6 +444,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
}
m.Use(middleware.Recovery(hs.Cfg))
m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName))
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public", "/public/views/swagger.html")

@ -56,6 +56,7 @@ func (t *handleResponseTransport) RoundTrip(req *http.Request) (*http.Response,
return nil, err
}
res.Header.Del("Set-Cookie")
proxyutil.SetProxyResponseHeaders(res.Header)
return res, nil
}

@ -670,6 +670,20 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie"))
})
t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
ctx, ds := setUp(t)
var routes []*plugins.Route
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasources.ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.HandleRequest()
require.NoError(t, writeErr)
assert.Equal(t, "sandbox", proxy.ctx.Resp.Header().Get("Content-Security-Policy"))
})
t.Run("Data source returns status code 401", func(t *testing.T) {
ctx, ds := setUp(t, setUpCfg{
writeCb: func(w http.ResponseWriter, r *http.Request) {

@ -83,5 +83,11 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
}
}
return &httputil.ReverseProxy{Director: director}
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyResponse}
}
func modifyResponse(resp *http.Response) error {
proxyutil.SetProxyResponseHeaders(resp.Header)
return nil
}

@ -4,6 +4,7 @@ import (
"context"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/models"
@ -238,6 +239,46 @@ func TestPluginProxy(t *testing.T) {
require.NoError(t, err)
require.Equal(t, `{ "url": "https://dynamic.grafana.com", "secret": "123" }`, string(content))
})
t.Run("When proxying a request should set expected response headers", func(t *testing.T) {
requestHandled := false
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
_, _ = w.Write([]byte("I am the backend"))
requestHandled = true
}))
t.Cleanup(backendServer.Close)
responseWriter := web.NewResponseWriter("GET", httptest.NewRecorder())
route := &plugins.Route{
Path: "/",
URL: backendServer.URL,
}
ctx := &models.ReqContext{
SignedInUser: &models.SignedInUser{},
Context: &web.Context{
Req: httptest.NewRequest("GET", "/", nil),
Resp: responseWriter,
},
}
store := mockstore.NewSQLStoreMock()
store.ExpectedPluginSetting = &models.PluginSetting{
SecureJsonData: map[string][]byte{},
}
proxy := NewApiPluginProxy(ctx, "", route, "", &setting.Cfg{}, store, secretsService)
proxy.ServeHTTP(ctx.Resp, ctx.Req)
for {
if requestHandled {
break
}
}
require.Equal(t, "sandbox", ctx.Resp.Header().Get("Content-Security-Policy"))
})
}
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.

@ -613,6 +613,9 @@ func (hs *HTTPServer) flushStream(stream callResourceClientResponseStream, w htt
w.Header().Add(k, v)
}
}
proxyutil.SetProxyResponseHeaders(w.Header())
w.WriteHeader(resp.Status)
}

@ -1,9 +1,12 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@ -11,6 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
@ -185,6 +189,28 @@ func Test_GetPluginAssets(t *testing.T) {
})
}
func TestMakePluginResourceRequest(t *testing.T) {
pluginClient := &fakePluginClient{}
hs := HTTPServer{
Cfg: setting.NewCfg(),
log: log.New(),
pluginClient: pluginClient,
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
pCtx := backend.PluginContext{}
err := hs.makePluginResourceRequest(resp, req, pCtx)
require.NoError(t, err)
for {
if resp.Flushed {
break
}
}
require.Equal(t, "sandbox", resp.Header().Get("Content-Security-Policy"))
}
func callGetPluginAsset(sc *scenarioContext) {
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
}
@ -221,3 +247,25 @@ type logger struct {
func (l *logger) Warn(msg string, ctx ...interface{}) {
l.warnings = append(l.warnings, msg)
}
type fakePluginClient struct {
plugins.Client
req *backend.CallResourceRequest
}
func (c *fakePluginClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
c.req = req
bytes, err := json.Marshal(map[string]interface{}{
"message": "hello",
})
if err != nil {
return err
}
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Headers: make(map[string][]string),
Body: bytes,
})
}

@ -65,6 +65,7 @@ func createShortURLScenario(t *testing.T, desc string, url string, routePattern
sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(cmd)
c.Req.Header.Add("Content-Type", "application/json")
sc.context = c
sc.context.SignedInUser = &models.SignedInUser{OrgId: testOrgID, UserId: testUserID}

@ -130,16 +130,11 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
page = 1
}
var userIdFilter int64
if hs.Cfg.EditorsCanAdmin && c.OrgRole != models.ROLE_ADMIN {
userIdFilter = c.SignedInUser.UserId
}
query := models.SearchTeamsQuery{
OrgId: c.OrgId,
Query: c.Query("query"),
Name: c.Query("name"),
UserIdFilter: userIdFilter,
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
Page: page,
Limit: perPage,
SignedInUser: c.SignedInUser,
@ -186,17 +181,32 @@ func (hs *HTTPServer) getTeamAccessControlMetadata(c *models.ReqContext, teamID
return accesscontrol.GetResourcesMetadata(c.Req.Context(), userPermissions, "teams", teamIDs)[key], nil
}
// UserFilter returns the user ID used in a filter when querying a team
// 1. If the user is a viewer or editor, this will return the user's ID.
// 2. If EditorsCanAdmin is enabled and the user is an editor, this will return models.FilterIgnoreUser (0)
// 3. If the user is an admin, this will return models.FilterIgnoreUser (0)
func userFilter(editorsCanAdmin bool, c *models.ReqContext) int64 {
userIdFilter := c.SignedInUser.UserId
if (editorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) || c.OrgRole == models.ROLE_ADMIN {
userIdFilter = models.FilterIgnoreUser
}
return userIdFilter
}
// GET /api/teams/:teamId
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
query := models.GetTeamByIdQuery{
OrgId: c.OrgId,
Id: teamId,
SignedInUser: c.SignedInUser,
HiddenUsers: hs.Cfg.HiddenUsers,
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
}
if err := hs.SQLStore.GetTeamById(c.Req.Context(), &query); err != nil {

@ -25,6 +25,9 @@ func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response {
}
query := models.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: teamId}
if err := hs.teamGuardian.CanAdmin(c.Req.Context(), query.OrgId, query.TeamId, c.SignedInUser); err != nil {
return response.Error(403, "Not allowed to list team members", err)
}
if err := hs.SQLStore.GetTeamMembers(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get Team Members", err)

@ -21,6 +21,14 @@ import (
"github.com/stretchr/testify/require"
)
type TeamGuardianMock struct {
result error
}
func (t *TeamGuardianMock) CanAdmin(ctx context.Context, orgId int64, teamId int64, user *models.SignedInUser) error {
return t.result
}
func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) {
const testOrgID int64 = 1
var userCmd models.CreateUserCommand
@ -44,9 +52,10 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) {
settings := setting.NewCfg()
sqlStore := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: settings,
License: &licensing.OSSLicensingService{},
SQLStore: sqlStore,
Cfg: settings,
License: &licensing.OSSLicensingService{},
SQLStore: sqlStore,
teamGuardian: &TeamGuardianMock{},
}
mock := mockstore.NewSQLStoreMock()

@ -36,6 +36,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
hs := setupSimpleHTTPServer(nil)
hs.SQLStore = sqlstore.InitTestDB(t)
mock := &mockstore.SQLStoreMock{}
hs.Cfg.EditorsCanAdmin = true
loggedInUserScenario(t, "When calling GET on", "/api/teams/search", "/api/teams/search", func(sc *scenarioContext) {
_, err := hs.SQLStore.CreateTeam("team1", "", 1)
require.NoError(t, err)
@ -96,6 +97,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
}
c.OrgRole = models.ROLE_EDITOR
c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName})
c.Req.Header.Add("Content-Type", "application/json")
r := hs.CreateTeam(c)
assert.Equal(t, 200, r.Status())
@ -112,6 +114,7 @@ func TestTeamAPIEndpoint(t *testing.T) {
}
c.OrgRole = models.ROLE_EDITOR
c.Req.Body = mockRequestBody(models.CreateTeamCommand{Name: teamName})
c.Req.Header.Add("Content-Type", "application/json")
r := hs.CreateTeam(c)
assert.Equal(t, 200, r.Status())
assert.False(t, stub.warnCalled)

@ -128,20 +128,22 @@ func Auth(options *AuthOptions) web.Handler {
}
}
// AdminOrFeatureEnabled creates a middleware that allows access
// if the signed in user is either an Org Admin or if the
// feature flag is enabled.
// AdminOrEditorAndFeatureEnabled creates a middleware that allows
// access if the signed in user is either an Org Admin or if they
// are an Org Editor and the feature flag is enabled.
// Intended for when feature flags open up access to APIs that
// are otherwise only available to admins.
func AdminOrFeatureEnabled(enabled bool) web.Handler {
func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler {
return func(c *models.ReqContext) {
if c.OrgRole == models.ROLE_ADMIN {
return
}
if !enabled {
accessForbidden(c)
if c.OrgRole == models.ROLE_EDITOR && enabled {
return
}
accessForbidden(c)
}
}

@ -0,0 +1,39 @@
package middleware
import (
"errors"
"net/http"
"net/url"
"strings"
)
func CSRF(loginCookieName string) func(http.Handler) http.Handler {
// As per RFC 7231/4.2.2 these methods are idempotent:
// (GET is excluded because it may have side effects in some APIs)
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If request has no login cookie - skip CSRF checks
if _, err := r.Cookie(loginCookieName); errors.Is(err, http.ErrNoCookie) {
next.ServeHTTP(w, r)
return
}
// Skip CSRF checks for "safe" methods
for _, method := range safeMethods {
if r.Method == method {
next.ServeHTTP(w, r)
return
}
}
// Otherwise - verify that Origin matches the server origin
host := strings.Split(r.Host, ":")[0]
origin, err := url.Parse(r.Header.Get("Origin"))
if err != nil || (origin.String() != "" && origin.Hostname() != host) {
http.Error(w, "origin not allowed", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}

@ -55,8 +55,12 @@ type GetTeamByIdQuery struct {
SignedInUser *SignedInUser
HiddenUsers map[string]struct{}
Result *TeamDTO
UserIdFilter int64
}
// FilterIgnoreUser is used in a get / search teams query when the caller does not want to filter teams by user ID / membership
const FilterIgnoreUser int64 = 0
type GetTeamsByUserQuery struct {
OrgId int64
UserId int64 `json:"userId"`

@ -43,6 +43,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
resp, err := s.Send(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
@ -57,6 +58,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
UserId: 1,
})
@ -73,6 +75,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
UserId: 1,
})
@ -90,6 +93,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
UserId: 1,
})
@ -132,6 +136,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
UserId: 1,
})
@ -160,6 +165,7 @@ func TestImportDashboardAPI(t *testing.T) {
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewRequest(http.MethodPost, "/api/dashboards/import", bytes.NewReader(jsonBytes))
req.Header.Add("Content-Type", "application/json")
webtest.RequestWithSignedInUser(req, &models.SignedInUser{
UserId: 1,
})

@ -289,7 +289,11 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
t.Helper()
t.Run(desc, func(t *testing.T) {
ctx := web.Context{Req: &http.Request{}}
ctx := web.Context{Req: &http.Request{
Header: http.Header{
"Content-Type": []string{"application/json"},
},
}}
orgID := int64(1)
role := models.ROLE_ADMIN
sqlStore := sqlstore.InitTestDB(t)

@ -35,7 +35,10 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
t.Helper()
t.Run(desc, func(t *testing.T) {
ctx := web.Context{Req: &http.Request{}}
ctx := web.Context{Req: &http.Request{
Header: http.Header{},
}}
ctx.Req.Header.Add("Content-Type", "application/json")
sqlStore := sqlstore.InitTestDB(t)
service := QueryHistoryService{
Cfg: setting.NewCfg(),

@ -113,6 +113,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
var requestResponse = func(server *web.Mux, httpMethod, requestpath string, requestBody io.Reader) *httptest.ResponseRecorder {
req, err := http.NewRequest(httpMethod, requestpath, requestBody)
require.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, req)
return recorder
@ -206,6 +207,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
var requestResponse = func(server *web.Mux, httpMethod, requestpath string, requestBody io.Reader) *httptest.ResponseRecorder {
req, err := http.NewRequest(httpMethod, requestpath, requestBody)
require.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, req)
return recorder

@ -64,18 +64,6 @@ func getTeamMemberCount(filteredUsers []string) string {
return "(SELECT COUNT(*) FROM team_member WHERE team_member.team_id = team.id) AS member_count "
}
func getTeamSearchSQLBase(filteredUsers []string) string {
return `SELECT
team.id AS id,
team.org_id,
team.name AS name,
team.email AS email,
team_member.permission, ` +
getTeamMemberCount(filteredUsers) +
` FROM team AS team
INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ? `
}
func getTeamSelectSQLBase(filteredUsers []string) string {
return `SELECT
team.id as id,
@ -198,17 +186,15 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu
params := make([]interface{}, 0)
filteredUsers := getFilteredUsers(query.SignedInUser, query.HiddenUsers)
if query.UserIdFilter > 0 {
sql.WriteString(getTeamSearchSQLBase(filteredUsers))
for _, user := range filteredUsers {
params = append(params, user)
}
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
for _, user := range filteredUsers {
params = append(params, user)
}
if query.UserIdFilter != models.FilterIgnoreUser {
sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`)
params = append(params, query.UserIdFilter)
} else {
sql.WriteString(getTeamSelectSQLBase(filteredUsers))
for _, user := range filteredUsers {
params = append(params, user)
}
}
sql.WriteString(` WHERE team.org_id = ?`)
@ -237,6 +223,8 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu
team := models.Team{}
countSess := x.Table("team")
countSess.Where("team.org_id=?", query.OrgId)
if query.Query != "" {
countSess.Where(`name `+dialect.LikeStr()+` ?`, queryWithWildcards)
}
@ -245,6 +233,18 @@ func (ss *SQLStore) SearchTeams(ctx context.Context, query *models.SearchTeamsQu
countSess.Where("name=?", query.Name)
}
// If we're not retrieving all results, then only search for teams that this user has access to
if query.UserIdFilter != models.FilterIgnoreUser {
countSess.
Where(`
team.id IN (
SELECT
team_id
FROM team_member
WHERE team_member.user_id = ?
)`, query.UserIdFilter)
}
count, err := countSess.Count(&team)
query.Result.TotalCount = count
@ -261,6 +261,11 @@ func (ss *SQLStore) GetTeamById(ctx context.Context, query *models.GetTeamByIdQu
params = append(params, user)
}
if query.UserIdFilter != models.FilterIgnoreUser {
sql.WriteString(` INNER JOIN team_member ON team.id = team_member.team_id AND team_member.user_id = ?`)
params = append(params, query.UserIdFilter)
}
sql.WriteString(` WHERE team.org_id = ? and team.id = ?`)
params = append(params, query.OrgId, query.Id)

@ -42,3 +42,9 @@ func ClearCookieHeader(req *http.Request, keepCookiesNames []string) {
req.AddCookie(c)
}
}
// SetProxyResponseHeaders sets proxy response headers.
// Sets Content-Security-Policy: sandbox
func SetProxyResponseHeaders(header http.Header) {
header.Set("Content-Security-Policy", "sandbox")
}

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"mime"
"net/http"
"reflect"
)
@ -12,8 +13,15 @@ import (
// Bind deserializes JSON payload from the request
func Bind(req *http.Request, v interface{}) error {
if req.Body != nil {
m, _, err := mime.ParseMediaType(req.Header.Get("Content-type"))
if err != nil {
return err
}
if m != "application/json" {
return errors.New("bad content type")
}
defer func() { _ = req.Body.Close() }()
err := json.NewDecoder(req.Body).Decode(v)
err = json.NewDecoder(req.Body).Decode(v)
if err != nil && !errors.Is(err, io.EOF) {
return err
}

Loading…
Cancel
Save