mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Refactor AzureAD to init itself * Use mapstructure to convert data to OAuthInfo * Update * Align tests * Remove unused functions * Add owner to mapstructure * Clean up, lint * Refactor Okta init, Align tests * Address review comments, fix name in newSocialBase * Update newSocialBase first param * Refactor GitLab init, align tests * Update pkg/login/social/common.go Co-authored-by: Karl Persson <kalle.persson@grafana.com> * Use ini conversion to map * Leftovers * Refactor GitHub connector initialization, align tests * Refactor Google connector init, align tests * Refactor grafana_com connector, align tests * Refactor generic_oauth connector init, align tests * cleanup * Remove util.go * Add tests for custom field init * Change OAuthInfo's Extra type * Fix * Replace interface{} with any * clean up --------- Co-authored-by: Karl Persson <kalle.persson@grafana.com>
259 lines
6.9 KiB
Go
259 lines
6.9 KiB
Go
package social
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
const (
|
|
legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo"
|
|
googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups"
|
|
googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
|
|
googleProviderName = "google"
|
|
)
|
|
|
|
type SocialGoogle struct {
|
|
*SocialBase
|
|
hostedDomain string
|
|
apiUrl string
|
|
skipOrgRoleSync bool
|
|
}
|
|
|
|
type googleUserData struct {
|
|
ID string `json:"sub"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
EmailVerified bool `json:"email_verified"`
|
|
rawJSON []byte `json:"-"`
|
|
}
|
|
|
|
func NewGoogleProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGoogle, error) {
|
|
info, err := createOAuthInfoFromKeyValues(settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config := createOAuthConfig(info, cfg, googleProviderName)
|
|
provider := &SocialGoogle{
|
|
SocialBase: newSocialBase(googleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
|
hostedDomain: info.HostedDomain,
|
|
apiUrl: info.ApiUrl,
|
|
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,
|
|
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
|
// skipOrgRoleSync: info.SkipOrgRoleSync
|
|
}
|
|
|
|
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
|
|
provider.log.Warn("Using legacy Google API URL, please update your configuration")
|
|
}
|
|
|
|
return provider, nil
|
|
}
|
|
|
|
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
|
data, errToken := s.extractFromToken(ctx, client, token)
|
|
if errToken != nil {
|
|
return nil, errToken
|
|
}
|
|
|
|
if data == nil {
|
|
var errAPI error
|
|
data, errAPI = s.extractFromAPI(ctx, client)
|
|
if errAPI != nil {
|
|
return nil, errAPI
|
|
}
|
|
}
|
|
|
|
if data.ID == "" {
|
|
return nil, fmt.Errorf("error getting user info: id is empty")
|
|
}
|
|
|
|
if !data.EmailVerified {
|
|
return nil, fmt.Errorf("user email is not verified")
|
|
}
|
|
|
|
groups, errPage := s.retrieveGroups(ctx, client, data)
|
|
if errPage != nil {
|
|
s.log.Warn("Error retrieving groups", "error", errPage)
|
|
}
|
|
|
|
if !s.isGroupMember(groups) {
|
|
return nil, errMissingGroupMembership
|
|
}
|
|
|
|
userInfo := &BasicUserInfo{
|
|
Id: data.ID,
|
|
Name: data.Name,
|
|
Email: data.Email,
|
|
Login: data.Email,
|
|
Role: "",
|
|
IsGrafanaAdmin: nil,
|
|
Groups: groups,
|
|
}
|
|
|
|
if !s.skipOrgRoleSync {
|
|
role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups)
|
|
if errRole != nil {
|
|
return nil, errRole
|
|
}
|
|
|
|
if s.allowAssignGrafanaAdmin {
|
|
userInfo.IsGrafanaAdmin = &grafanaAdmin
|
|
}
|
|
|
|
userInfo.Role = role
|
|
}
|
|
|
|
s.log.Debug("Resolved user info", "data", fmt.Sprintf("%+v", userInfo))
|
|
|
|
return userInfo, nil
|
|
}
|
|
|
|
func (s *SocialGoogle) GetOAuthInfo() *OAuthInfo {
|
|
return s.info
|
|
}
|
|
|
|
type googleAPIData struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
EmailVerified bool `json:"verified_email"`
|
|
}
|
|
|
|
func (s *SocialGoogle) extractFromAPI(ctx context.Context, client *http.Client) (*googleUserData, error) {
|
|
if strings.HasPrefix(s.apiUrl, legacyAPIURL) {
|
|
data := googleAPIData{}
|
|
response, err := s.httpGet(ctx, client, s.apiUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error retrieving legacy user info: %s", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(response.Body, &data); err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling legacy user info: %s", err)
|
|
}
|
|
|
|
return &googleUserData{
|
|
ID: data.ID,
|
|
Name: data.Name,
|
|
Email: data.Email,
|
|
EmailVerified: data.EmailVerified,
|
|
rawJSON: response.Body,
|
|
}, nil
|
|
}
|
|
|
|
data := googleUserData{}
|
|
response, err := s.httpGet(ctx, client, s.apiUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting user info: %s", err)
|
|
}
|
|
|
|
if err := json.Unmarshal(response.Body, &data); err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling user info: %s", err)
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (s *SocialGoogle) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
|
|
if s.features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) && s.useRefreshToken {
|
|
opts = append(opts, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
|
|
}
|
|
return s.SocialBase.AuthCodeURL(state, opts...)
|
|
}
|
|
|
|
func (s *SocialGoogle) extractFromToken(ctx context.Context, client *http.Client, token *oauth2.Token) (*googleUserData, error) {
|
|
s.log.Debug("Extracting user info from OAuth token")
|
|
|
|
idToken := token.Extra("id_token")
|
|
if idToken == nil {
|
|
s.log.Debug("No id_token found, defaulting to API access", "token", token)
|
|
return nil, nil
|
|
}
|
|
|
|
rawJSON, err := s.retrieveRawIDToken(idToken)
|
|
if err != nil {
|
|
s.log.Warn("Error retrieving id_token", "error", err, "token", fmt.Sprintf("%+v", idToken))
|
|
return nil, nil
|
|
}
|
|
|
|
if setting.Env == setting.Dev {
|
|
s.log.Debug("Received id_token", "raw_json", string(rawJSON))
|
|
}
|
|
|
|
var data googleUserData
|
|
if err := json.Unmarshal(rawJSON, &data); err != nil {
|
|
return nil, fmt.Errorf("Error getting user info: %s", err)
|
|
}
|
|
|
|
data.rawJSON = rawJSON
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
type googleGroupResp struct {
|
|
Memberships []struct {
|
|
Group string `json:"group"`
|
|
GroupKey struct {
|
|
ID string `json:"id"`
|
|
} `json:"groupKey"`
|
|
DisplayName string `json:"displayName"`
|
|
} `json:"memberships"`
|
|
NextPageToken string `json:"nextPageToken"`
|
|
}
|
|
|
|
func (s *SocialGoogle) retrieveGroups(ctx context.Context, client *http.Client, userData *googleUserData) ([]string, error) {
|
|
s.log.Debug("Retrieving groups", "scopes", s.SocialBase.Config.Scopes)
|
|
if !slices.Contains(s.Scopes, googleIAMScope) {
|
|
return nil, nil
|
|
}
|
|
|
|
groups := []string{}
|
|
|
|
url := fmt.Sprintf("%s?query=member_key_id=='%s'", googleIAMGroupsEndpoint, userData.Email)
|
|
nextPageToken := ""
|
|
for page, errPage := s.getGroupsPage(ctx, client, url, nextPageToken); ; page, errPage = s.getGroupsPage(ctx, client, url, nextPageToken) {
|
|
if errPage != nil {
|
|
return nil, errPage
|
|
}
|
|
|
|
for _, group := range page.Memberships {
|
|
groups = append(groups, group.GroupKey.ID)
|
|
}
|
|
|
|
nextPageToken = page.NextPageToken
|
|
if nextPageToken == "" {
|
|
break
|
|
}
|
|
}
|
|
|
|
return groups, nil
|
|
}
|
|
|
|
func (s *SocialGoogle) getGroupsPage(ctx context.Context, client *http.Client, url, nextPageToken string) (*googleGroupResp, error) {
|
|
if nextPageToken != "" {
|
|
url = fmt.Sprintf("%s&pageToken=%s", url, nextPageToken)
|
|
}
|
|
|
|
s.log.Debug("Retrieving groups", "url", url)
|
|
resp, err := s.httpGet(ctx, client, url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting groups: %s", err)
|
|
}
|
|
|
|
var data googleGroupResp
|
|
if err := json.Unmarshal(resp.Body, &data); err != nil {
|
|
return nil, fmt.Errorf("error unmarshalling groups: %s", err)
|
|
}
|
|
|
|
return &data, nil
|
|
}
|