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:
@@ -31,6 +31,7 @@ A support bundle can include any of the following components:
|
|||||||
- **Settings**: Settings for the Grafana instance
|
- **Settings**: Settings for the Grafana instance
|
||||||
- **SAML**: Healthcheck connection and metadata for SAML (only displayed if SAML is enabled)
|
- **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)
|
- **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
|
## Steps
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
||||||
"github.com/grafana/grafana/pkg/services/rendering"
|
"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/services/updatechecker"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
@@ -71,7 +72,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
|
|||||||
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
PluginsCDNURLTemplate: cfg.PluginsCDNURLTemplate,
|
||||||
PluginSettings: cfg.PluginSettings,
|
PluginSettings: cfg.PluginSettings,
|
||||||
}),
|
}),
|
||||||
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}),
|
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService()),
|
||||||
}
|
}
|
||||||
|
|
||||||
m := web.New()
|
m := web.New()
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/licensing"
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
"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/setting"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
@@ -33,7 +34,7 @@ func setupSocialHTTPServerWithConfig(t *testing.T, cfg *setting.Cfg) *HTTPServer
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||||
SQLStore: sqlStore,
|
SQLStore: sqlStore,
|
||||||
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}),
|
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService()),
|
||||||
HooksService: hooks.ProvideService(),
|
HooksService: hooks.ProvideService(),
|
||||||
SecretsService: fakes.NewFakeSecretsService(),
|
SecretsService: fakes.NewFakeSecretsService(),
|
||||||
Features: features,
|
Features: features,
|
||||||
|
|||||||
@@ -262,3 +262,13 @@ func groupsGraphAPIURL(claims azureClaims, token *oauth2.Token) (string, error)
|
|||||||
}
|
}
|
||||||
return endpoint, nil
|
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...)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
+55
-26
@@ -1,6 +1,7 @@
|
|||||||
package social
|
package social
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"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/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
@@ -34,37 +36,40 @@ type SocialService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuthInfo struct {
|
type OAuthInfo struct {
|
||||||
ClientId, ClientSecret string
|
ApiUrl string `toml:"api_url"`
|
||||||
Scopes []string
|
AuthUrl string `toml:"auth_url"`
|
||||||
AuthUrl, TokenUrl string
|
ClientId string `toml:"client_id"`
|
||||||
Enabled bool
|
ClientSecret string `toml:"-"`
|
||||||
EmailAttributeName string
|
EmailAttributeName string `toml:"email_attribute_name"`
|
||||||
EmailAttributePath string
|
EmailAttributePath string `toml:"email_attribute_path"`
|
||||||
RoleAttributePath string
|
GroupsAttributePath string `toml:"groups_attribute_path"`
|
||||||
RoleAttributeStrict bool
|
HostedDomain string `toml:"hosted_domain"`
|
||||||
GroupsAttributePath string
|
Icon string `toml:"icon"`
|
||||||
TeamIdsAttributePath string
|
Name string `toml:"name"`
|
||||||
AllowedDomains []string
|
RoleAttributePath string `toml:"role_attribute_path"`
|
||||||
AllowAssignGrafanaAdmin bool
|
TeamIdsAttributePath string `toml:"team_ids_attribute_path"`
|
||||||
HostedDomain string
|
TeamsUrl string `toml:"teams_url"`
|
||||||
ApiUrl string
|
TlsClientCa string `toml:"tls_client_ca"`
|
||||||
TeamsUrl string
|
TlsClientCert string `toml:"tls_client_cert"`
|
||||||
AllowSignup bool
|
TlsClientKey string `toml:"tls_client_key"`
|
||||||
Name string
|
TokenUrl string `toml:"token_url"`
|
||||||
Icon string
|
AllowedDomains []string `toml:"allowed_domains"`
|
||||||
TlsClientCert string
|
Scopes []string `toml:"scopes"`
|
||||||
TlsClientKey string
|
AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"`
|
||||||
TlsClientCa string
|
AllowSignup bool `toml:"allow_signup"`
|
||||||
TlsSkipVerify bool
|
AutoLogin bool `toml:"auto_login"`
|
||||||
UsePKCE bool
|
Enabled bool `toml:"enabled"`
|
||||||
AutoLogin bool
|
RoleAttributeStrict bool `toml:"role_attribute_strict"`
|
||||||
|
TlsSkipVerify bool `toml:"tls_skip_verify"`
|
||||||
|
UsePKCE bool `toml:"use_pkce"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg,
|
func ProvideService(cfg *setting.Cfg,
|
||||||
features *featuremgmt.FeatureManager,
|
features *featuremgmt.FeatureManager,
|
||||||
usageStats usagestats.Service,
|
usageStats usagestats.Service,
|
||||||
|
bundleRegistry supportbundles.Service,
|
||||||
) *SocialService {
|
) *SocialService {
|
||||||
ss := SocialService{
|
ss := &SocialService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
oAuthProvider: make(map[string]*OAuthInfo),
|
oAuthProvider: make(map[string]*OAuthInfo),
|
||||||
socialMap: make(map[string]SocialConnector),
|
socialMap: make(map[string]SocialConnector),
|
||||||
@@ -234,7 +239,9 @@ func ProvideService(cfg *setting.Cfg,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ss
|
ss.registerSupportBundleCollectors(bundleRegistry)
|
||||||
|
|
||||||
|
return ss
|
||||||
}
|
}
|
||||||
|
|
||||||
type BasicUserInfo struct {
|
type BasicUserInfo struct {
|
||||||
@@ -261,6 +268,7 @@ type SocialConnector interface {
|
|||||||
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||||
Client(ctx context.Context, t *oauth2.Token) *http.Client
|
Client(ctx context.Context, t *oauth2.Token) *http.Client
|
||||||
TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
|
TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
|
||||||
|
SupportBundleContent(*bytes.Buffer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SocialBase struct {
|
type SocialBase struct {
|
||||||
@@ -331,6 +339,27 @@ type groupStruct struct {
|
|||||||
Groups []string `json:"groups"`
|
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) {
|
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string, legacy bool) (org.RoleType, bool) {
|
||||||
if s.roleAttributePath == "" {
|
if s.roleAttributePath == "" {
|
||||||
return s.defaultRole(legacy), false
|
return s.defaultRole(legacy), false
|
||||||
|
|||||||
@@ -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
|
package oauthtoken
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -303,6 +304,10 @@ func (m *MockSocialConnector) TokenSource(ctx context.Context, t *oauth2.Token)
|
|||||||
return args.Get(0).(oauth2.TokenSource)
|
return args.Get(0).(oauth2.TokenSource)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockSocialConnector) SupportBundleContent(bf *bytes.Buffer) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type FakeAuthInfoStore struct {
|
type FakeAuthInfoStore struct {
|
||||||
login.Store
|
login.Store
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
|
|||||||
Reference in New Issue
Block a user