SupportBundles: Add OAuth bundle collectors (#64810)

* wip

* add oauth support bundles

* add specific configs for generic oauth and azureAD

* add doc entry

* optimize struct packing

* Update pkg/login/social/azuread_oauth.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* nit update

---------

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Jo 2023-03-16 07:46:25 +00:00 committed by GitHub
parent 6d5688ed94
commit ccbf200c4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 28 deletions

View File

@ -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

View File

@ -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()

View File

@ -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,

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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))
}
}

View File

@ -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