@ -12,6 +12,8 @@ import (
"github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
@ -31,7 +33,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode0 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -59,7 +61,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode1 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -87,7 +89,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode2 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -115,7 +117,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode3 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -143,7 +145,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode4 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -171,7 +173,7 @@ func TestSearchFallback(t *testing.T) {
"dashboards.dashboard.grafana.app" : { DualWriterMode : rest . Mode5 } ,
} ,
}
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient )
searchHandler := NewSearchHandler ( tracing . NewNoopTracerService ( ) , cfg , mockLegacyClient , nil )
searchHandler . client = resource . NewSearchClient ( cfg , setting . UnifiedStorageConfigKeyDashboard , mockUnifiedCtxclient , mockLegacyClient )
rr := httptest . NewRecorder ( )
@ -191,17 +193,19 @@ func TestSearchFallback(t *testing.T) {
}
func TestSearchHandler ( t * testing . T ) {
// Create a mock client
mockClient := & MockClient { }
// Initialize the search handler with the mock client
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
}
t . Run ( "Multiple comma separated fields will be appended to default dashboard search fields" , func ( t * testing . T ) {
// Create a mock client
mockClient := & MockClient { }
features := featuremgmt . WithFeatures ( )
// Initialize the search handler with the mock client
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search?field=field1&field=field2&field=field3" , nil )
req . Header . Add ( "content-type" , "application/json" )
@ -219,6 +223,18 @@ func TestSearchHandler(t *testing.T) {
} )
t . Run ( "Single field will be appended to default dashboard search fields" , func ( t * testing . T ) {
// Create a mock client
mockClient := & MockClient { }
features := featuremgmt . WithFeatures ( )
// Initialize the search handler with the mock client
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search?field=field1" , nil )
req . Header . Add ( "content-type" , "application/json" )
@ -236,6 +252,18 @@ func TestSearchHandler(t *testing.T) {
} )
t . Run ( "Passing no fields will search using default dashboard fields" , func ( t * testing . T ) {
// Create a mock client
mockClient := & MockClient { }
features := featuremgmt . WithFeatures ( )
// Initialize the search handler with the mock client
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search" , nil )
req . Header . Add ( "content-type" , "application/json" )
@ -253,6 +281,41 @@ func TestSearchHandler(t *testing.T) {
} )
t . Run ( "Sort - default sort by resource then title" , func ( t * testing . T ) {
rows := make ( [ ] * resource . ResourceTableRow , len ( mockResults ) )
for i , r := range mockResults {
rows [ i ] = & resource . ResourceTableRow {
Key : & resource . ResourceKey {
Name : r . Name ,
Resource : r . Resource ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( r . Value ) ,
} ,
}
}
mockResponse := & resource . ResourceSearchResponse {
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{ Name : resource . SEARCH_FIELD_TITLE } ,
} ,
Rows : rows ,
} ,
}
// Create a mock client
mockClient := & MockClient {
MockResponses : [ ] * resource . ResourceSearchResponse { mockResponse } ,
}
features := featuremgmt . WithFeatures ( )
// Initialize the search handler with the mock client
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search" , nil )
req . Header . Add ( "content-type" , "application/json" )
@ -280,6 +343,125 @@ func TestSearchHandler(t *testing.T) {
} )
}
func TestSearchHandlerSharedDashboards ( t * testing . T ) {
t . Run ( "should bail out if FlagUnifiedStorageSearchPermissionFiltering is not enabled globally" , func ( t * testing . T ) {
mockClient := & MockClient { }
features := featuremgmt . WithFeatures ( )
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search?folder=sharedwithme" , nil )
req . Header . Add ( "content-type" , "application/json" )
req = req . WithContext ( identity . WithRequester ( req . Context ( ) , & user . SignedInUser { Namespace : "test" } ) )
searchHandler . DoSearch ( rr , req )
assert . Equal ( t , mockClient . CallCount , 1 )
} )
t . Run ( "should return the dashboards shared with the user" , func ( t * testing . T ) {
// dashboardSearchRequest
mockResponse1 := & resource . ResourceSearchResponse {
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "folder" ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
Name : "dashboardinroot" ,
Resource : "dashboard" ,
} ,
Cells : [ ] [ ] byte { [ ] byte ( "" ) } , // root folder doesn't have uid
} ,
{
Key : & resource . ResourceKey {
Name : "dashboardinprivatefolder" ,
Resource : "dashboard" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "privatefolder" ) , // folder uid
} ,
} ,
{
Key : & resource . ResourceKey {
Name : "dashboardinpublicfolder" ,
Resource : "dashboard" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "publicfolder" ) , // folder uid
} ,
} ,
} ,
} ,
}
// folderSearchRequest
mockResponse2 := & resource . ResourceSearchResponse {
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{
Name : "folder" ,
} ,
} ,
Rows : [ ] * resource . ResourceTableRow {
{
Key : & resource . ResourceKey {
Name : "publicfolder" ,
Resource : "folder" ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( "" ) , // root folder uid
} ,
} ,
} ,
} ,
}
mockClient := & MockClient {
MockResponses : [ ] * resource . ResourceSearchResponse { mockResponse1 , mockResponse2 } ,
}
features := featuremgmt . WithFeatures ( featuremgmt . FlagUnifiedStorageSearchPermissionFiltering )
searchHandler := SearchHandler {
log : log . New ( "test" , "test" ) ,
client : func ( context . Context ) resource . ResourceIndexClient { return mockClient } ,
tracer : tracing . NewNoopTracerService ( ) ,
features : features ,
}
rr := httptest . NewRecorder ( )
req := httptest . NewRequest ( "GET" , "/search?folder=sharedwithme" , nil )
req . Header . Add ( "content-type" , "application/json" )
allPermissions := make ( map [ int64 ] map [ string ] [ ] string )
permissions := make ( map [ string ] [ ] string )
permissions [ dashboards . ActionDashboardsRead ] = [ ] string { "dashboards:uid:dashboardinroot" , "dashboards:uid:dashboardinprivatefolder" , "dashboards:uid:dashboardinpublicfolder" }
allPermissions [ 1 ] = permissions
req = req . WithContext ( identity . WithRequester ( req . Context ( ) , & user . SignedInUser { Namespace : "test" , OrgID : 1 , Permissions : allPermissions } ) )
searchHandler . DoSearch ( rr , req )
assert . Equal ( t , mockClient . CallCount , 3 )
// first call gets all dashboards user has permission for
firstCall := mockClient . MockCalls [ 0 ]
assert . Equal ( t , firstCall . Options . Fields [ 0 ] . Values , [ ] string { "dashboardinroot" , "dashboardinprivatefolder" , "dashboardinpublicfolder" } )
// second call gets folders associated with the previous dashboards
secondCall := mockClient . MockCalls [ 1 ]
assert . Equal ( t , secondCall . Options . Fields [ 0 ] . Values , [ ] string { "privatefolder" , "publicfolder" } )
// lastly, search ONLY for dashboards user has permission to read that are within folders the user does NOT have
// permission to read
thirdCall := mockClient . MockCalls [ 2 ]
assert . Equal ( t , thirdCall . Options . Fields [ 0 ] . Values , [ ] string { "dashboardinprivatefolder" } )
} )
}
// MockClient implements the ResourceIndexClient interface for testing
type MockClient struct {
resource . ResourceIndexClient
@ -287,6 +469,10 @@ type MockClient struct {
// Capture the last SearchRequest for assertions
LastSearchRequest * resource . ResourceSearchRequest
MockResponses [ ] * resource . ResourceSearchResponse
MockCalls [ ] * resource . ResourceSearchRequest
CallCount int
}
type MockResult struct {
@ -320,28 +506,16 @@ var mockResults = []MockResult{
func ( m * MockClient ) Search ( ctx context . Context , in * resource . ResourceSearchRequest , opts ... grpc . CallOption ) ( * resource . ResourceSearchResponse , error ) {
m . LastSearchRequest = in
m . MockCalls = append ( m . MockCalls , in )
rows := make ( [ ] * resource . ResourceTableRow , len ( mockResults ) )
for i , r := range mockResults {
rows [ i ] = & resource . ResourceTableRow {
Key : & resource . ResourceKey {
Name : r . Name ,
Resource : r . Resource ,
} ,
Cells : [ ] [ ] byte {
[ ] byte ( r . Value ) ,
} ,
}
var response * resource . ResourceSearchResponse
if m . CallCount < len ( m . MockResponses ) {
response = m . MockResponses [ m . CallCount ]
}
return & resource . ResourceSearchResponse {
Results : & resource . ResourceTable {
Columns : [ ] * resource . ResourceTableColumnDefinition {
{ Name : resource . SEARCH_FIELD_TITLE } ,
} ,
Rows : rows ,
} ,
} , nil
m . CallCount = m . CallCount + 1
return response , nil
}
func ( m * MockClient ) GetStats ( ctx context . Context , in * resource . ResourceStatsRequest , opts ... grpc . CallOption ) ( * resource . ResourceStatsResponse , error ) {
return nil , nil