mirror of
https://github.com/grafana/grafana.git
synced 2024-12-25 08:21:46 -06:00
6abe99efd6
* initial passwordless client * passwordless login page * Working basic e2e flow * Add todo comments * Improve the passwordless login flow * improved passwordless login, backend for passwordless signup * add expiration to emails * update email templates & render username & name fields on signup * improve email templates * change login page text while awaiting passwordless code * fix merge conflicts * use claims.TypeUser * add initial passwordless tests * better error messages * simplified error name * remove completed TODOs * linting & minor test improvements & rename passwordless routes * more linting fixes * move code generation to its own func, use locationService to get query params * fix ampersand in email templates & use passwordless api routes in LoginCtrl * txt emails more closely match html email copy * move passwordless auth behind experimental feature toggle * fix PasswordlessLogin property failing typecheck * make update-workspace * user correct placeholder * Update emails/templates/passwordless_verify_existing_user.txt Co-authored-by: Dan Cech <dcech@grafana.com> * Update emails/templates/passwordless_verify_existing_user.mjml Co-authored-by: Dan Cech <dcech@grafana.com> * Update emails/templates/passwordless_verify_new_user.txt Co-authored-by: Dan Cech <dcech@grafana.com> * Update emails/templates/passwordless_verify_new_user.txt Co-authored-by: Dan Cech <dcech@grafana.com> * Update emails/templates/passwordless_verify_new_user.mjml Co-authored-by: Dan Cech <dcech@grafana.com> * use & in email templates * Update emails/templates/passwordless_verify_existing_user.txt Co-authored-by: Dan Cech <dcech@grafana.com> * remove IP address validation * struct for passwordless settings * revert go.work.sum changes * mock locationService.getSearch in failing test --------- Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com> Co-authored-by: Dan Cech <dcech@grafana.com>
770 lines
27 KiB
Go
770 lines
27 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"hash"
|
|
"net/http"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/api/webassets"
|
|
"github.com/grafana/grafana/pkg/login/social"
|
|
"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/folder"
|
|
"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"
|
|
)
|
|
|
|
// Returns a file that is easy to check for changes
|
|
// Any changes to the file means we should refresh the frontend
|
|
func (hs *HTTPServer) GetFrontendAssets(c *contextmodel.ReqContext) {
|
|
c, span := hs.injectSpan(c, "api.GetFrontendAssets")
|
|
defer span.End()
|
|
|
|
hash := sha256.New()
|
|
keys := map[string]any{}
|
|
|
|
// BuildVersion
|
|
hash.Reset()
|
|
_, _ = hash.Write([]byte(setting.BuildVersion))
|
|
_, _ = hash.Write([]byte(setting.BuildCommit))
|
|
keys["version"] = fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
// Plugin configs
|
|
plugins := []string{}
|
|
for _, p := range hs.pluginStore.Plugins(c.Req.Context()) {
|
|
plugins = append(plugins, fmt.Sprintf("%s@%s", p.Name, p.Info.Version))
|
|
}
|
|
keys["plugins"] = sortedHash(plugins, hash)
|
|
|
|
// Feature flags
|
|
enabled := []string{}
|
|
for flag, set := range hs.Features.GetEnabled(c.Req.Context()) {
|
|
if set {
|
|
enabled = append(enabled, flag)
|
|
}
|
|
}
|
|
keys["flags"] = sortedHash(enabled, hash)
|
|
|
|
// Assets
|
|
hash.Reset()
|
|
dto, err := webassets.GetWebAssets(c.Req.Context(), hs.Cfg, hs.License)
|
|
if err == nil && dto != nil {
|
|
_, _ = hash.Write([]byte(dto.ContentDeliveryURL))
|
|
_, _ = hash.Write([]byte(dto.Dark))
|
|
_, _ = hash.Write([]byte(dto.Light))
|
|
for _, f := range dto.JSFiles {
|
|
_, _ = hash.Write([]byte(f.FilePath))
|
|
_, _ = hash.Write([]byte(f.Integrity))
|
|
}
|
|
}
|
|
keys["assets"] = fmt.Sprintf("%x", hash.Sum(nil))
|
|
|
|
c.JSON(http.StatusOK, keys)
|
|
}
|
|
|
|
func sortedHash(vals []string, hash hash.Hash) string {
|
|
hash.Reset()
|
|
sort.Strings(vals)
|
|
for _, v := range vals {
|
|
_, _ = hash.Write([]byte(v))
|
|
}
|
|
return fmt.Sprintf("%x", hash.Sum(nil))
|
|
}
|
|
|
|
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.
|
|
//
|
|
//nolint:gocyclo
|
|
func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.FrontendSettingsDTO, error) {
|
|
c, span := hs.injectSpan(c, "api.getFrontendSettings")
|
|
defer span.End()
|
|
|
|
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] = hs.newAppDTO(
|
|
c.Req.Context(),
|
|
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(c.Req.Context(), featuremgmt.FlagEnableDatagridEditing) {
|
|
continue
|
|
}
|
|
|
|
panels[panel.ID] = plugins.PanelDTO{
|
|
ID: panel.ID,
|
|
Name: panel.Name,
|
|
AliasIDs: panel.AliasIDs,
|
|
Info: panel.Info,
|
|
Module: panel.Module,
|
|
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), panel),
|
|
BaseURL: panel.BaseURL,
|
|
SkipDataQuery: panel.SkipDataQuery,
|
|
HideFromList: panel.HideFromList,
|
|
ReleaseState: string(panel.State),
|
|
Signature: string(panel.Signature),
|
|
Sort: getPanelSort(panel.ID),
|
|
Angular: panel.Angular,
|
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), panel),
|
|
}
|
|
}
|
|
|
|
hideVersion := hs.Cfg.AnonymousHideVersion && !c.IsSignedIn
|
|
version := setting.BuildVersion
|
|
commit := setting.BuildCommit
|
|
commitShort := getShortCommitHash(setting.BuildCommit, 10)
|
|
buildstamp := setting.BuildStamp
|
|
versionString := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, version, commitShort)
|
|
|
|
if hideVersion {
|
|
version = ""
|
|
versionString = setting.ApplicationName
|
|
commit = ""
|
|
commitShort = ""
|
|
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"))
|
|
isCloudMigrationTarget := hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagOnPremToCloudMigrations) && hs.Cfg.CloudMigration.IsTarget
|
|
featureToggles := hs.Features.GetEnabled(c.Req.Context())
|
|
// this is needed for backwards compatibility with external plugins
|
|
// we should remove this once we can be sure that no external plugins rely on this
|
|
featureToggles["topnav"] = true
|
|
|
|
frontendSettings := &dtos.FrontendSettingsDTO{
|
|
DefaultDatasource: defaultDS,
|
|
Datasources: dataSources,
|
|
MinRefreshInterval: hs.Cfg.MinRefreshInterval,
|
|
Panels: panels,
|
|
Apps: apps,
|
|
AppUrl: hs.Cfg.AppURL,
|
|
AppSubUrl: hs.Cfg.AppSubURL,
|
|
AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin,
|
|
AuthProxyEnabled: hs.Cfg.AuthProxy.Enabled,
|
|
LdapEnabled: hs.Cfg.LDAPAuthEnabled,
|
|
JwtHeaderName: hs.Cfg.JWTAuth.HeaderName,
|
|
JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin,
|
|
LiveEnabled: hs.Cfg.LiveMaxConnections != 0,
|
|
AutoAssignOrg: hs.Cfg.AutoAssignOrg,
|
|
VerifyEmailEnabled: hs.Cfg.VerifyEmailEnabled,
|
|
SigV4AuthEnabled: hs.Cfg.SigV4AuthEnabled,
|
|
AzureAuthEnabled: hs.Cfg.AzureAuthEnabled,
|
|
RbacEnabled: true,
|
|
ExploreEnabled: hs.Cfg.ExploreEnabled,
|
|
HelpEnabled: hs.Cfg.HelpEnabled,
|
|
ProfileEnabled: hs.Cfg.ProfileEnabled,
|
|
NewsFeedEnabled: hs.Cfg.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,
|
|
AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting,
|
|
FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled,
|
|
ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString,
|
|
ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl,
|
|
DisableLoginForm: hs.Cfg.DisableLoginForm,
|
|
DisableUserSignUp: !hs.Cfg.AllowUserSignUp,
|
|
LoginHint: hs.Cfg.LoginHint,
|
|
PasswordHint: hs.Cfg.PasswordHint,
|
|
ExternalUserMngInfo: hs.Cfg.ExternalUserMngInfo,
|
|
ExternalUserMngLinkUrl: hs.Cfg.ExternalUserMngLinkUrl,
|
|
ExternalUserMngLinkName: hs.Cfg.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,
|
|
EnableFrontendSandboxForPlugins: hs.Cfg.EnableFrontendSandboxForPlugins,
|
|
PublicDashboardAccessToken: c.PublicDashboardAccessToken,
|
|
PublicDashboardsEnabled: hs.Cfg.PublicDashboardsEnabled,
|
|
CloudMigrationIsTarget: isCloudMigrationTarget,
|
|
CloudMigrationFeedbackURL: hs.Cfg.CloudMigration.FeedbackURL,
|
|
CloudMigrationPollIntervalMs: int(hs.Cfg.CloudMigration.FrontendPollInterval.Milliseconds()),
|
|
SharedWithMeFolderUID: folder.SharedWithMeFolderUID,
|
|
RootFolderUID: accesscontrol.GeneralFolderUID,
|
|
LocalFileSystemAvailable: hs.Cfg.LocalFileSystemAvailable,
|
|
ReportingStaticContext: hs.Cfg.ReportingStaticContext,
|
|
ExploreDefaultTimeOffset: hs.Cfg.ExploreDefaultTimeOffset,
|
|
|
|
BuildInfo: dtos.FrontendSettingsBuildInfoDTO{
|
|
HideVersion: hideVersion,
|
|
Version: version,
|
|
VersionString: versionString,
|
|
Commit: commit,
|
|
CommitShort: commitShort,
|
|
Buildstamp: buildstamp,
|
|
Edition: hs.License.Edition(),
|
|
LatestVersion: hs.grafanaUpdateChecker.LatestVersion(),
|
|
HasUpdate: hs.grafanaUpdateChecker.UpdateAvailable(),
|
|
Env: hs.Cfg.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: featureToggles,
|
|
AnonymousEnabled: hs.Cfg.AnonymousEnabled,
|
|
AnonymousDeviceLimit: hs.Cfg.AnonymousDeviceLimit,
|
|
RendererAvailable: hs.RenderService.IsAvailable(c.Req.Context()),
|
|
RendererVersion: hs.RenderService.Version(),
|
|
RendererDefaultImageWidth: hs.Cfg.RendererDefaultImageWidth,
|
|
RendererDefaultImageHeight: hs.Cfg.RendererDefaultImageHeight,
|
|
RendererDefaultImageScale: hs.Cfg.RendererDefaultImageScale,
|
|
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,
|
|
PluginCatalogManagedPlugins: hs.managedPluginsService.ManagedPlugins(c.Req.Context()),
|
|
PluginCatalogPreinstalledPlugins: hs.Cfg.PreinstallPlugins,
|
|
ExpressionsEnabled: hs.Cfg.ExpressionsEnabled,
|
|
AwsAllowedAuthProviders: hs.Cfg.AWSAllowedAuthProviders,
|
|
AwsAssumeRoleEnabled: hs.Cfg.AWSAssumeRoleEnabled,
|
|
SupportBundlesEnabled: isSupportBundlesEnabled(hs),
|
|
|
|
Azure: dtos.FrontendSettingsAzureDTO{
|
|
Cloud: hs.Cfg.Azure.Cloud,
|
|
Clouds: hs.Cfg.Azure.CustomClouds(),
|
|
ManagedIdentityEnabled: hs.Cfg.Azure.ManagedIdentityEnabled,
|
|
WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled,
|
|
UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled,
|
|
UserIdentityFallbackCredentialsEnabled: hs.Cfg.Azure.UserIdentityFallbackCredentialsEnabled,
|
|
AzureEntraPasswordCredentialsEnabled: hs.Cfg.Azure.AzureEntraPasswordCredentialsEnabled,
|
|
},
|
|
|
|
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),
|
|
},
|
|
Analytics: dtos.FrontendSettingsAnalyticsDTO{
|
|
Enabled: hs.Cfg.SectionWithEnvOverrides("analytics").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
|
|
}
|
|
|
|
// It returns false if the provider is not enabled or the skip org role sync is false.
|
|
parseSkipOrgRoleSyncEnabled := func(info *social.OAuthInfo) bool {
|
|
if info == nil {
|
|
return false
|
|
}
|
|
return info.SkipOrgRoleSync
|
|
}
|
|
|
|
oauthProviders := hs.SocialService.GetOAuthInfoProviders()
|
|
frontendSettings.Auth = dtos.FrontendSettingsAuthDTO{
|
|
AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken,
|
|
SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync,
|
|
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
|
|
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync,
|
|
GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]),
|
|
GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]),
|
|
GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]),
|
|
AzureADSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.AzureADProviderName]),
|
|
GithubSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitHubProviderName]),
|
|
GitLabSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitlabProviderName]),
|
|
OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]),
|
|
DisableLogin: hs.Cfg.DisableLogin,
|
|
BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy,
|
|
PasswordlessEnabled: hs.Cfg.PasswordlessMagicLinkAuth.Enabled && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagPasswordlessMagicLinkAuthentication),
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Set the kubernetes namespace
|
|
frontendSettings.Namespace = hs.namespacer(c.SignedInUser.OrgID)
|
|
|
|
// experimental scope features
|
|
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagScopeFilters) {
|
|
frontendSettings.ListScopesEndpoint = hs.Cfg.ScopesListScopesURL
|
|
frontendSettings.ListDashboardScopesEndpoint = hs.Cfg.ScopesListDashboardsURL
|
|
}
|
|
|
|
return frontendSettings, nil
|
|
}
|
|
|
|
func isSupportBundlesEnabled(hs *HTTPServer) bool {
|
|
return hs.Cfg.SectionWithEnvOverrides("support_bundles").Key("enabled").MustBool(true)
|
|
}
|
|
|
|
func getShortCommitHash(commitHash string, maxLength int) string {
|
|
if len(commitHash) > maxLength {
|
|
return commitHash[:maxLength]
|
|
}
|
|
return commitHash
|
|
}
|
|
|
|
func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlugins AvailablePlugins) (map[string]plugins.DataSourceDTO, error) {
|
|
c, span := hs.injectSpan(c, "api.getFSDataSources")
|
|
defer span.End()
|
|
|
|
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).FilterDatasourcesByReadPermissions(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,
|
|
APIVersion: ds.APIVersion,
|
|
}
|
|
|
|
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,
|
|
ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), plugin),
|
|
BaseURL: plugin.BaseURL,
|
|
Angular: plugin.Angular,
|
|
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
|
}
|
|
|
|
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,
|
|
// ModuleHash: hs.pluginAssets.ModuleHash(c.Req.Context(), ds),
|
|
BaseURL: ds.BaseURL,
|
|
Angular: ds.Angular,
|
|
},
|
|
}
|
|
if ds.Name == grafanads.DatasourceName {
|
|
dto.ID = grafanads.DatasourceID
|
|
dto.UID = grafanads.DatasourceUID
|
|
}
|
|
dataSources[ds.Name] = dto
|
|
}
|
|
}
|
|
|
|
return dataSources, nil
|
|
}
|
|
|
|
func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin, settings pluginsettings.InfoDTO) *plugins.AppDTO {
|
|
app := &plugins.AppDTO{
|
|
ID: plugin.ID,
|
|
Version: plugin.Info.Version,
|
|
Path: plugin.Module,
|
|
Preload: false,
|
|
Angular: plugin.Angular,
|
|
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
|
Extensions: plugin.Extensions,
|
|
Dependencies: plugin.Dependencies,
|
|
ModuleHash: hs.pluginAssets.ModuleHash(ctx, plugin),
|
|
}
|
|
|
|
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) {
|
|
ctx, span := hs.tracer.Start(ctx, "api.availablePlugins")
|
|
defer span.End()
|
|
|
|
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) {
|
|
ctx, span := hs.tracer.Start(ctx, "api.pluginSettings")
|
|
defer span.End()
|
|
|
|
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
|
|
}
|