mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
committed by
GitHub
parent
d3d7411e36
commit
605d056136
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
39
pkg/middleware/csrf.go
Normal file
39
pkg/middleware/csrf.go
Normal file
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user