diff --git a/docs/sources/troubleshooting/support-bundles/index.md b/docs/sources/troubleshooting/support-bundles/index.md index d5650abfab2..f8decb69cb6 100644 --- a/docs/sources/troubleshooting/support-bundles/index.md +++ b/docs/sources/troubleshooting/support-bundles/index.md @@ -31,6 +31,7 @@ A support bundle can include any of the following components: - **Settings**: Settings for the Grafana instance - **SAML**: Healthcheck connection and metadata for SAML (only displayed if SAML is enabled) - **LDAP**: Healthcheck connection and metadata for LDAP (only displayed if LDAP is enabled) +- **OAuth2**: Healthcheck connection and metadata for each OAuth2 Provider supporter (only displayed if OAuth provider is enabled) ## Steps diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index 0ab25e49009..8f9b9eb6b4e 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -71,7 +72,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt. PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate, PluginSettings: cfg.PluginSettings, }), - SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}), + SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService()), } m := web.New() diff --git a/pkg/api/login_oauth_test.go b/pkg/api/login_oauth_test.go index 7b4e469a1ed..1e5b23dd10d 100644 --- a/pkg/api/login_oauth_test.go +++ b/pkg/api/login_oauth_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -33,7 +34,7 @@ func setupSocialHTTPServerWithConfig(t *testing.T, cfg *setting.Cfg) *HTTPServer Cfg: cfg, License: &licensing.OSSLicensingService{Cfg: cfg}, SQLStore: sqlStore, - SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}), + SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService()), HooksService: hooks.ProvideService(), SecretsService: fakes.NewFakeSecretsService(), Features: features, diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index fa0600715ab..32d65e56619 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -262,3 +262,13 @@ func groupsGraphAPIURL(claims azureClaims, token *oauth2.Token) (string, error) } return endpoint, nil } + +func (s *SocialAzureAD) SupportBundleContent(bf *bytes.Buffer) error { + bf.WriteString("## AzureAD specific configuration\n\n") + bf.WriteString("```ini\n") + bf.WriteString(fmt.Sprintf("allowed_groups = %v\n", s.allowedGroups)) + bf.WriteString(fmt.Sprintf("forceUseGraphAPI = %v\n", s.forceUseGraphAPI)) + bf.WriteString("```\n\n") + + return s.SocialBase.SupportBundleContent(bf) +} diff --git a/pkg/login/social/generic_oauth.go b/pkg/login/social/generic_oauth.go index 7a0425ce3a2..3eb9e76490e 100644 --- a/pkg/login/social/generic_oauth.go +++ b/pkg/login/social/generic_oauth.go @@ -518,3 +518,17 @@ func (s *SocialGenericOAuth) AuthCodeURL(state string, opts ...oauth2.AuthCodeOp } return s.SocialBase.AuthCodeURL(state, opts...) } + +func (s *SocialGenericOAuth) SupportBundleContent(bf *bytes.Buffer) error { + bf.WriteString("## GenericOAuth specific configuration\n\n") + bf.WriteString("```ini\n") + bf.WriteString(fmt.Sprintf("name_attribute_path = %s\n", s.nameAttributePath)) + bf.WriteString(fmt.Sprintf("login_attribute_path = %s\n", s.loginAttributePath)) + bf.WriteString(fmt.Sprintf("id_token_attribute_name = %s\n", s.idTokenAttributeName)) + bf.WriteString(fmt.Sprintf("team_ids_attribute_path = %s\n", s.teamIdsAttributePath)) + bf.WriteString(fmt.Sprintf("team_ids = %v\n", s.teamIds)) + bf.WriteString(fmt.Sprintf("allowed_organizations = %v\n", s.allowedOrganizations)) + bf.WriteString("```\n\n") + + return s.SocialBase.SupportBundleContent(bf) +} diff --git a/pkg/login/social/social.go b/pkg/login/social/social.go index 26c098d7f22..3729e471cac 100644 --- a/pkg/login/social/social.go +++ b/pkg/login/social/social.go @@ -1,6 +1,7 @@ package social import ( + "bytes" "context" "crypto/tls" "crypto/x509" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -34,37 +36,40 @@ type SocialService struct { } type OAuthInfo struct { - ClientId, ClientSecret string - Scopes []string - AuthUrl, TokenUrl string - Enabled bool - EmailAttributeName string - EmailAttributePath string - RoleAttributePath string - RoleAttributeStrict bool - GroupsAttributePath string - TeamIdsAttributePath string - AllowedDomains []string - AllowAssignGrafanaAdmin bool - HostedDomain string - ApiUrl string - TeamsUrl string - AllowSignup bool - Name string - Icon string - TlsClientCert string - TlsClientKey string - TlsClientCa string - TlsSkipVerify bool - UsePKCE bool - AutoLogin bool + ApiUrl string `toml:"api_url"` + AuthUrl string `toml:"auth_url"` + ClientId string `toml:"client_id"` + ClientSecret string `toml:"-"` + EmailAttributeName string `toml:"email_attribute_name"` + EmailAttributePath string `toml:"email_attribute_path"` + GroupsAttributePath string `toml:"groups_attribute_path"` + HostedDomain string `toml:"hosted_domain"` + Icon string `toml:"icon"` + Name string `toml:"name"` + RoleAttributePath string `toml:"role_attribute_path"` + TeamIdsAttributePath string `toml:"team_ids_attribute_path"` + TeamsUrl string `toml:"teams_url"` + TlsClientCa string `toml:"tls_client_ca"` + TlsClientCert string `toml:"tls_client_cert"` + TlsClientKey string `toml:"tls_client_key"` + TokenUrl string `toml:"token_url"` + AllowedDomains []string `toml:"allowed_domains"` + Scopes []string `toml:"scopes"` + AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"` + AllowSignup bool `toml:"allow_signup"` + AutoLogin bool `toml:"auto_login"` + Enabled bool `toml:"enabled"` + RoleAttributeStrict bool `toml:"role_attribute_strict"` + TlsSkipVerify bool `toml:"tls_skip_verify"` + UsePKCE bool `toml:"use_pkce"` } func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager, usageStats usagestats.Service, + bundleRegistry supportbundles.Service, ) *SocialService { - ss := SocialService{ + ss := &SocialService{ cfg: cfg, oAuthProvider: make(map[string]*OAuthInfo), socialMap: make(map[string]SocialConnector), @@ -234,7 +239,9 @@ func ProvideService(cfg *setting.Cfg, } } - return &ss + ss.registerSupportBundleCollectors(bundleRegistry) + + return ss } type BasicUserInfo struct { @@ -261,6 +268,7 @@ type SocialConnector interface { Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) Client(ctx context.Context, t *oauth2.Token) *http.Client TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource + SupportBundleContent(*bytes.Buffer) error } type SocialBase struct { @@ -331,6 +339,27 @@ type groupStruct struct { Groups []string `json:"groups"` } +func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error { + bf.WriteString("## Client configuration\n\n") + bf.WriteString("```ini\n") + bf.WriteString(fmt.Sprintf("allow_assign_grafana_admin = %v\n", s.allowAssignGrafanaAdmin)) + bf.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.allowSignup)) + bf.WriteString(fmt.Sprintf("allowed_domains = %v\n", s.allowedDomains)) + bf.WriteString(fmt.Sprintf("auto_assign_org_role = %v\n", s.autoAssignOrgRole)) + bf.WriteString(fmt.Sprintf("role_attribute_path = %v\n", s.roleAttributePath)) + bf.WriteString(fmt.Sprintf("role_attribute_strict = %v\n", s.roleAttributeStrict)) + bf.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.skipOrgRoleSync)) + bf.WriteString(fmt.Sprintf("client_id = %v\n", s.Config.ClientID)) + bf.WriteString(fmt.Sprintf("client_secret = %v ; issue if empty\n", strings.Repeat("*", len(s.Config.ClientSecret)))) + bf.WriteString(fmt.Sprintf("auth_url = %v\n", s.Config.Endpoint.AuthURL)) + bf.WriteString(fmt.Sprintf("token_url = %v\n", s.Config.Endpoint.TokenURL)) + bf.WriteString(fmt.Sprintf("auth_style = %v\n", s.Config.Endpoint.AuthStyle)) + bf.WriteString(fmt.Sprintf("redirect_url = %v\n", s.Config.RedirectURL)) + bf.WriteString(fmt.Sprintf("scopes = %v\n", s.Config.Scopes)) + bf.WriteString("```\n\n") + return nil +} + func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string, legacy bool) (org.RoleType, bool) { if s.roleAttributePath == "" { return s.defaultRole(legacy), false diff --git a/pkg/login/social/support_bundle.go b/pkg/login/social/support_bundle.go new file mode 100644 index 00000000000..87ee04dd5f4 --- /dev/null +++ b/pkg/login/social/support_bundle.go @@ -0,0 +1,88 @@ +package social + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + + "github.com/BurntSushi/toml" + + "github.com/grafana/grafana/pkg/services/supportbundles" +) + +func (ss *SocialService) registerSupportBundleCollectors(bundleRegistry supportbundles.Service) { + for name := range ss.oAuthProvider { + bundleRegistry.RegisterSupportItemCollector(supportbundles.Collector{ + UID: "oauth-" + name, + DisplayName: "OAuth " + strings.Title(strings.ReplaceAll(name, "_", " ")), + Description: "OAuth configuration and healthchecks for " + name, + IncludedByDefault: false, + Default: false, + Fn: ss.supportBundleCollectorFn(name, ss.socialMap[name], ss.oAuthProvider[name]), + }) + } +} + +func (ss *SocialService) supportBundleCollectorFn(name string, sc SocialConnector, oinfo *OAuthInfo) func(context.Context) (*supportbundles.SupportItem, error) { + return func(ctx context.Context) (*supportbundles.SupportItem, error) { + bWriter := bytes.NewBuffer(nil) + + if _, err := bWriter.WriteString(fmt.Sprintf("# OAuth %s information\n\n", name)); err != nil { + return nil, err + } + + if _, err := bWriter.WriteString("## Parsed Configuration\n\n"); err != nil { + return nil, err + } + + bWriter.WriteString("```toml\n") + errM := toml.NewEncoder(bWriter).Encode(oinfo) + if errM != nil { + bWriter.WriteString( + fmt.Sprintf("Unable to encode OAuth configuration \n Err: %s", errM)) + } + bWriter.WriteString("```\n\n") + + if err := sc.SupportBundleContent(bWriter); err != nil { + return nil, err + } + + ss.healthCheckSocialConnector(ctx, name, oinfo, bWriter) + + return &supportbundles.SupportItem{ + Filename: "oauth-" + name + ".md", + FileBytes: bWriter.Bytes(), + }, nil + } +} + +func (ss *SocialService) healthCheckSocialConnector(ctx context.Context, name string, oinfo *OAuthInfo, bWriter *bytes.Buffer) { + bWriter.WriteString("## Health checks\n\n") + client, err := ss.GetOAuthHttpClient(name) + if err != nil { + bWriter.WriteString(fmt.Sprintf("Unable to create HTTP client \n Err: %s\n", err)) + return + } + + healthCheckEndpoint(client, bWriter, "API", oinfo.ApiUrl) + healthCheckEndpoint(client, bWriter, "Auth", oinfo.AuthUrl) + healthCheckEndpoint(client, bWriter, "Token", oinfo.TokenUrl) + healthCheckEndpoint(client, bWriter, "Teams", oinfo.TeamsUrl) +} + +func healthCheckEndpoint(client *http.Client, bWriter *bytes.Buffer, endpointName string, url string) { + if url == "" { + return + } + + bWriter.WriteString(fmt.Sprintf("### %s URL\n\n", endpointName)) + resp, err := client.Get(url) + _ = resp.Body.Close() + if err != nil { + bWriter.WriteString(fmt.Sprintf("Unable to GET %s URL \n Err: %s\n\n", endpointName, err)) + } else { + bWriter.WriteString(fmt.Sprintf("Able to reach %s URL. Status Code does not need to be 200.\n Retrieved Status Code: %d \n\n", endpointName, resp.StatusCode)) + } +} diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index 1392d1e229e..1962c5e18ff 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -1,6 +1,7 @@ package oauthtoken import ( + "bytes" "context" "errors" "net/http" @@ -303,6 +304,10 @@ func (m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token) return args.Get(0).(oauth2.TokenSource) } +func (m *MockSocialConnector) SupportBundleContent(bf *bytes.Buffer) error { + return nil +} + type FakeAuthInfoStore struct { login.Store ExpectedError error