|
|
|
|
@ -5,6 +5,7 @@ import ( |
|
|
|
|
"fmt" |
|
|
|
|
"io" |
|
|
|
|
"net/http" |
|
|
|
|
"strings" |
|
|
|
|
"testing" |
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert" |
|
|
|
|
@ -14,10 +15,10 @@ import ( |
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos" |
|
|
|
|
"github.com/grafana/grafana/pkg/api/response" |
|
|
|
|
"github.com/grafana/grafana/pkg/api/routing" |
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson" |
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db" |
|
|
|
|
"github.com/grafana/grafana/pkg/models" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/annotations" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/annotations/annotationstest" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/dashboards" |
|
|
|
|
@ -25,7 +26,8 @@ import ( |
|
|
|
|
"github.com/grafana/grafana/pkg/services/org" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/team/teamtest" |
|
|
|
|
"github.com/grafana/grafana/pkg/services/user" |
|
|
|
|
"github.com/grafana/grafana/pkg/setting" |
|
|
|
|
"github.com/grafana/grafana/pkg/web/webtest" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
func TestAnnotationsAPIEndpoint(t *testing.T) { |
|
|
|
|
@ -386,371 +388,232 @@ func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePatte |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestAPI_Annotations_AccessControl(t *testing.T) { |
|
|
|
|
sc := setupHTTPServer(t, true) |
|
|
|
|
setInitCtxSignedInEditor(sc.initCtx) |
|
|
|
|
_, err := sc.hs.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "TestOrg", UserID: testUserID}) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
origNewGuardian := guardian.New |
|
|
|
|
t.Cleanup(func() { |
|
|
|
|
guardian.New = origNewGuardian |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Create a dashboard
|
|
|
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) |
|
|
|
|
sc.dashboardPermissionsService.On("SetPermissions", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string"), mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) |
|
|
|
|
cmd := &dashboards.SaveDashboardDTO{ |
|
|
|
|
OrgId: testOrgID, |
|
|
|
|
User: &user.SignedInUser{UserID: testUserID, OrgID: testOrgID}, |
|
|
|
|
Dashboard: &models.Dashboard{ |
|
|
|
|
OrgId: testOrgID, |
|
|
|
|
Title: "1 test dash", |
|
|
|
|
Data: simplejson.NewFromAny(map[string]interface{}{}), |
|
|
|
|
}} |
|
|
|
|
dashboard, err := sc.hs.DashboardService.SaveDashboard(context.Background(), cmd, false) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.NotNil(t, dashboard) |
|
|
|
|
t.Cleanup(func() { |
|
|
|
|
err := sc.hs.DashboardService.DeleteDashboard(context.Background(), dashboard.Id, dashboard.OrgId) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: dashboard.Id} |
|
|
|
|
organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0} |
|
|
|
|
|
|
|
|
|
_ = sc.hs.annotationsRepo.Save(context.Background(), dashboardAnnotation) |
|
|
|
|
_ = sc.hs.annotationsRepo.Save(context.Background(), organizationAnnotation) |
|
|
|
|
|
|
|
|
|
postOrganizationCmd := dtos.PostAnnotationsCmd{ |
|
|
|
|
Time: 1000, |
|
|
|
|
Text: "annotation text", |
|
|
|
|
Tags: []string{"tag1", "tag2"}, |
|
|
|
|
PanelId: 1, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
postDashboardCmd := dtos.PostAnnotationsCmd{ |
|
|
|
|
Time: 1000, |
|
|
|
|
Text: "annotation text", |
|
|
|
|
Tags: []string{"tag1", "tag2"}, |
|
|
|
|
DashboardId: 1, |
|
|
|
|
PanelId: 1, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateCmd := dtos.UpdateAnnotationsCmd{ |
|
|
|
|
Time: 1000, |
|
|
|
|
Text: "annotation text", |
|
|
|
|
Tags: []string{"tag1", "tag2"}, |
|
|
|
|
type testCase struct { |
|
|
|
|
desc string |
|
|
|
|
path string |
|
|
|
|
method string |
|
|
|
|
body string |
|
|
|
|
expectedCode int |
|
|
|
|
permissions []accesscontrol.Permission |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
patchCmd := dtos.PatchAnnotationsCmd{ |
|
|
|
|
Time: 1000, |
|
|
|
|
Text: "annotation text", |
|
|
|
|
Tags: []string{"tag1", "tag2"}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
postGraphiteCmd := dtos.PostGraphiteAnnotationsCmd{ |
|
|
|
|
When: 1000, |
|
|
|
|
What: "annotation text", |
|
|
|
|
Data: "Deploy", |
|
|
|
|
Tags: []string{"tag1", "tag2"}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
type args struct { |
|
|
|
|
permissions []accesscontrol.Permission |
|
|
|
|
url string |
|
|
|
|
body io.Reader |
|
|
|
|
method string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
tests := []struct { |
|
|
|
|
name string |
|
|
|
|
args args |
|
|
|
|
want int |
|
|
|
|
}{ |
|
|
|
|
tests := []testCase{ |
|
|
|
|
{ |
|
|
|
|
desc: "should be able to fetch annotations with correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting annotations with correct permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to fetch annotations without correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting annotations without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to fetch annotation by id with correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting annotation by ID with correct permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead, Scope: accesscontrol.ScopeAnnotationsAll}}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to fetch annotation by id without correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting annotation by ID without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to fetch annotation tags with correct permission", |
|
|
|
|
path: "/api/annotations/tags", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting tags for annotations with correct permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsRead}}, |
|
|
|
|
url: "/api/annotations/tags", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to fetch annotation tags without correct permission", |
|
|
|
|
path: "/api/annotations/tags", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl getting tags for annotations without correct permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite}}, |
|
|
|
|
url: "/api/annotations/tags", |
|
|
|
|
method: http.MethodGet, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to update dashboard annotation with correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl update dashboard annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
body: mockRequestBody(updateCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to update dashboard annotation without correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl update dashboard annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
body: mockRequestBody(updateCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to update organization annotation with correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl update organization annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsAll, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
body: mockRequestBody(updateCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to update organization annotation without correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl update organization annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodPut, |
|
|
|
|
body: mockRequestBody(updateCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to patch dashboard annotation with correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl patch dashboard annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
body: mockRequestBody(patchCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to patch dashboard annotation without correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl patch dashboard annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
body: mockRequestBody(patchCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to patch organization annotation with correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl patch organization annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsAll, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
body: mockRequestBody(patchCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to patch organization annotation without correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl patch organization annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsWrite, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodPatch, |
|
|
|
|
body: mockRequestBody(patchCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to create dashboard annotation with correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: "{\"dashboardId\": 2,\"text\": \"test\"}", |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create dashboard annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postDashboardCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should not be able to create dashboard annotation without correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: "{\"dashboardId\": 2,\"text\": \"test\"}", |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create dashboard annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postDashboardCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should be able to create organization annotation with correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: "{\"text\": \"test\"}", |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create dashboard annotation with incorrect permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postDashboardCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should not be able to create organization annotation without correct permission", |
|
|
|
|
path: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: "{\"text\": \"test\"}", |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create organization annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsAll, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postOrganizationCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should be able to delete dashboard annotation with correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create organization annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postOrganizationCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should not be able to delete dashboard annotation without correct permission", |
|
|
|
|
path: "/api/annotations/2", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl delete dashboard annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should be able to delete organization annotation with correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl delete dashboard annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{}, |
|
|
|
|
url: "/api/annotations/1", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should not be able to delete organization annotation without correct permission", |
|
|
|
|
path: "/api/annotations/1", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl delete organization annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsAll, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should be able to create graphite annotation with correct permission", |
|
|
|
|
path: "/api/annotations/graphite", |
|
|
|
|
body: "{\"what\": \"test\", \"tags\": []}", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl delete organization annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/2", |
|
|
|
|
method: http.MethodDelete, |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should not be able to create graphite annotation without correct permission", |
|
|
|
|
path: "/api/annotations/graphite", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create graphite annotation with permissions is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsAll, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/graphite", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postGraphiteCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
desc: "should be able to mass delete dashboard annotations with correct permission", |
|
|
|
|
path: "/api/annotations/mass-delete", |
|
|
|
|
body: "{\"dashboardId\": 2, \"panelId\": 1}", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
expectedCode: http.StatusOK, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl create organization annotation without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{ |
|
|
|
|
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeDashboard, |
|
|
|
|
}}, |
|
|
|
|
url: "/api/annotations/graphite", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(postGraphiteCmd), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
desc: "should not be able to mass delete dashboard annotations without correct permission", |
|
|
|
|
path: "/api/annotations/mass-delete", |
|
|
|
|
body: "{\"dashboardId\": 2, \"panelId\": 1}", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
expectedCode: http.StatusForbidden, |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for _, tt := range tests { |
|
|
|
|
t.Run(tt.name, func(t *testing.T) { |
|
|
|
|
t.Run(tt.desc, func(t *testing.T) { |
|
|
|
|
setUpRBACGuardian(t) |
|
|
|
|
sc.acmock. |
|
|
|
|
RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(sc.hs.annotationsRepo)) |
|
|
|
|
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgID) |
|
|
|
|
server := SetupAPITestServer(t, func(hs *HTTPServer) { |
|
|
|
|
hs.Cfg = setting.NewCfg() |
|
|
|
|
repo := annotationstest.NewFakeAnnotationsRepo() |
|
|
|
|
_ = repo.Save(context.Background(), &annotations.Item{Id: 1, DashboardId: 0}) |
|
|
|
|
_ = repo.Save(context.Background(), &annotations.Item{Id: 2, DashboardId: 1}) |
|
|
|
|
hs.annotationsRepo = repo |
|
|
|
|
hs.AccessControl = acimpl.ProvideAccessControl(hs.Cfg) |
|
|
|
|
hs.AccessControl.RegisterScopeAttributeResolver(AnnotationTypeScopeResolver(hs.annotationsRepo)) |
|
|
|
|
}) |
|
|
|
|
var body io.Reader |
|
|
|
|
if tt.body != "" { |
|
|
|
|
body = strings.NewReader(tt.body) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t) |
|
|
|
|
assert.Equalf(t, tt.want, r.Code, "Annotations API(%v)", tt.args.url) |
|
|
|
|
req := webtest.RequestWithSignedInUser(server.NewRequest(tt.method, tt.path, body), userWithPermissions(1, tt.permissions)) |
|
|
|
|
res, err := server.SendJSON(req) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
assert.Equal(t, tt.expectedCode, res.StatusCode) |
|
|
|
|
require.NoError(t, res.Body.Close()) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestService_AnnotationTypeScopeResolver(t *testing.T) { |
|
|
|
|
type testCaseResolver struct { |
|
|
|
|
desc string |
|
|
|
|
@ -811,166 +674,6 @@ func TestService_AnnotationTypeScopeResolver(t *testing.T) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestAPI_MassDeleteAnnotations_AccessControl(t *testing.T) { |
|
|
|
|
sc := setupHTTPServer(t, true) |
|
|
|
|
setInitCtxSignedInEditor(sc.initCtx) |
|
|
|
|
_, err := sc.hs.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "TestOrg", UserID: testUserID}) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
type args struct { |
|
|
|
|
permissions []accesscontrol.Permission |
|
|
|
|
url string |
|
|
|
|
body io.Reader |
|
|
|
|
method string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
origNewGuardian := guardian.New |
|
|
|
|
t.Cleanup(func() { |
|
|
|
|
guardian.New = origNewGuardian |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
// Create a dashboard
|
|
|
|
|
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) |
|
|
|
|
sc.dashboardPermissionsService.On("SetPermissions", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string"), mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) |
|
|
|
|
cmd := &dashboards.SaveDashboardDTO{ |
|
|
|
|
OrgId: testOrgID, |
|
|
|
|
User: &user.SignedInUser{UserID: testUserID, OrgID: testOrgID}, |
|
|
|
|
Dashboard: &models.Dashboard{ |
|
|
|
|
OrgId: testOrgID, |
|
|
|
|
Title: "1 test dash", |
|
|
|
|
Data: simplejson.NewFromAny(map[string]interface{}{}), |
|
|
|
|
}} |
|
|
|
|
dashboard, err := sc.hs.DashboardService.SaveDashboard(context.Background(), cmd, false) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.NotNil(t, dashboard) |
|
|
|
|
t.Cleanup(func() { |
|
|
|
|
err := sc.hs.DashboardService.DeleteDashboard(context.Background(), dashboard.Id, dashboard.OrgId) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
tests := []struct { |
|
|
|
|
name string |
|
|
|
|
args args |
|
|
|
|
want int |
|
|
|
|
}{ |
|
|
|
|
{ |
|
|
|
|
name: "Mass delete dashboard annotations without dashboardId is not allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
DashboardId: 0, |
|
|
|
|
PanelId: 1, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusBadRequest, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "Mass delete dashboard annotations without panelId is not allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
DashboardId: 10, |
|
|
|
|
PanelId: 0, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusBadRequest, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl mass delete dashboard annotations with correct dashboardId and panelId as input is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
DashboardId: dashboard.Id, |
|
|
|
|
PanelId: 1, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "Mass delete organization annotations without input to delete all organization annotations is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
DashboardId: 0, |
|
|
|
|
PanelId: 0, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "Mass delete organization annotations without permissions is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
DashboardId: 0, |
|
|
|
|
PanelId: 0, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl mass delete dashboard annotations with correct annotationId as input is allowed", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
AnnotationId: 1, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusOK, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl mass delete annotation without access to dashboard annotations is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeOrganization}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
AnnotationId: 1, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
}, |
|
|
|
|
{ |
|
|
|
|
name: "AccessControl mass delete annotation without access to organization annotations is forbidden", |
|
|
|
|
args: args{ |
|
|
|
|
permissions: []accesscontrol.Permission{{Action: accesscontrol.ActionAnnotationsDelete, Scope: accesscontrol.ScopeAnnotationsTypeDashboard}}, |
|
|
|
|
url: "/api/annotations/mass-delete", |
|
|
|
|
method: http.MethodPost, |
|
|
|
|
body: mockRequestBody(dtos.MassDeleteAnnotationsCmd{ |
|
|
|
|
AnnotationId: 2, |
|
|
|
|
}), |
|
|
|
|
}, |
|
|
|
|
want: http.StatusForbidden, |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
for _, tt := range tests { |
|
|
|
|
t.Run(tt.name, func(t *testing.T) { |
|
|
|
|
setUpRBACGuardian(t) |
|
|
|
|
setAccessControlPermissions(sc.acmock, tt.args.permissions, sc.initCtx.OrgID) |
|
|
|
|
dashboardAnnotation := &annotations.Item{Id: 1, DashboardId: 1} |
|
|
|
|
organizationAnnotation := &annotations.Item{Id: 2, DashboardId: 0} |
|
|
|
|
|
|
|
|
|
_ = sc.hs.annotationsRepo.Save(context.Background(), dashboardAnnotation) |
|
|
|
|
_ = sc.hs.annotationsRepo.Save(context.Background(), organizationAnnotation) |
|
|
|
|
|
|
|
|
|
r := callAPI(sc.server, tt.args.method, tt.args.url, tt.args.body, t) |
|
|
|
|
assert.Equalf(t, tt.want, r.Code, "Annotations API(%v)", tt.args.url) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func setUpACL() { |
|
|
|
|
viewerRole := org.RoleViewer |
|
|
|
|
editorRole := org.RoleEditor |
|
|
|
|
|