2022-06-02 18:27:23 -08:00
package service
import (
"context"
2022-10-19 17:24:00 -03:00
"errors"
2022-06-22 13:58:52 -08:00
"time"
2022-06-02 18:27:23 -08:00
2022-08-29 18:13:06 -03:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2022-07-06 15:51:44 -08:00
"github.com/grafana/grafana/pkg/infra/log"
2022-06-02 18:27:23 -08:00
"github.com/grafana/grafana/pkg/models"
2022-10-18 19:48:20 -06:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/dashboards"
2022-07-06 15:51:44 -08:00
"github.com/grafana/grafana/pkg/services/publicdashboards"
2022-10-25 16:29:18 -03:00
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
2022-07-06 15:51:44 -08:00
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
2022-07-21 13:56:20 -06:00
"github.com/grafana/grafana/pkg/services/publicdashboards/validation"
2022-08-29 18:13:06 -03:00
"github.com/grafana/grafana/pkg/services/query"
2022-08-10 11:56:48 +02:00
"github.com/grafana/grafana/pkg/services/user"
2022-07-06 15:51:44 -08:00
"github.com/grafana/grafana/pkg/setting"
2022-08-29 18:13:06 -03:00
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
2022-10-25 16:29:18 -03:00
"github.com/grafana/grafana/pkg/util"
2022-06-02 18:27:23 -08:00
)
2022-10-20 16:43:33 -03:00
// PublicDashboardServiceImpl Define the Service Implementation. We're generating mock implementation
2022-07-06 15:51:44 -08:00
// automatically
type PublicDashboardServiceImpl struct {
2022-08-29 18:13:06 -03:00
log log . Logger
cfg * setting . Cfg
store publicdashboards . Store
intervalCalculator intervalv2 . Calculator
QueryDataService * query . Service
2022-10-18 19:48:20 -06:00
AnnotationsRepo annotations . Repository
2022-10-19 17:24:00 -03:00
ac accesscontrol . AccessControl
2022-07-06 15:51:44 -08:00
}
2022-08-01 14:46:48 -08:00
var LogPrefix = "publicdashboards.service"
2022-07-06 15:51:44 -08:00
// Gives us compile time error if the service does not adhere to the contract of
// the interface
var _ publicdashboards . Service = ( * PublicDashboardServiceImpl ) ( nil )
2022-10-20 16:43:33 -03:00
// ProvideService Factory for method used by wire to inject dependencies.
2022-07-06 15:51:44 -08:00
// builds the service, and api, and configures routes
func ProvideService (
cfg * setting . Cfg ,
store publicdashboards . Store ,
2022-08-29 18:13:06 -03:00
qds * query . Service ,
2022-10-18 19:48:20 -06:00
anno annotations . Repository ,
2022-10-19 17:24:00 -03:00
ac accesscontrol . AccessControl ,
2022-07-06 15:51:44 -08:00
) * PublicDashboardServiceImpl {
return & PublicDashboardServiceImpl {
2022-08-29 18:13:06 -03:00
log : log . New ( LogPrefix ) ,
cfg : cfg ,
store : store ,
intervalCalculator : intervalv2 . NewCalculator ( ) ,
QueryDataService : qds ,
2022-10-18 19:48:20 -06:00
AnnotationsRepo : anno ,
2022-10-19 17:24:00 -03:00
ac : ac ,
2022-07-06 15:51:44 -08:00
}
}
2022-10-20 16:43:33 -03:00
// GetDashboard Gets a dashboard by Uid
2022-07-21 13:56:20 -06:00
func ( pd * PublicDashboardServiceImpl ) GetDashboard ( ctx context . Context , dashboardUid string ) ( * models . Dashboard , error ) {
dashboard , err := pd . store . GetDashboard ( ctx , dashboardUid )
if err != nil {
return nil , err
}
return dashboard , err
}
2022-10-21 18:25:20 -03:00
// GetPublicDashboardAndDashboard Gets public dashboard via access token
func ( pd * PublicDashboardServiceImpl ) GetPublicDashboardAndDashboard ( ctx context . Context , accessToken string ) ( * PublicDashboard , * models . Dashboard , error ) {
pubdash , dash , err := pd . store . GetPublicDashboardAndDashboard ( ctx , accessToken )
2022-10-17 13:17:24 -08:00
ctxLogger := pd . log . FromContext ( ctx )
2022-06-02 18:27:23 -08:00
if err != nil {
2022-08-26 01:21:52 -08:00
return nil , nil , err
2022-06-02 18:27:23 -08:00
}
2022-10-17 13:17:24 -08:00
if pubdash == nil {
2022-10-21 18:25:20 -03:00
ctxLogger . Error ( "GetPublicDashboardAndDashboard: Public dashboard not found" , "accessToken" , accessToken )
2022-10-17 13:17:24 -08:00
return nil , nil , ErrPublicDashboardNotFound
}
if dash == nil {
2022-10-21 18:25:20 -03:00
ctxLogger . Error ( "GetPublicDashboardAndDashboard: Dashboard not found" , "accessToken" , accessToken )
2022-08-26 01:21:52 -08:00
return nil , nil , ErrPublicDashboardNotFound
2022-06-02 18:27:23 -08:00
}
2022-06-22 13:58:52 -08:00
if ! pubdash . IsEnabled {
2022-10-21 18:25:20 -03:00
ctxLogger . Error ( "GetPublicDashboardAndDashboard: Public dashboard is disabled" , "accessToken" , accessToken )
2022-08-26 01:21:52 -08:00
return nil , nil , ErrPublicDashboardNotFound
2022-06-02 18:27:23 -08:00
}
2022-08-26 01:21:52 -08:00
return pubdash , dash , nil
2022-06-02 18:27:23 -08:00
}
2022-10-21 18:25:20 -03:00
// GetPublicDashboard is a helper method to retrieve the public dashboard configuration for a given dashboard from the database
func ( pd * PublicDashboardServiceImpl ) GetPublicDashboard ( ctx context . Context , orgId int64 , dashboardUid string ) ( * PublicDashboard , error ) {
pdc , err := pd . store . GetPublicDashboard ( ctx , orgId , dashboardUid )
2022-06-02 18:27:23 -08:00
if err != nil {
return nil , err
}
return pdc , nil
}
2022-10-21 18:25:20 -03:00
// SavePublicDashboard is a helper method to persist the sharing config
2022-06-02 18:27:23 -08:00
// to the database. It handles validations for sharing config and persistence
2022-10-21 18:25:20 -03:00
func ( pd * PublicDashboardServiceImpl ) SavePublicDashboard ( ctx context . Context , u * user . SignedInUser , dto * SavePublicDashboardConfigDTO ) ( * PublicDashboard , error ) {
2022-09-28 15:34:53 -03:00
// validate if the dashboard exists
2022-07-21 13:56:20 -06:00
dashboard , err := pd . GetDashboard ( ctx , dto . DashboardUid )
if err != nil {
return nil , err
}
2022-08-26 11:28:54 -08:00
2022-06-22 13:58:52 -08:00
// set default value for time settings
if dto . PublicDashboard . TimeSettings == nil {
2022-09-13 13:33:41 -03:00
dto . PublicDashboard . TimeSettings = & TimeSettings { }
2022-06-02 18:27:23 -08:00
}
2022-08-26 11:28:54 -08:00
// get existing public dashboard if exists
existingPubdash , err := pd . store . GetPublicDashboardByUid ( ctx , dto . PublicDashboard . Uid )
if err != nil {
return nil , err
2022-06-22 13:58:52 -08:00
}
2022-06-02 18:27:23 -08:00
2022-08-26 11:28:54 -08:00
// save changes
var pubdashUid string
if existingPubdash == nil {
2022-09-28 15:34:53 -03:00
err = validation . ValidateSavePublicDashboard ( dto , dashboard )
if err != nil {
return nil , err
}
2022-10-21 18:25:20 -03:00
pubdashUid , err = pd . savePublicDashboard ( ctx , dto )
2022-08-26 11:28:54 -08:00
} else {
2022-10-21 18:25:20 -03:00
pubdashUid , err = pd . updatePublicDashboard ( ctx , dto )
2022-08-26 11:28:54 -08:00
}
if err != nil {
return nil , err
}
//Get latest public dashboard to return
newPubdash , err := pd . store . GetPublicDashboardByUid ( ctx , pubdashUid )
if err != nil {
return nil , err
}
pd . logIsEnabledChanged ( existingPubdash , newPubdash , u )
return newPubdash , err
2022-06-22 13:58:52 -08:00
}
2022-10-25 16:29:18 -03:00
// GenerateNewPublicDashboardUid Generates a unique uid to create a public dashboard. Will make 3 attempts and fail if it cannot find an unused uid
func ( pd * PublicDashboardServiceImpl ) GenerateNewPublicDashboardUid ( ctx context . Context ) ( string , error ) {
var uid string
for i := 0 ; i < 3 ; i ++ {
uid = util . GenerateShortUID ( )
pubdash , _ := pd . store . GetPublicDashboardByUid ( ctx , uid )
if pubdash == nil {
return uid , nil
}
}
return "" , ErrPublicDashboardFailedGenerateUniqueUid
}
// GenerateNewPublicDashboardAccessToken Generates a unique accessToken to create a public dashboard. Will make 3 attempts and fail if it cannot find an unused access token
func ( pd * PublicDashboardServiceImpl ) GenerateNewPublicDashboardAccessToken ( ctx context . Context ) ( string , error ) {
var accessToken string
for i := 0 ; i < 3 ; i ++ {
var err error
accessToken , err = tokens . GenerateAccessToken ( )
if err != nil {
continue
}
pubdash , _ := pd . store . GetPublicDashboardByAccessToken ( ctx , accessToken )
if pubdash == nil {
return accessToken , nil
}
}
return "" , ErrPublicDashboardFailedGenerateAccessToken
}
2022-10-21 18:25:20 -03:00
// Called by SavePublicDashboard this handles business logic
2022-08-26 11:28:54 -08:00
// to generate token and calls create at the database layer
2022-10-21 18:25:20 -03:00
func ( pd * PublicDashboardServiceImpl ) savePublicDashboard ( ctx context . Context , dto * SavePublicDashboardConfigDTO ) ( string , error ) {
2022-10-25 16:29:18 -03:00
uid , err := pd . GenerateNewPublicDashboardUid ( ctx )
2022-06-02 18:27:23 -08:00
if err != nil {
2022-08-26 11:28:54 -08:00
return "" , err
2022-06-02 18:27:23 -08:00
}
2022-10-25 16:29:18 -03:00
accessToken , err := pd . GenerateNewPublicDashboardAccessToken ( ctx )
2022-06-13 17:23:56 -06:00
if err != nil {
2022-08-26 11:28:54 -08:00
return "" , err
2022-06-13 17:23:56 -06:00
}
2022-07-06 15:51:44 -08:00
cmd := SavePublicDashboardConfigCommand {
PublicDashboard : PublicDashboard {
2022-10-21 13:42:14 -06:00
Uid : uid ,
DashboardUid : dto . DashboardUid ,
OrgId : dto . OrgId ,
IsEnabled : dto . PublicDashboard . IsEnabled ,
AnnotationsEnabled : dto . PublicDashboard . AnnotationsEnabled ,
TimeSettings : dto . PublicDashboard . TimeSettings ,
CreatedBy : dto . UserId ,
CreatedAt : time . Now ( ) ,
AccessToken : accessToken ,
2022-06-22 13:58:52 -08:00
} ,
2022-06-13 17:23:56 -06:00
}
2022-10-21 18:25:20 -03:00
err = pd . store . SavePublicDashboard ( ctx , cmd )
2022-08-26 11:28:54 -08:00
if err != nil {
return "" , err
}
return uid , nil
2022-06-22 13:58:52 -08:00
}
2022-08-26 11:28:54 -08:00
// Called by SavePublicDashboard this handles business logic for updating a
// dashboard and calls update at the database layer
2022-10-21 18:25:20 -03:00
func ( pd * PublicDashboardServiceImpl ) updatePublicDashboard ( ctx context . Context , dto * SavePublicDashboardConfigDTO ) ( string , error ) {
2022-07-06 15:51:44 -08:00
cmd := SavePublicDashboardConfigCommand {
PublicDashboard : PublicDashboard {
2022-10-21 13:42:14 -06:00
Uid : dto . PublicDashboard . Uid ,
IsEnabled : dto . PublicDashboard . IsEnabled ,
AnnotationsEnabled : dto . PublicDashboard . AnnotationsEnabled ,
TimeSettings : dto . PublicDashboard . TimeSettings ,
UpdatedBy : dto . UserId ,
UpdatedAt : time . Now ( ) ,
2022-06-22 13:58:52 -08:00
} ,
}
2022-10-21 18:25:20 -03:00
return dto . PublicDashboard . Uid , pd . store . UpdatePublicDashboard ( ctx , cmd )
2022-06-22 13:58:52 -08:00
}
2022-10-19 17:24:00 -03:00
// Gets a list of public dashboards by orgId
func ( pd * PublicDashboardServiceImpl ) ListPublicDashboards ( ctx context . Context , u * user . SignedInUser , orgId int64 ) ( [ ] PublicDashboardListResponse , error ) {
publicDashboards , err := pd . store . ListPublicDashboards ( ctx , orgId )
if err != nil {
return nil , err
}
return pd . filterDashboardsByPermissions ( ctx , u , publicDashboards )
}
2022-10-21 18:25:20 -03:00
func ( pd * PublicDashboardServiceImpl ) PublicDashboardIsEnabled ( ctx context . Context , dashboardUid string ) ( bool , error ) {
2022-10-21 09:37:38 -03:00
return pd . store . PublicDashboardEnabledExistsByDashboardUid ( ctx , dashboardUid )
2022-07-19 17:44:41 -06:00
}
2022-10-21 09:37:38 -03:00
func ( pd * PublicDashboardServiceImpl ) PublicDashboardEnabledExistsByAccessToken ( ctx context . Context , accessToken string ) ( bool , error ) {
return pd . store . PublicDashboardEnabledExistsByAccessToken ( ctx , accessToken )
2022-08-10 11:14:48 -06:00
}
2022-10-06 12:35:19 -08:00
func ( pd * PublicDashboardServiceImpl ) GetPublicDashboardOrgId ( ctx context . Context , accessToken string ) ( int64 , error ) {
return pd . store . GetPublicDashboardOrgId ( ctx , accessToken )
2022-06-22 13:58:52 -08:00
}
2022-08-26 11:28:54 -08:00
2022-08-29 18:13:06 -03:00
// intervalMS and maxQueryData values are being calculated on the frontend for regular dashboards
// we are doing the same for public dashboards but because this access would be public, we need a way to keep this
// values inside reasonable bounds to avoid an attack that could hit data sources with a small interval and a big
// time range and perform big calculations
// this is an additional validation, all data sources implements QueryData interface and should have proper validations
// of these limits
// for the maxDataPoints we took a hard limit from prometheus which is 11000
2022-09-07 12:08:52 -06:00
func ( pd * PublicDashboardServiceImpl ) getSafeIntervalAndMaxDataPoints ( reqDTO PublicDashboardQueryDTO , ts TimeSettings ) ( int64 , int64 ) {
2022-08-29 18:13:06 -03:00
// arbitrary max value for all data sources, it is actually a hard limit defined in prometheus
safeResolution := int64 ( 11000 )
// interval calculated on the frontend
interval := time . Duration ( reqDTO . IntervalMs ) * time . Millisecond
// calculate a safe interval with time range from dashboard and safeResolution
dataTimeRange := legacydata . NewDataTimeRange ( ts . From , ts . To )
tr := backend . TimeRange {
From : dataTimeRange . GetFromAsTimeUTC ( ) ,
To : dataTimeRange . GetToAsTimeUTC ( ) ,
}
safeInterval := pd . intervalCalculator . CalculateSafeInterval ( tr , safeResolution )
if interval > safeInterval . Value {
return reqDTO . IntervalMs , reqDTO . MaxDataPoints
}
return safeInterval . Value . Milliseconds ( ) , safeResolution
}
2022-08-26 11:28:54 -08:00
// Log when PublicDashboard.IsEnabled changed
func ( pd * PublicDashboardServiceImpl ) logIsEnabledChanged ( existingPubdash * PublicDashboard , newPubdash * PublicDashboard , u * user . SignedInUser ) {
if publicDashboardIsEnabledChanged ( existingPubdash , newPubdash ) {
verb := "disabled"
if newPubdash . IsEnabled {
verb = "enabled"
}
2022-10-17 13:17:24 -08:00
pd . log . Info ( "Public dashboard " + verb , "publicDashboardUid" , newPubdash . Uid , "dashboardUid" , newPubdash . DashboardUid , "user" , u . Login )
2022-08-26 11:28:54 -08:00
}
}
2022-10-19 17:24:00 -03:00
// Filter out dashboards that user does not have read access to
func ( pd * PublicDashboardServiceImpl ) filterDashboardsByPermissions ( ctx context . Context , u * user . SignedInUser , publicDashboards [ ] PublicDashboardListResponse ) ( [ ] PublicDashboardListResponse , error ) {
result := make ( [ ] PublicDashboardListResponse , 0 )
for i := range publicDashboards {
hasAccess , err := pd . ac . Evaluate ( ctx , u , accesscontrol . EvalPermission ( dashboards . ActionDashboardsRead , dashboards . ScopeDashboardsProvider . GetResourceScopeUID ( publicDashboards [ i ] . DashboardUid ) ) )
// If original dashboard does not exist, the public dashboard is an orphan. We want to list it anyway
if err != nil && ! errors . Is ( err , dashboards . ErrDashboardNotFound ) {
return nil , err
}
// If user has access to the original dashboard or the dashboard does not exist, add the pubdash to the result
if hasAccess || errors . Is ( err , dashboards . ErrDashboardNotFound ) {
result = append ( result , publicDashboards [ i ] )
}
}
return result , nil
}
// Checks to see if PublicDashboard.IsEnabled is true on create or changed on update
2022-08-26 11:28:54 -08:00
func publicDashboardIsEnabledChanged ( existingPubdash * PublicDashboard , newPubdash * PublicDashboard ) bool {
// creating dashboard, enabled true
newDashCreated := existingPubdash == nil && newPubdash . IsEnabled
// updating dashboard, enabled changed
isEnabledChanged := existingPubdash != nil && newPubdash . IsEnabled != existingPubdash . IsEnabled
return newDashCreated || isEnabledChanged
}