mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Add AuthNSvc reload handling * Working, need to add test * Remove commented out code * Add Reload implementation to connectors * Align and add tests, refactor * Add more tests, linting * Add extra checks + tests to oauth client * Clean up based on reviews * Move config instantiation into newSocialBase * Use specific error
230 lines
6.6 KiB
Go
230 lines
6.6 KiB
Go
package connectors
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/login/social"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/ssosettings"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
type SocialBase struct {
|
|
*oauth2.Config
|
|
info *social.OAuthInfo
|
|
cfg *setting.Cfg
|
|
reloadMutex sync.RWMutex
|
|
log log.Logger
|
|
features featuremgmt.FeatureToggles
|
|
}
|
|
|
|
func newSocialBase(name string,
|
|
info *social.OAuthInfo,
|
|
features featuremgmt.FeatureToggles,
|
|
cfg *setting.Cfg,
|
|
) *SocialBase {
|
|
logger := log.New("oauth." + name)
|
|
|
|
return &SocialBase{
|
|
Config: createOAuthConfig(info, cfg, name),
|
|
info: info,
|
|
log: logger,
|
|
features: features,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
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.info.AllowAssignGrafanaAdmin))
|
|
bf.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.info.AllowSignup))
|
|
bf.WriteString(fmt.Sprintf("allowed_domains = %v\n", s.info.AllowedDomains))
|
|
bf.WriteString(fmt.Sprintf("auto_assign_org_role = %v\n", s.cfg.AutoAssignOrgRole))
|
|
bf.WriteString(fmt.Sprintf("role_attribute_path = %v\n", s.info.RoleAttributePath))
|
|
bf.WriteString(fmt.Sprintf("role_attribute_strict = %v\n", s.info.RoleAttributeStrict))
|
|
bf.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.info.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) GetOAuthInfo() *social.OAuthInfo {
|
|
s.reloadMutex.RLock()
|
|
defer s.reloadMutex.RUnlock()
|
|
|
|
return s.info
|
|
}
|
|
|
|
func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
|
|
if s.info.RoleAttributePath == "" {
|
|
if s.info.RoleAttributeStrict {
|
|
return "", false, errRoleAttributePathNotSet.Errorf("role_attribute_path not set and role_attribute_strict is set")
|
|
}
|
|
return "", false, nil
|
|
}
|
|
|
|
if role, gAdmin := s.searchRole(rawJSON, groups); role.IsValid() {
|
|
return role, gAdmin, nil
|
|
} else if role != "" {
|
|
return "", false, errInvalidRole.Errorf("invalid role: %s", role)
|
|
}
|
|
|
|
if s.info.RoleAttributeStrict {
|
|
return "", false, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute, but role_attribute_strict is set")
|
|
}
|
|
|
|
return "", false, nil
|
|
}
|
|
|
|
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
|
|
role, gAdmin, err := s.extractRoleAndAdminOptional(rawJSON, groups)
|
|
if role == "" {
|
|
role = s.defaultRole()
|
|
}
|
|
|
|
return role, gAdmin, err
|
|
}
|
|
|
|
func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType, bool) {
|
|
role, err := s.searchJSONForStringAttr(s.info.RoleAttributePath, rawJSON)
|
|
if err == nil && role != "" {
|
|
return getRoleFromSearch(role)
|
|
}
|
|
|
|
if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil {
|
|
role, err := s.searchJSONForStringAttr(s.info.RoleAttributePath, groupBytes)
|
|
if err == nil && role != "" {
|
|
return getRoleFromSearch(role)
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
// defaultRole returns the default role for the user based on the autoAssignOrgRole setting
|
|
// if legacy is enabled "" is returned indicating the previous role assignment is used.
|
|
func (s *SocialBase) defaultRole() org.RoleType {
|
|
if s.cfg.AutoAssignOrgRole != "" {
|
|
s.log.Debug("No role found, returning default.")
|
|
return org.RoleType(s.cfg.AutoAssignOrgRole)
|
|
}
|
|
|
|
// should never happen
|
|
return org.RoleViewer
|
|
}
|
|
|
|
func (s *SocialBase) isGroupMember(groups []string) bool {
|
|
if len(s.info.AllowedGroups) == 0 {
|
|
return true
|
|
}
|
|
|
|
for _, allowedGroup := range s.info.AllowedGroups {
|
|
for _, group := range groups {
|
|
if group == allowedGroup {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
|
|
tokenString, ok := idToken.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("id_token is not a string: %v", idToken)
|
|
}
|
|
|
|
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
|
|
matched := jwtRegexp.FindStringSubmatch(tokenString)
|
|
if matched == nil {
|
|
return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString)
|
|
}
|
|
|
|
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error base64 decoding id_token: %w", err)
|
|
}
|
|
|
|
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error base64 decoding header: %w", err)
|
|
}
|
|
|
|
var header map[string]any
|
|
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
|
return nil, fmt.Errorf("error deserializing header: %w", err)
|
|
}
|
|
|
|
if compressionVal, exists := header["zip"]; exists {
|
|
compression, ok := compressionVal.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unrecognized compression header: %v", compressionVal)
|
|
}
|
|
|
|
if compression != "DEF" {
|
|
return nil, fmt.Errorf("unknown compression algorithm: %s", compression)
|
|
}
|
|
|
|
fr, err := zlib.NewReader(bytes.NewReader(rawJSON))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating zlib reader: %w", err)
|
|
}
|
|
defer func() {
|
|
if err := fr.Close(); err != nil {
|
|
s.log.Warn("Failed closing zlib reader", "error", err)
|
|
}
|
|
}()
|
|
|
|
rawJSON, err = io.ReadAll(fr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decompressing payload: %w", err)
|
|
}
|
|
}
|
|
|
|
return rawJSON, nil
|
|
}
|
|
|
|
// match grafana admin role and translate to org role and bool.
|
|
// treat the JSON search result to ensure correct casing.
|
|
func getRoleFromSearch(role string) (org.RoleType, bool) {
|
|
if strings.EqualFold(role, social.RoleGrafanaAdmin) {
|
|
return org.RoleAdmin, true
|
|
}
|
|
|
|
return org.RoleType(cases.Title(language.Und).String(role)), false
|
|
}
|
|
|
|
func validateInfo(info *social.OAuthInfo) error {
|
|
if info.ClientId == "" {
|
|
return ssosettings.ErrEmptyClientId.Errorf("clientId is empty")
|
|
}
|
|
|
|
return nil
|
|
}
|