mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 15:45:43 -06:00
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:
parent
6d5688ed94
commit
ccbf200c4a
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
88
pkg/login/social/support_bundle.go
Normal file
88
pkg/login/social/support_bundle.go
Normal 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))
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user