grafana/pkg/api/frontendsettings.go
2023-11-01 09:17:38 -07:00

628 lines
21 KiB
Go

package api
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/accesscontrol"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
"github.com/grafana/grafana/pkg/util"
)
func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) {
settings, err := hs.getFrontendSettings(c)
if err != nil {
c.JsonApiErr(400, "Failed to get frontend settings", err)
return
}
c.JSON(http.StatusOK, settings)
}
// getFrontendSettings returns a json object with all the settings needed for front end initialisation.
func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.FrontendSettingsDTO, error) {
availablePlugins, err := hs.availablePlugins(c.Req.Context(), c.SignedInUser.GetOrgID())
if err != nil {
return nil, err
}
apps := make(map[string]*plugins.AppDTO, 0)
for _, ap := range availablePlugins[plugins.TypeApp] {
apps[ap.Plugin.ID] = newAppDTO(
ap.Plugin,
ap.Settings,
)
}
dataSources, err := hs.getFSDataSources(c, availablePlugins)
if err != nil {
return nil, err
}
defaultDS := "-- Grafana --"
for n, ds := range dataSources {
if ds.IsDefault {
defaultDS = n
}
}
panels := make(map[string]plugins.PanelDTO)
for _, ap := range availablePlugins[plugins.TypePanel] {
panel := ap.Plugin
if panel.State == plugins.ReleaseStateAlpha && !hs.Cfg.PluginsEnableAlpha {
continue
}
if panel.ID == "datagrid" && !hs.Features.IsEnabled(featuremgmt.FlagEnableDatagridEditing) {
continue
}
panels[panel.ID] = plugins.PanelDTO{
ID: panel.ID,
Name: panel.Name,
AliasIDs: panel.AliasIDs,
Info: panel.Info,
Module: panel.Module,
BaseURL: panel.BaseURL,
SkipDataQuery: panel.SkipDataQuery,
HideFromList: panel.HideFromList,
ReleaseState: string(panel.State),
Signature: string(panel.Signature),
Sort: getPanelSort(panel.ID),
AngularDetected: panel.AngularDetected,
}
}
hideVersion := hs.Cfg.AnonymousHideVersion && !c.IsSignedIn
version := setting.BuildVersion
commit := setting.BuildCommit
buildstamp := setting.BuildStamp
if hideVersion {
version = ""
commit = ""
buildstamp = 0
}
hasAccess := accesscontrol.HasAccess(hs.AccessControl, c)
secretsManagerPluginEnabled := kvstore.EvaluateRemoteSecretsPlugin(c.Req.Context(), hs.secretsPluginManager, hs.Cfg) == nil
trustedTypesDefaultPolicyEnabled := (hs.Cfg.CSPEnabled && strings.Contains(hs.Cfg.CSPTemplate, "require-trusted-types-for")) || (hs.Cfg.CSPReportOnlyEnabled && strings.Contains(hs.Cfg.CSPReportOnlyTemplate, "require-trusted-types-for"))
frontendSettings := &dtos.FrontendSettingsDTO{
DefaultDatasource: defaultDS,
Datasources: dataSources,
MinRefreshInterval: setting.MinRefreshInterval,
Panels: panels,
Apps: apps,
AppUrl: hs.Cfg.AppURL,
AppSubUrl: hs.Cfg.AppSubURL,
AllowOrgCreate: (setting.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
AuthProxyEnabled: hs.Cfg.AuthProxyEnabled,
LdapEnabled: hs.Cfg.LDAPAuthEnabled,
JwtHeaderName: hs.Cfg.JWTAuthHeaderName,
JwtUrlLogin: hs.Cfg.JWTAuthURLLogin,
AlertingErrorOrTimeout: setting.AlertingErrorOrTimeout,
AlertingNoDataOrNullValues: setting.AlertingNoDataOrNullValues,
AlertingMinInterval: setting.AlertingMinInterval,
LiveEnabled: hs.Cfg.LiveMaxConnections != 0,
AutoAssignOrg: hs.Cfg.AutoAssignOrg,
VerifyEmailEnabled: setting.VerifyEmailEnabled,
SigV4AuthEnabled: setting.SigV4AuthEnabled,
AzureAuthEnabled: setting.AzureAuthEnabled,
RbacEnabled: true,
ExploreEnabled: setting.ExploreEnabled,
HelpEnabled: setting.HelpEnabled,
ProfileEnabled: setting.ProfileEnabled,
NewsFeedEnabled: setting.NewsFeedEnabled,
QueryHistoryEnabled: hs.Cfg.QueryHistoryEnabled,
GoogleAnalyticsId: hs.Cfg.GoogleAnalyticsID,
GoogleAnalytics4Id: hs.Cfg.GoogleAnalytics4ID,
GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews,
RudderstackWriteKey: hs.Cfg.RudderstackWriteKey,
RudderstackDataPlaneUrl: hs.Cfg.RudderstackDataPlaneURL,
RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL,
RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL,
RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL,
FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled,
ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString,
ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl,
DisableLoginForm: hs.Cfg.DisableLoginForm,
DisableUserSignUp: !setting.AllowUserSignUp,
LoginHint: setting.LoginHint,
PasswordHint: setting.PasswordHint,
ExternalUserMngInfo: setting.ExternalUserMngInfo,
ExternalUserMngLinkUrl: setting.ExternalUserMngLinkUrl,
ExternalUserMngLinkName: setting.ExternalUserMngLinkName,
ViewersCanEdit: hs.Cfg.ViewersCanEdit,
AngularSupportEnabled: hs.Cfg.AngularSupportEnabled,
EditorsCanAdmin: hs.Cfg.EditorsCanAdmin,
DisableSanitizeHtml: hs.Cfg.DisableSanitizeHtml,
TrustedTypesDefaultPolicyEnabled: trustedTypesDefaultPolicyEnabled,
CSPReportOnlyEnabled: hs.Cfg.CSPReportOnlyEnabled,
DateFormats: hs.Cfg.DateFormats,
SecureSocksDSProxyEnabled: hs.Cfg.SecureSocksDSProxy.Enabled && hs.Cfg.SecureSocksDSProxy.ShowUI,
DisableFrontendSandboxForPlugins: hs.Cfg.DisableFrontendSandboxForPlugins,
PublicDashboardAccessToken: c.PublicDashboardAccessToken,
Auth: dtos.FrontendSettingsAuthDTO{
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync,
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
GoogleSkipOrgRoleSync: hs.Cfg.GoogleSkipOrgRoleSync,
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuthSkipOrgRoleSync,
GrafanaComSkipOrgRoleSync: hs.Cfg.GrafanaComSkipOrgRoleSync,
GenericOAuthSkipOrgRoleSync: hs.Cfg.GenericOAuthSkipOrgRoleSync,
AzureADSkipOrgRoleSync: hs.Cfg.AzureADSkipOrgRoleSync,
GithubSkipOrgRoleSync: hs.Cfg.GitHubSkipOrgRoleSync,
GitLabSkipOrgRoleSync: hs.Cfg.GitLabSkipOrgRoleSync,
OktaSkipOrgRoleSync: hs.Cfg.OktaSkipOrgRoleSync,
AuthProxyEnableLoginToken: hs.Cfg.AuthProxyEnableLoginToken,
},
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{
HideVersion: hideVersion,
Version: version,
Commit: commit,
Buildstamp: buildstamp,
Edition: hs.License.Edition(),
LatestVersion: hs.grafanaUpdateChecker.LatestVersion(),
HasUpdate: hs.grafanaUpdateChecker.UpdateAvailable(),
Env: setting.Env,
},
LicenseInfo: dtos.FrontendSettingsLicenseInfoDTO{
Expiry: hs.License.Expiry(),
StateInfo: hs.License.StateInfo(),
LicenseUrl: hs.License.LicenseURL(hasAccess(licensing.PageAccess)),
Edition: hs.License.Edition(),
EnabledFeatures: hs.License.EnabledFeatures(),
},
FeatureToggles: hs.Features.GetEnabled(c.Req.Context()),
AnonymousEnabled: hs.Cfg.AnonymousEnabled,
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
RendererVersion: hs.RenderService.Version(),
SecretsManagerPluginEnabled: secretsManagerPluginEnabled,
Http2Enabled: hs.Cfg.Protocol == setting.HTTP2Scheme,
GrafanaJavascriptAgent: hs.Cfg.GrafanaJavascriptAgent,
PluginCatalogURL: hs.Cfg.PluginCatalogURL,
PluginAdminEnabled: hs.Cfg.PluginAdminEnabled,
PluginAdminExternalManageEnabled: hs.Cfg.PluginAdminEnabled && hs.Cfg.PluginAdminExternalManageEnabled,
PluginCatalogHiddenPlugins: hs.Cfg.PluginCatalogHiddenPlugins,
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
SupportBundlesEnabled: isSupportBundlesEnabled(hs),
Azure: dtos.FrontendSettingsAzureDTO{
Cloud: hs.Cfg.Azure.Cloud,
ManagedIdentityEnabled: hs.Cfg.Azure.ManagedIdentityEnabled,
WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled,
UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled,
},
Caching: dtos.FrontendSettingsCachingDTO{
Enabled: hs.Cfg.SectionWithEnvOverrides("caching").Key("enabled").MustBool(true),
},
RecordedQueries: dtos.FrontendSettingsRecordedQueriesDTO{
Enabled: hs.Cfg.SectionWithEnvOverrides("recorded_queries").Key("enabled").MustBool(true),
},
Reporting: dtos.FrontendSettingsReportingDTO{
Enabled: hs.Cfg.SectionWithEnvOverrides("reporting").Key("enabled").MustBool(true),
},
UnifiedAlerting: dtos.FrontendSettingsUnifiedAlertingDTO{
MinInterval: hs.Cfg.UnifiedAlerting.MinInterval.String(),
},
Oauth: hs.getEnabledOAuthProviders(),
SamlEnabled: hs.samlEnabled(),
SamlName: hs.samlName(),
TokenExpirationDayLimit: hs.Cfg.SATokenExpirationDayLimit,
SnapshotEnabled: hs.Cfg.SnapshotEnabled,
SqlConnectionLimits: dtos.FrontendSettingsSqlConnectionLimitsDTO{
MaxOpenConns: hs.Cfg.SqlDatasourceMaxOpenConnsDefault,
MaxIdleConns: hs.Cfg.SqlDatasourceMaxIdleConnsDefault,
ConnMaxLifetime: hs.Cfg.SqlDatasourceMaxConnLifetimeDefault,
},
}
if hs.Cfg.UnifiedAlerting.StateHistory.Enabled {
frontendSettings.UnifiedAlerting.AlertStateHistoryBackend = hs.Cfg.UnifiedAlerting.StateHistory.Backend
frontendSettings.UnifiedAlerting.AlertStateHistoryPrimary = hs.Cfg.UnifiedAlerting.StateHistory.MultiPrimary
}
if hs.Cfg.UnifiedAlerting.Enabled != nil {
frontendSettings.UnifiedAlertingEnabled = *hs.Cfg.UnifiedAlerting.Enabled
}
if setting.AlertingEnabled != nil {
frontendSettings.AlertingEnabled = *setting.AlertingEnabled
}
if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() {
cdnBaseURL, err := hs.pluginsCDNService.BaseURL()
if err != nil {
return nil, fmt.Errorf("plugins cdn base url: %w", err)
}
frontendSettings.PluginsCDNBaseURL = cdnBaseURL
}
if hs.Cfg.GeomapDefaultBaseLayerConfig != nil {
frontendSettings.GeomapDefaultBaseLayerConfig = &hs.Cfg.GeomapDefaultBaseLayerConfig
}
if !hs.Cfg.GeomapEnableCustomBaseLayers {
frontendSettings.GeomapDisableCustomBaseLayer = true
}
return frontendSettings, nil
}
func isSupportBundlesEnabled(hs *HTTPServer) bool {
return hs.Cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(true)
}
func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlugins AvailablePlugins) (map[string]plugins.DataSourceDTO, error) {
orgDataSources := make([]*datasources.DataSource, 0)
if c.SignedInUser.GetOrgID() != 0 {
query := datasources.GetDataSourcesQuery{OrgID: c.SignedInUser.GetOrgID(), DataSourceLimit: hs.Cfg.DataSourceLimit}
dataSources, err := hs.DataSourcesService.GetDataSources(c.Req.Context(), &query)
if err != nil {
return nil, err
}
if c.IsPublicDashboardView() {
// If RBAC is enabled, it will filter out all datasources for a public user, so we need to skip it
orgDataSources = dataSources
} else {
filtered, err := hs.dsGuardian.New(c.SignedInUser.OrgID, c.SignedInUser).FilterDatasourcesByQueryPermissions(dataSources)
if err != nil {
return nil, err
}
orgDataSources = filtered
}
}
dataSources := make(map[string]plugins.DataSourceDTO)
for _, ds := range orgDataSources {
url := ds.URL
if ds.Access == datasources.DS_ACCESS_PROXY {
url = "/api/datasources/proxy/uid/" + ds.UID
}
dsDTO := plugins.DataSourceDTO{
ID: ds.ID,
UID: ds.UID,
Type: ds.Type,
Name: ds.Name,
URL: url,
IsDefault: ds.IsDefault,
Access: string(ds.Access),
ReadOnly: ds.ReadOnly,
}
ap, exists := availablePlugins.Get(plugins.TypeDataSource, ds.Type)
if !exists {
c.Logger.Error("Could not find plugin definition for data source", "datasource_type", ds.Type)
continue
}
plugin := ap.Plugin
dsDTO.Type = plugin.ID
dsDTO.Preload = plugin.Preload
dsDTO.Module = plugin.Module
dsDTO.PluginMeta = &plugins.PluginMetaDTO{
JSONData: plugin.JSONData,
Signature: plugin.Signature,
Module: plugin.Module,
BaseURL: plugin.BaseURL,
}
dsDTO.AngularDetected = plugin.AngularDetected
if ds.JsonData == nil {
dsDTO.JSONData = make(map[string]any)
} else {
dsDTO.JSONData = ds.JsonData.MustMap()
}
if ds.Access == datasources.DS_ACCESS_DIRECT {
if ds.BasicAuth {
password, err := hs.DataSourcesService.DecryptedBasicAuthPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.BasicAuth = util.GetBasicAuthHeader(
ds.BasicAuthUser,
password,
)
}
if ds.WithCredentials {
dsDTO.WithCredentials = ds.WithCredentials
}
if ds.Type == datasources.DS_INFLUXDB_08 {
password, err := hs.DataSourcesService.DecryptedPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.Username = ds.User
dsDTO.Password = password
dsDTO.URL = url + "/db/" + ds.Database
}
if ds.Type == datasources.DS_INFLUXDB {
password, err := hs.DataSourcesService.DecryptedPassword(c.Req.Context(), ds)
if err != nil {
return nil, err
}
dsDTO.Username = ds.User
dsDTO.Password = password
dsDTO.URL = url
}
}
// Update `jsonData.database` for outdated provisioned SQL datasources created WITHOUT the `jsonData` object in their configuration.
// In these cases, the `Database` value is defined (if at all) on the root level of the provisioning config object.
// This is done for easier warning/error checking on the front end.
if (ds.Type == datasources.DS_MSSQL) || (ds.Type == datasources.DS_MYSQL) || (ds.Type == datasources.DS_POSTGRES) {
// Only update if the value isn't already assigned.
if dsDTO.JSONData["database"] == nil || dsDTO.JSONData["database"] == "" {
dsDTO.JSONData["database"] = ds.Database
}
}
if (ds.Type == datasources.DS_INFLUXDB) || (ds.Type == datasources.DS_ES) {
dsDTO.Database = ds.Database
}
if ds.Type == datasources.DS_PROMETHEUS {
// add unproxied server URL for link to Prometheus web UI
ds.JsonData.Set("directUrl", ds.URL)
}
dataSources[ds.Name] = dsDTO
}
// add data sources that are built in (meaning they are not added via data sources page, nor have any entry in
// the datasource table)
for _, ds := range hs.pluginStore.Plugins(c.Req.Context(), plugins.TypeDataSource) {
if ds.BuiltIn {
dto := plugins.DataSourceDTO{
Type: string(ds.Type),
Name: ds.Name,
JSONData: make(map[string]any),
PluginMeta: &plugins.PluginMetaDTO{
JSONData: ds.JSONData,
Signature: ds.Signature,
Module: ds.Module,
BaseURL: ds.BaseURL,
},
AngularDetected: ds.AngularDetected,
}
if ds.Name == grafanads.DatasourceName {
dto.ID = grafanads.DatasourceID
dto.UID = grafanads.DatasourceUID
}
dataSources[ds.Name] = dto
}
}
return dataSources, nil
}
func newAppDTO(plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plugins.AppDTO {
app := &plugins.AppDTO{
ID: plugin.ID,
Version: plugin.Info.Version,
Path: plugin.Module,
Preload: false,
AngularDetected: plugin.AngularDetected,
}
if settings.Enabled {
app.Preload = plugin.Preload
}
return app
}
func getPanelSort(id string) int {
sort := 100
switch id {
case "timeseries":
sort = 1
case "barchart":
sort = 2
case "stat":
sort = 3
case "gauge":
sort = 4
case "bargauge":
sort = 5
case "table":
sort = 6
case "singlestat":
sort = 7
case "piechart":
sort = 8
case "state-timeline":
sort = 9
case "heatmap":
sort = 10
case "status-history":
sort = 11
case "histogram":
sort = 12
case "graph":
sort = 13
case "text":
sort = 14
case "alertlist":
sort = 15
case "dashlist":
sort = 16
case "news":
sort = 17
}
return sort
}
type availablePluginDTO struct {
Plugin pluginstore.Plugin
Settings pluginsettings.InfoDTO
}
// AvailablePlugins represents a mapping from plugin types (panel, data source, etc.) to plugin IDs to plugins
// For example ["panel"] -> ["piechart"] -> {pie chart plugin DTO}
type AvailablePlugins map[plugins.Type]map[string]*availablePluginDTO
func (ap AvailablePlugins) Get(pluginType plugins.Type, pluginID string) (*availablePluginDTO, bool) {
p, exists := ap[pluginType][pluginID]
if exists {
return p, true
}
for _, p = range ap[pluginType] {
if p.Plugin.ID == pluginID || slices.Contains(p.Plugin.AliasIDs, pluginID) {
return p, true
}
}
return nil, false
}
func (hs *HTTPServer) availablePlugins(ctx context.Context, orgID int64) (AvailablePlugins, error) {
ap := make(AvailablePlugins)
pluginSettingMap, err := hs.pluginSettings(ctx, orgID)
if err != nil {
return ap, err
}
apps := make(map[string]*availablePluginDTO)
for _, app := range hs.pluginStore.Plugins(ctx, plugins.TypeApp) {
if s, exists := pluginSettingMap[app.ID]; exists {
app.Pinned = s.Pinned
apps[app.ID] = &availablePluginDTO{
Plugin: app,
Settings: *s,
}
}
}
ap[plugins.TypeApp] = apps
dataSources := make(map[string]*availablePluginDTO)
for _, ds := range hs.pluginStore.Plugins(ctx, plugins.TypeDataSource) {
if s, exists := pluginSettingMap[ds.ID]; exists {
dataSources[ds.ID] = &availablePluginDTO{
Plugin: ds,
Settings: *s,
}
}
}
ap[plugins.TypeDataSource] = dataSources
panels := make(map[string]*availablePluginDTO)
for _, p := range hs.pluginStore.Plugins(ctx, plugins.TypePanel) {
if s, exists := pluginSettingMap[p.ID]; exists {
panels[p.ID] = &availablePluginDTO{
Plugin: p,
Settings: *s,
}
}
}
ap[plugins.TypePanel] = panels
return ap, nil
}
func (hs *HTTPServer) pluginSettings(ctx context.Context, orgID int64) (map[string]*pluginsettings.InfoDTO, error) {
pluginSettings := make(map[string]*pluginsettings.InfoDTO)
// fill settings from database
if pss, err := hs.PluginSettings.GetPluginSettings(ctx, &pluginsettings.GetArgs{OrgID: orgID}); err != nil {
return nil, err
} else {
for _, ps := range pss {
pluginSettings[ps.PluginID] = ps
}
}
// fill settings from app plugins
for _, plugin := range hs.pluginStore.Plugins(ctx, plugins.TypeApp) {
// ignore settings that already exist
if _, exists := pluginSettings[plugin.ID]; exists {
continue
}
// add new setting which is enabled depending on if AutoEnabled: true
pluginSetting := &pluginsettings.InfoDTO{
PluginID: plugin.ID,
OrgID: orgID,
Enabled: plugin.AutoEnabled,
Pinned: plugin.AutoEnabled,
PluginVersion: plugin.Info.Version,
}
pluginSettings[plugin.ID] = pluginSetting
}
// fill settings from all remaining plugins (including potential app child plugins)
for _, plugin := range hs.pluginStore.Plugins(ctx) {
// ignore settings that already exist
if _, exists := pluginSettings[plugin.ID]; exists {
continue
}
// add new setting which is enabled by default
pluginSetting := &pluginsettings.InfoDTO{
PluginID: plugin.ID,
OrgID: orgID,
Enabled: true,
Pinned: false,
PluginVersion: plugin.Info.Version,
}
// if plugin is included in an app, check app settings
if plugin.IncludedInAppID != "" {
// app child plugins are disabled unless app is enabled
pluginSetting.Enabled = false
if p, exists := pluginSettings[plugin.IncludedInAppID]; exists {
pluginSetting.Enabled = p.Enabled
}
}
pluginSettings[plugin.ID] = pluginSetting
}
return pluginSettings, nil
}
func (hs *HTTPServer) getEnabledOAuthProviders() map[string]any {
providers := make(map[string]any)
for key, oauth := range hs.SocialService.GetOAuthInfoProviders() {
providers[key] = map[string]string{
"name": oauth.Name,
"icon": oauth.Icon,
}
}
return providers
}