mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Refactor OAuth connectors' initialization (#77919)
* 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>
This commit is contained in:
2
go.mod
2
go.mod
@@ -361,7 +361,7 @@ require (
|
||||
github.com/mattn/go-ieproxy v0.0.3 // indirect
|
||||
github.com/mattn/goveralls v0.0.6 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 //@grafana/grafana-authnz-team
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
|
||||
@@ -16,9 +16,16 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const azureADProviderName = "azuread"
|
||||
|
||||
var _ SocialConnector = (*SocialAzureAD)(nil)
|
||||
|
||||
type SocialAzureAD struct {
|
||||
*SocialBase
|
||||
cache remotecache.CacheStorage
|
||||
@@ -57,6 +64,30 @@ type keySetJWKS struct {
|
||||
jose.JSONWebKeySet
|
||||
}
|
||||
|
||||
func NewAzureADProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (*SocialAzureAD, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := createOAuthConfig(info, cfg, azureADProviderName)
|
||||
provider := &SocialAzureAD{
|
||||
SocialBase: newSocialBase(azureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
cache: cache,
|
||||
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
|
||||
forceUseGraphAPI: mustBool(info.Extra["force_use_graph_api"], false),
|
||||
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
}
|
||||
|
||||
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
appendUniqueScope(config, OfflineAccessScope)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
|
||||
idToken := token.Extra("id_token")
|
||||
if idToken == nil {
|
||||
@@ -122,6 +153,10 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SocialAzureAD) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) {
|
||||
claims, err := s.validateIDTokenSignature(ctx, client, parsedToken)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func trueBoolPtr() *bool {
|
||||
@@ -32,10 +33,8 @@ func falseBoolPtr() *bool {
|
||||
|
||||
func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
type fields struct {
|
||||
SocialBase *SocialBase
|
||||
allowedGroups []string
|
||||
allowedOrganizations []string
|
||||
forceUseGraphAPI bool
|
||||
providerCfg map[string]any
|
||||
cfg *setting.Cfg
|
||||
usGovURL bool
|
||||
}
|
||||
type args struct {
|
||||
@@ -61,7 +60,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -75,7 +80,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "No email",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "",
|
||||
@@ -90,6 +101,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "No id token",
|
||||
claims: nil,
|
||||
fields: fields{
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
@@ -103,7 +123,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
usGovURL: true,
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
@@ -125,7 +151,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -139,7 +171,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Admin role",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -160,7 +198,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Lowercase Admin role",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -181,7 +225,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Only other roles",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -199,6 +249,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
Groups: []string{},
|
||||
},
|
||||
},
|
||||
// TODO: @mgyongyosi check this test
|
||||
{
|
||||
name: "role from env variable",
|
||||
claims: &azureClaims{
|
||||
@@ -209,7 +260,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -230,7 +287,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
ID: "1234",
|
||||
},
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1234",
|
||||
@@ -244,7 +307,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Admin and Editor roles in claim",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -264,7 +333,17 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Grafana Admin but setting is disabled",
|
||||
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures())},
|
||||
fields: fields{
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": false,
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
PreferredUsername: "",
|
||||
@@ -285,8 +364,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Editor roles in claim and GrafanaAdminAssignment enabled",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": true,
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -307,8 +392,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Grafana Admin and Editor roles in claim",
|
||||
fields: fields{SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())},
|
||||
fields: fields{
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": true,
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
PreferredUsername: "",
|
||||
@@ -329,8 +422,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Error if user is not a member of allowed_groups",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
allowedGroups: []string{"dead-beef"},
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": false,
|
||||
"allowed_groups": "dead-beef",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -346,8 +446,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Error if user is not a member of allowed_organizations",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures()),
|
||||
allowedOrganizations: []string{"uuid-1234"},
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": false,
|
||||
"allowed_organizations": "uuid-1234",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Editor",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -360,10 +467,18 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: "No error if user is a member of allowed_organizations",
|
||||
fields: fields{
|
||||
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allowed_organizations": "uuid-1234,uuid-5678",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -387,9 +502,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "No Error if user is a member of allowed_groups",
|
||||
fields: fields{
|
||||
allowedGroups: []string{"foo", "bar"},
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": "false",
|
||||
"allowed_groups": "foo, bar",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "Viewer",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -411,7 +532,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch groups when ClaimsNames and ClaimsSources is set",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
ID: "1",
|
||||
@@ -436,8 +563,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch groups when forceUseGraphAPI is set",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()),
|
||||
forceUseGraphAPI: true,
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"force_use_graph_api": "true",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
ID: "1",
|
||||
@@ -463,7 +596,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch empty role when strict attribute role is true and no match",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"role_attribute_strict": "true",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -479,7 +619,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
{
|
||||
name: "Fetch empty role when strict attribute role is true and no role claims returned",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"role_attribute_strict": "true",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -528,20 +675,8 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialAzureAD{
|
||||
SocialBase: tt.fields.SocialBase,
|
||||
allowedOrganizations: tt.fields.allowedOrganizations,
|
||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
if tt.fields.SocialBase == nil {
|
||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
|
||||
}
|
||||
|
||||
if tt.fields.allowedGroups != nil {
|
||||
s.allowedGroups = tt.fields.allowedGroups
|
||||
}
|
||||
s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
|
||||
require.NoError(t, err)
|
||||
|
||||
if tt.fields.usGovURL {
|
||||
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
|
||||
@@ -592,9 +727,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
token = token.WithExtra(map[string]any{"id_token": raw})
|
||||
}
|
||||
|
||||
if tt.fields.SocialBase != nil {
|
||||
tt.args.client = s.Client(context.Background(), token)
|
||||
}
|
||||
|
||||
got, err := s.UserInfo(context.Background(), tt.args.client, token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
@@ -610,19 +743,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
|
||||
func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
type fields struct {
|
||||
SocialBase *SocialBase
|
||||
allowedGroups []string
|
||||
forceUseGraphAPI bool
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
type args struct {
|
||||
client *http.Client
|
||||
providerCfg map[string]any
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
claims *azureClaims
|
||||
args args
|
||||
settingAutoAssignOrgRole string
|
||||
want *BasicUserInfo
|
||||
wantErr bool
|
||||
@@ -630,10 +758,16 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
{
|
||||
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{ClientID: "client-id-example"},
|
||||
&OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
skipOrgRoleSync: false,
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": "true",
|
||||
"skip_org_role_sync": "false",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
OAuthSkipOrgRoleUpdateSync: false,
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -655,10 +789,16 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
{
|
||||
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles",
|
||||
fields: fields{
|
||||
SocialBase: newSocialBase("azuread",
|
||||
&oauth2.Config{ClientID: "client-id-example"},
|
||||
&OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()),
|
||||
skipOrgRoleSync: false,
|
||||
providerCfg: map[string]any{
|
||||
"name": "azuread",
|
||||
"client_id": "client-id-example",
|
||||
"allow_assign_grafana_admin": "true",
|
||||
"skip_org_role_sync": "false",
|
||||
},
|
||||
cfg: &setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
OAuthSkipOrgRoleUpdateSync: false,
|
||||
},
|
||||
},
|
||||
claims: &azureClaims{
|
||||
Email: "me@example.com",
|
||||
@@ -711,18 +851,8 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialAzureAD{
|
||||
SocialBase: tt.fields.SocialBase,
|
||||
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
|
||||
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
if tt.fields.SocialBase == nil {
|
||||
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{
|
||||
AllowedGroups: tt.fields.allowedGroups,
|
||||
}, "", false, *featuremgmt.WithFeatures())
|
||||
}
|
||||
s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
|
||||
require.NoError(t, err)
|
||||
|
||||
s.SocialBase.Endpoint.AuthURL = authURL
|
||||
|
||||
@@ -769,11 +899,9 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
token = token.WithExtra(map[string]any{"id_token": raw})
|
||||
}
|
||||
|
||||
if tt.fields.SocialBase != nil {
|
||||
tt.args.client = s.Client(context.Background(), token)
|
||||
}
|
||||
provClient := s.Client(context.Background(), token)
|
||||
|
||||
got, err := s.UserInfo(context.Background(), tt.args.client, token)
|
||||
got, err := s.UserInfo(context.Background(), provClient, token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
@@ -783,3 +911,46 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
|
||||
type settingFields struct {
|
||||
forceUseGraphAPI bool
|
||||
allowedOrganizations []string
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
settings map[string]any
|
||||
want settingFields
|
||||
}{
|
||||
{
|
||||
name: "forceUseGraphAPI is set to true",
|
||||
settings: map[string]any{
|
||||
"force_use_graph_api": "true",
|
||||
},
|
||||
want: settingFields{
|
||||
forceUseGraphAPI: true,
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowedOrganizations is set",
|
||||
settings: map[string]any{
|
||||
"allowed_organizations": "uuid-1234,uuid-5678",
|
||||
},
|
||||
want: settingFields{
|
||||
forceUseGraphAPI: false,
|
||||
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := NewAzureADProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.want.forceUseGraphAPI, s.forceUseGraphAPI)
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,16 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/jmespath/go-jmespath"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"golang.org/x/oauth2"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -132,3 +139,101 @@ func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []b
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func createOAuthConfig(info *OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config {
|
||||
var authStyle oauth2.AuthStyle
|
||||
switch strings.ToLower(info.AuthStyle) {
|
||||
case "inparams":
|
||||
authStyle = oauth2.AuthStyleInParams
|
||||
case "inheader":
|
||||
authStyle = oauth2.AuthStyleInHeader
|
||||
default:
|
||||
authStyle = oauth2.AuthStyleAutoDetect
|
||||
}
|
||||
|
||||
config := oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: info.AuthUrl,
|
||||
TokenURL: info.TokenUrl,
|
||||
AuthStyle: authStyle,
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + defaultName,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
func mustBool(value any, defaultValue bool) bool {
|
||||
if value == nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
str, ok := value.(string)
|
||||
if ok {
|
||||
result, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
result, ok := value.(bool)
|
||||
if !ok {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// convertIniSectionToMap converts key value pairs from an ini section to a map[string]any
|
||||
func convertIniSectionToMap(sec *ini.Section) map[string]any {
|
||||
mappedSettings := make(map[string]any)
|
||||
for k, v := range sec.KeysHash() {
|
||||
mappedSettings[k] = v
|
||||
}
|
||||
return mappedSettings
|
||||
}
|
||||
|
||||
// createOAuthInfoFromKeyValues creates an OAuthInfo struct from a map[string]any using mapstructure
|
||||
// it puts all extra key values into OAuthInfo's Extra map
|
||||
func createOAuthInfoFromKeyValues(settingsKV map[string]any) (*OAuthInfo, error) {
|
||||
emptyStrToSliceDecodeHook := func(from reflect.Type, to reflect.Type, data any) (any, error) {
|
||||
if from.Kind() == reflect.String && to.Kind() == reflect.Slice {
|
||||
strData, ok := data.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to convert %v to string", data)
|
||||
}
|
||||
|
||||
if strData == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
return util.SplitString(strData), nil
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
var oauthInfo OAuthInfo
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: emptyStrToSliceDecodeHook,
|
||||
Result: &oauthInfo,
|
||||
WeaklyTypedInput: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = decoder.Decode(settingsKV)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if oauthInfo.EmptyScopes {
|
||||
oauthInfo.Scopes = []string{}
|
||||
}
|
||||
|
||||
return &oauthInfo, err
|
||||
}
|
||||
|
||||
101
pkg/login/social/commont_test.go
Normal file
101
pkg/login/social/commont_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestMapping_IniSectionOAuthInfo(t *testing.T) {
|
||||
iniContent := `
|
||||
[test]
|
||||
name = OAuth
|
||||
icon = signin
|
||||
enabled = true
|
||||
allow_sign_up = false
|
||||
auto_login = true
|
||||
client_id = test_client_id
|
||||
client_secret = test_client_secret
|
||||
scopes = ["openid", "profile", "email"]
|
||||
empty_scopes = false
|
||||
email_attribute_name = email:primary
|
||||
email_attribute_path = email
|
||||
login_attribute_path = login
|
||||
name_attribute_path = name
|
||||
role_attribute_path = role
|
||||
role_attribute_strict = true
|
||||
groups_attribute_path = groups
|
||||
id_token_attribute_name = id_token
|
||||
team_ids_attribute_path = team_ids
|
||||
auth_url = test_auth_url
|
||||
token_url = test_token_url
|
||||
api_url = test_api_url
|
||||
teams_url = test_teams_url
|
||||
allowed_domains = domain1.com
|
||||
allowed_groups =
|
||||
team_ids = first, second
|
||||
allowed_organizations = org1, org2
|
||||
tls_skip_verify_insecure = true
|
||||
tls_client_cert =
|
||||
tls_client_key =
|
||||
tls_client_ca =
|
||||
use_pkce = false
|
||||
auth_style =
|
||||
allow_assign_grafana_admin = true
|
||||
skip_org_role_sync = true
|
||||
use_refresh_token = true
|
||||
empty_scopes =
|
||||
hosted_domain = test_hosted_domain
|
||||
`
|
||||
|
||||
iniFile, err := ini.Load([]byte(iniContent))
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedOAuthInfo := &OAuthInfo{
|
||||
Name: "OAuth",
|
||||
Icon: "signin",
|
||||
Enabled: true,
|
||||
AllowSignup: false,
|
||||
AutoLogin: true,
|
||||
ClientId: "test_client_id",
|
||||
ClientSecret: "test_client_secret",
|
||||
Scopes: []string{"openid", "profile", "email"},
|
||||
EmptyScopes: false,
|
||||
EmailAttributeName: "email:primary",
|
||||
EmailAttributePath: "email",
|
||||
RoleAttributePath: "role",
|
||||
RoleAttributeStrict: true,
|
||||
GroupsAttributePath: "groups",
|
||||
TeamIdsAttributePath: "team_ids",
|
||||
AuthUrl: "test_auth_url",
|
||||
TokenUrl: "test_token_url",
|
||||
ApiUrl: "test_api_url",
|
||||
TeamsUrl: "test_teams_url",
|
||||
AllowedDomains: []string{"domain1.com"},
|
||||
AllowedGroups: []string{},
|
||||
TlsSkipVerify: true,
|
||||
TlsClientCert: "",
|
||||
TlsClientKey: "",
|
||||
TlsClientCa: "",
|
||||
UsePKCE: false,
|
||||
AuthStyle: "",
|
||||
AllowAssignGrafanaAdmin: true,
|
||||
UseRefreshToken: true,
|
||||
HostedDomain: "test_hosted_domain",
|
||||
Extra: map[string]string{
|
||||
"allowed_organizations": "org1, org2",
|
||||
"id_token_attribute_name": "id_token",
|
||||
"login_attribute_path": "login",
|
||||
"name_attribute_path": "name",
|
||||
"skip_org_role_sync": "true",
|
||||
"team_ids": "first, second",
|
||||
},
|
||||
}
|
||||
|
||||
settingsKVs := convertIniSectionToMap(iniFile.Section("test"))
|
||||
oauthInfo, err := createOAuthInfoFromKeyValues(settingsKVs)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedOAuthInfo, oauthInfo)
|
||||
}
|
||||
@@ -11,8 +11,14 @@ import (
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const genericOAuthProviderName = "generic_oauth"
|
||||
|
||||
type SocialGenericOAuth struct {
|
||||
*SocialBase
|
||||
allowedOrganizations []string
|
||||
@@ -30,6 +36,36 @@ type SocialGenericOAuth struct {
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
|
||||
func NewGenericOAuthProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGenericOAuth, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := createOAuthConfig(info, cfg, genericOAuthProviderName)
|
||||
provider := &SocialGenericOAuth{
|
||||
SocialBase: newSocialBase(genericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamsUrl: info.TeamsUrl,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
emailAttributePath: info.EmailAttributePath,
|
||||
nameAttributePath: info.Extra["name_attribute_path"],
|
||||
groupsAttributePath: info.GroupsAttributePath,
|
||||
loginAttributePath: info.Extra["login_attribute_path"],
|
||||
idTokenAttributeName: info.Extra["id_token_attribute_name"],
|
||||
teamIdsAttributePath: info.TeamIdsAttributePath,
|
||||
teamIds: util.SplitString(info.Extra["team_ids"]),
|
||||
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
|
||||
allowedGroups: info.AllowedGroups,
|
||||
skipOrgRoleSync: cfg.GenericOAuthSkipOrgRoleSync,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// TODOD: remove this in the next PR and use the isGroupMember from social.go
|
||||
func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool {
|
||||
if len(s.allowedGroups) == 0 {
|
||||
return true
|
||||
@@ -205,6 +241,10 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson {
|
||||
s.log.Debug("Extracting user info from OAuth token")
|
||||
|
||||
|
||||
@@ -8,28 +8,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
)
|
||||
|
||||
func newLogger(name string, lev string) log.Logger {
|
||||
logger := log.New(name)
|
||||
logger.Swap(level.NewFilter(logger.GetLogger(), level.AllowInfo()))
|
||||
return logger
|
||||
}
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestSearchJSONForEmail(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -113,11 +105,8 @@ func TestSearchJSONForEmail(t *testing.T) {
|
||||
|
||||
func TestSearchJSONForGroups(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -176,11 +165,8 @@ func TestSearchJSONForGroups(t *testing.T) {
|
||||
|
||||
func TestSearchJSONForRole(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -238,12 +224,10 @@ func TestSearchJSONForRole(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
emailAttributePath: "email",
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{
|
||||
"email_attribute_path": "email",
|
||||
}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -508,12 +492,10 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
|
||||
|
||||
func TestUserInfoSearchesForLogin(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
loginAttributePath: "login",
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{
|
||||
"login_attribute_path": "login",
|
||||
}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -603,12 +585,10 @@ func TestUserInfoSearchesForLogin(t *testing.T) {
|
||||
|
||||
func TestUserInfoSearchesForName(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
nameAttributePath: "name",
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{
|
||||
"name_attribute_path": "name",
|
||||
}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -701,12 +681,6 @@ func TestUserInfoSearchesForName(t *testing.T) {
|
||||
|
||||
func TestUserInfoSearchesForGroup(t *testing.T) {
|
||||
t.Run("Given a generic OAuth provider", func(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
groupsAttributePath string
|
||||
@@ -742,7 +716,6 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
provider.groupsAttributePath = test.groupsAttributePath
|
||||
body, err := json.Marshal(test.responseBody)
|
||||
require.NoError(t, err)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -752,7 +725,13 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
|
||||
_, err := w.Write(body)
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
provider.apiUrl = ts.URL
|
||||
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{
|
||||
"groups_attribute_path": test.groupsAttributePath,
|
||||
"api_url": ts.URL,
|
||||
}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "",
|
||||
TokenType: "",
|
||||
@@ -769,12 +748,10 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPayloadCompression(t *testing.T) {
|
||||
provider := SocialGenericOAuth{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("generic_oauth_test", "debug"),
|
||||
},
|
||||
emailAttributePath: "email",
|
||||
}
|
||||
provider, err := NewGenericOAuthProvider(map[string]any{
|
||||
"email_attribute_path": "email",
|
||||
}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
@@ -836,3 +813,97 @@ func TestPayloadCompression(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
|
||||
type settingFields struct {
|
||||
nameAttributePath string
|
||||
loginAttributePath string
|
||||
idTokenAttributeName string
|
||||
teamIds []string
|
||||
allowedOrganizations []string
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
settings map[string]any
|
||||
want settingFields
|
||||
}{
|
||||
{
|
||||
name: "nameAttributePath is set",
|
||||
settings: map[string]any{
|
||||
"name_attribute_path": "name",
|
||||
},
|
||||
want: settingFields{
|
||||
nameAttributePath: "name",
|
||||
loginAttributePath: "",
|
||||
idTokenAttributeName: "",
|
||||
teamIds: []string{},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "loginAttributePath is set",
|
||||
settings: map[string]any{
|
||||
"login_attribute_path": "login",
|
||||
},
|
||||
want: settingFields{
|
||||
nameAttributePath: "",
|
||||
loginAttributePath: "login",
|
||||
idTokenAttributeName: "",
|
||||
teamIds: []string{},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "idTokenAttributeName is set",
|
||||
settings: map[string]any{
|
||||
"id_token_attribute_name": "id_token",
|
||||
},
|
||||
want: settingFields{
|
||||
nameAttributePath: "",
|
||||
loginAttributePath: "",
|
||||
idTokenAttributeName: "id_token",
|
||||
teamIds: []string{},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "teamIds is set",
|
||||
settings: map[string]any{
|
||||
"team_ids": "[\"team1\", \"team2\"]",
|
||||
},
|
||||
want: settingFields{
|
||||
nameAttributePath: "",
|
||||
loginAttributePath: "",
|
||||
idTokenAttributeName: "",
|
||||
teamIds: []string{"team1", "team2"},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowedOrganizations is set",
|
||||
settings: map[string]any{
|
||||
"allowed_organizations": "org1, org2",
|
||||
},
|
||||
want: settingFields{
|
||||
nameAttributePath: "",
|
||||
loginAttributePath: "",
|
||||
idTokenAttributeName: "",
|
||||
teamIds: []string{},
|
||||
allowedOrganizations: []string{"org1", "org2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
|
||||
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)
|
||||
require.Equal(t, tc.want.idTokenAttributeName, s.idTokenAttributeName)
|
||||
require.Equal(t, tc.want.teamIds, s.teamIds)
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,20 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
const gitHubProviderName = "github"
|
||||
|
||||
type SocialGithub struct {
|
||||
*SocialBase
|
||||
allowedOrganizations []string
|
||||
@@ -43,6 +49,31 @@ var (
|
||||
"User is not a member of one of the required organizations. Please contact identity provider administrator."))
|
||||
)
|
||||
|
||||
func NewGitHubProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGithub, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamIds, err := mustInts(util.SplitString(info.Extra["team_ids"]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := createOAuthConfig(info, cfg, gitHubProviderName)
|
||||
provider := &SocialGithub{
|
||||
SocialBase: newSocialBase(gitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamIds: teamIds,
|
||||
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
|
||||
skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool {
|
||||
if len(s.teamIds) == 0 {
|
||||
return true
|
||||
@@ -276,6 +307,10 @@ func (t *GithubTeam) GetShorthand() (string, error) {
|
||||
return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil
|
||||
}
|
||||
|
||||
func (s *SocialGithub) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
func convertToGroupList(t []GithubTeam) []string {
|
||||
groups := make([]string, 0)
|
||||
for _, team := range t {
|
||||
@@ -291,3 +326,15 @@ func convertToGroupList(t []GithubTeam) []string {
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func mustInts(s []string) ([]int, error) {
|
||||
result := make([]int, 0, len(s))
|
||||
for _, v := range s {
|
||||
num, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, num)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const testGHUserTeamsJSON = `[
|
||||
@@ -238,14 +239,16 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
s := &SocialGithub{
|
||||
SocialBase: newSocialBase("github", &oauth2.Config{},
|
||||
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()),
|
||||
allowedOrganizations: []string{},
|
||||
apiUrl: server.URL + "/user",
|
||||
teamIds: []int{},
|
||||
skipOrgRoleSync: tt.settingSkipOrgRoleSync,
|
||||
}
|
||||
s, err := NewGitHubProvider(map[string]any{
|
||||
"allowed_organizations": "",
|
||||
"api_url": server.URL + "/user",
|
||||
"team_ids": "",
|
||||
"role_attribute_path": tt.roleAttributePath,
|
||||
}, &setting.Cfg{
|
||||
AutoAssignOrgRole: tt.autoAssignOrgRole,
|
||||
GitHubSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
|
||||
}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "fake_token",
|
||||
@@ -262,3 +265,57 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
|
||||
type settingFields struct {
|
||||
teamIds []int
|
||||
allowedOrganizations []string
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
settings map[string]any
|
||||
want settingFields
|
||||
}{
|
||||
{
|
||||
name: "teamIds is set",
|
||||
settings: map[string]any{
|
||||
"team_ids": "1234,5678",
|
||||
},
|
||||
want: settingFields{
|
||||
teamIds: []int{1234, 5678},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowedOrganizations is set",
|
||||
settings: map[string]any{
|
||||
"allowed_organizations": "uuid-1234,uuid-5678",
|
||||
},
|
||||
want: settingFields{
|
||||
teamIds: []int{},
|
||||
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "teamIds and allowedOrganizations are empty",
|
||||
settings: map[string]any{
|
||||
"team_ids": "",
|
||||
"allowed_organizations": "",
|
||||
},
|
||||
want: settingFields{
|
||||
teamIds: []int{},
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := NewGitHubProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.want.teamIds, s.teamIds)
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
groupPerPage = 50
|
||||
accessLevelGuest = "10"
|
||||
gitlabProviderName = "gitlab"
|
||||
)
|
||||
|
||||
type SocialGitlab struct {
|
||||
@@ -47,6 +49,24 @@ type userData struct {
|
||||
IsGrafanaAdmin *bool `json:"-"`
|
||||
}
|
||||
|
||||
func NewGitLabProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGitlab, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := createOAuthConfig(info, cfg, gitlabProviderName)
|
||||
provider := &SocialGitlab{
|
||||
SocialBase: newSocialBase(gitlabProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
|
||||
groups := make([]string, 0)
|
||||
nextPage := new(int)
|
||||
@@ -162,6 +182,10 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
|
||||
apiResp := &apiData{}
|
||||
response, err := s.httpGet(ctx, client, s.apiUrl+"/user")
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,12 +38,9 @@ const (
|
||||
|
||||
func TestSocialGitlab_UserInfo(t *testing.T) {
|
||||
var nilPointer *bool
|
||||
provider := SocialGitlab{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("gitlab_oauth_test", "debug"),
|
||||
},
|
||||
skipOrgRoleSync: false,
|
||||
}
|
||||
|
||||
provider, err := NewGitLabProvider(map[string]any{"skip_org_role_sync": false}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
type conf struct {
|
||||
AllowAssignGrafanaAdmin bool
|
||||
@@ -217,7 +216,7 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
|
||||
switch r.URL.Path {
|
||||
case "/oauth/token":
|
||||
// Return a dummy access token
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "dummy_access_token",
|
||||
"token_type": "Bearer",
|
||||
})
|
||||
@@ -347,26 +346,27 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
|
||||
// Create a test client with a dummy token
|
||||
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})
|
||||
|
||||
// Create a test SocialGitlab instance
|
||||
s := &SocialGitlab{
|
||||
SocialBase: &SocialBase{
|
||||
Config: tc.config,
|
||||
log: newLogger("test", "debug"),
|
||||
allowSignup: false,
|
||||
allowedDomains: []string{},
|
||||
roleAttributePath: "",
|
||||
roleAttributeStrict: false,
|
||||
autoAssignOrgRole: "",
|
||||
skipOrgRoleSync: false,
|
||||
s, err := NewGitLabProvider(map[string]any{
|
||||
"allowed_domains": []string{},
|
||||
"allow_sign_up": false,
|
||||
"role_attribute_path": "",
|
||||
"role_attribute_strict": false,
|
||||
"skip_org_role_sync": false,
|
||||
"auth_url": tc.config.Endpoint.AuthURL,
|
||||
"token_url": tc.config.Endpoint.TokenURL,
|
||||
},
|
||||
skipOrgRoleSync: false,
|
||||
}
|
||||
&setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
OAuthSkipOrgRoleUpdateSync: false,
|
||||
}, featuremgmt.WithFeatures())
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test case: successful extraction
|
||||
token := &oauth2.Token{}
|
||||
// build jwt
|
||||
// header
|
||||
header := map[string]interface{}{
|
||||
header := map[string]any{
|
||||
"alg": "RS256",
|
||||
"typ": "JWT",
|
||||
"kid": "dummy",
|
||||
@@ -381,7 +381,7 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
|
||||
// build token
|
||||
idToken := fmt.Sprintf("%s.%s.%s", headerEncoded, payloadEncoded, signatureEncoded)
|
||||
|
||||
token = token.WithExtra(map[string]interface{}{"id_token": idToken})
|
||||
token = token.WithExtra(map[string]any{"id_token": idToken})
|
||||
data, err := s.extractFromToken(context.Background(), client, token)
|
||||
if tc.wantErrMessage != "" {
|
||||
require.Error(t, err)
|
||||
@@ -450,12 +450,8 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) {
|
||||
defer mockServer.Close()
|
||||
|
||||
// Create a SocialGitlab instance with the mock server URL
|
||||
s := &SocialGitlab{
|
||||
apiUrl: mockServer.URL,
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("test", "debug"),
|
||||
},
|
||||
}
|
||||
s, err := NewGitLabProvider(map[string]any{"api_url": mockServer.URL}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call getGroups and verify that it returns all groups
|
||||
expectedGroups := []string{"admins", "editors", "viewers", "serveradmins"}
|
||||
|
||||
@@ -14,9 +14,12 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo"
|
||||
const googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups"
|
||||
const googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
|
||||
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
|
||||
@@ -33,6 +36,29 @@ type googleUserData struct {
|
||||
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 {
|
||||
@@ -92,6 +118,10 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *SocialGoogle) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
type googleAPIData struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
||||
@@ -180,21 +181,23 @@ func TestSocialGoogle_retrieveGroups(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialGoogle{
|
||||
SocialBase: &SocialBase{
|
||||
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
||||
log: log.NewNopLogger(),
|
||||
allowSignup: false,
|
||||
allowAssignGrafanaAdmin: false,
|
||||
allowedDomains: []string{},
|
||||
roleAttributePath: "",
|
||||
roleAttributeStrict: false,
|
||||
autoAssignOrgRole: "",
|
||||
skipOrgRoleSync: false,
|
||||
s, err := NewGoogleProvider(map[string]any{
|
||||
"api_url": "",
|
||||
"scopes": tt.fields.Scopes,
|
||||
"hosted_domain": "",
|
||||
"allowed_domains": []string{},
|
||||
"allow_sign_up": false,
|
||||
"role_attribute_path": "",
|
||||
"role_attribute_strict": false,
|
||||
"allow_assign_grafana_admin": false,
|
||||
},
|
||||
hostedDomain: "",
|
||||
apiUrl: "",
|
||||
}
|
||||
&setting.Cfg{
|
||||
AutoAssignOrgRole: "",
|
||||
GoogleSkipOrgRoleSync: false,
|
||||
},
|
||||
featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := s.retrieveGroups(context.Background(), tt.args.client, tt.args.userData)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("SocialGoogle.retrieveGroups() error = %v, wantErr %v", err, tt.wantErr)
|
||||
@@ -632,19 +635,20 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &SocialGoogle{
|
||||
apiUrl: tt.fields.apiURL,
|
||||
SocialBase: &SocialBase{
|
||||
Config: &oauth2.Config{Scopes: tt.fields.Scopes},
|
||||
log: log.NewNopLogger(),
|
||||
allowSignup: false,
|
||||
allowedGroups: tt.fields.allowedGroups,
|
||||
roleAttributePath: tt.fields.roleAttributePath,
|
||||
roleAttributeStrict: tt.fields.roleAttributeStrict,
|
||||
allowAssignGrafanaAdmin: tt.fields.allowAssignGrafanaAdmin,
|
||||
s, err := NewGoogleProvider(map[string]any{
|
||||
"api_url": tt.fields.apiURL,
|
||||
"scopes": tt.fields.Scopes,
|
||||
"allowed_groups": tt.fields.allowedGroups,
|
||||
"allow_sign_up": false,
|
||||
"role_attribute_path": tt.fields.roleAttributePath,
|
||||
"role_attribute_strict": tt.fields.roleAttributeStrict,
|
||||
"allow_assign_grafana_admin": tt.fields.allowAssignGrafanaAdmin,
|
||||
},
|
||||
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
}
|
||||
&setting.Cfg{
|
||||
GoogleSkipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
},
|
||||
featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token)
|
||||
if tt.wantErr {
|
||||
|
||||
@@ -9,9 +9,14 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const grafanaComProviderName = "grafana_com"
|
||||
|
||||
type SocialGrafanaCom struct {
|
||||
*SocialBase
|
||||
url string
|
||||
@@ -23,6 +28,30 @@ type OrgRecord struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
func NewGrafanaComProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGrafanaCom, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Override necessary settings
|
||||
info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize"
|
||||
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
|
||||
info.AuthStyle = "inheader"
|
||||
|
||||
config := createOAuthConfig(info, cfg, grafanaComProviderName)
|
||||
provider := &SocialGrafanaCom{
|
||||
SocialBase: newSocialBase(grafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
url: cfg.GrafanaComURL,
|
||||
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
|
||||
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
|
||||
return true
|
||||
}
|
||||
@@ -86,3 +115,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (s *SocialGrafanaCom) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -23,11 +25,8 @@ const (
|
||||
)
|
||||
|
||||
func TestSocialGrafanaCom_UserInfo(t *testing.T) {
|
||||
provider := SocialGrafanaCom{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("grafana_com_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
provider, err := NewGrafanaComProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
type conf struct {
|
||||
skipOrgRoleSync bool
|
||||
@@ -93,3 +92,40 @@ func TestSocialGrafanaCom_UserInfo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSocialGrafanaCom_InitializeExtraFields(t *testing.T) {
|
||||
type settingFields struct {
|
||||
allowedOrganizations []string
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
settings map[string]any
|
||||
want settingFields
|
||||
}{
|
||||
{
|
||||
name: "allowedOrganizations is not set",
|
||||
settings: map[string]any{},
|
||||
want: settingFields{
|
||||
allowedOrganizations: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowedOrganizations is set",
|
||||
settings: map[string]any{
|
||||
"allowed_organizations": "uuid-1234,uuid-5678",
|
||||
},
|
||||
want: settingFields{
|
||||
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s, err := NewGrafanaComProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const oktaProviderName = "okta"
|
||||
|
||||
type SocialOkta struct {
|
||||
*SocialBase
|
||||
apiUrl string
|
||||
@@ -39,6 +43,29 @@ type OktaClaims struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewOktaProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialOkta, error) {
|
||||
info, err := createOAuthInfoFromKeyValues(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := createOAuthConfig(info, cfg, oktaProviderName)
|
||||
provider := &SocialOkta{
|
||||
SocialBase: newSocialBase(oktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
allowedGroups: info.AllowedGroups,
|
||||
// FIXME: Move skipOrgRoleSync to OAuthInfo
|
||||
// skipOrgRoleSync: info.SkipOrgRoleSync
|
||||
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
|
||||
}
|
||||
|
||||
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
appendUniqueScope(config, OfflineAccessScope)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (claims *OktaClaims) extractEmail() string {
|
||||
if claims.Email == "" && claims.PreferredUsername != "" {
|
||||
return claims.PreferredUsername
|
||||
@@ -107,6 +134,10 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SocialOkta) GetOAuthInfo() *OAuthInfo {
|
||||
return s.info
|
||||
}
|
||||
|
||||
func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error {
|
||||
rawUserInfoResponse, err := s.httpGet(ctx, client, s.apiUrl)
|
||||
if err != nil {
|
||||
@@ -134,6 +165,7 @@ func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
// TODO: remove this in a separate PR and use the isGroupMember from the social.go
|
||||
func (s *SocialOkta) IsGroupMember(groups []string) bool {
|
||||
if len(s.allowedGroups) == 0 {
|
||||
return true
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestSocialOkta_UserInfo(t *testing.T) {
|
||||
@@ -95,14 +96,22 @@ func TestSocialOkta_UserInfo(t *testing.T) {
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
provider := &SocialOkta{
|
||||
SocialBase: newSocialBase("okta", &oauth2.Config{},
|
||||
&OAuthInfo{RoleAttributePath: tt.RoleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()),
|
||||
apiUrl: server.URL + "/user",
|
||||
skipOrgRoleSync: tt.settingSkipOrgRoleSync,
|
||||
}
|
||||
provider.allowAssignGrafanaAdmin = tt.allowAssignGrafanaAdmin
|
||||
provider.roleAttributePath = tt.RoleAttributePath
|
||||
|
||||
provider, err := NewOktaProvider(
|
||||
map[string]any{
|
||||
"api_url": server.URL + "/user",
|
||||
"role_attribute_path": tt.RoleAttributePath,
|
||||
"allow_assign_grafana_admin": tt.allowAssignGrafanaAdmin,
|
||||
"skip_org_role_sync": tt.settingSkipOrgRoleSync,
|
||||
},
|
||||
&setting.Cfg{
|
||||
OktaSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
|
||||
AutoAssignOrgRole: tt.autoAssignOrgRole,
|
||||
OAuthSkipOrgRoleUpdateSync: false,
|
||||
},
|
||||
featuremgmt.WithFeatures())
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a oauth2 token with a id_token
|
||||
staticToken := oauth2.Token{
|
||||
AccessToken: "",
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,34 +44,37 @@ type SocialService struct {
|
||||
}
|
||||
|
||||
type OAuthInfo struct {
|
||||
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"`
|
||||
AllowedGroups []string `toml:"allowed_groups"`
|
||||
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"`
|
||||
UseRefreshToken bool `toml:"use_refresh_token"`
|
||||
ApiUrl string `mapstructure:"api_url"`
|
||||
AuthUrl string `mapstructure:"auth_url"`
|
||||
AuthStyle string `mapstructure:"auth_style"`
|
||||
ClientId string `mapstructure:"client_id"`
|
||||
ClientSecret string `mapstructure:"client_secret"`
|
||||
EmailAttributeName string `mapstructure:"email_attribute_name"`
|
||||
EmailAttributePath string `mapstructure:"email_attribute_path"`
|
||||
EmptyScopes bool `mapstructure:"empty_scopes"`
|
||||
GroupsAttributePath string `mapstructure:"groups_attribute_path"`
|
||||
HostedDomain string `mapstructure:"hosted_domain"`
|
||||
Icon string `mapstructure:"icon"`
|
||||
Name string `mapstructure:"name"`
|
||||
RoleAttributePath string `mapstructure:"role_attribute_path"`
|
||||
TeamIdsAttributePath string `mapstructure:"team_ids_attribute_path"`
|
||||
TeamsUrl string `mapstructure:"teams_url"`
|
||||
TlsClientCa string `mapstructure:"tls_client_ca"`
|
||||
TlsClientCert string `mapstructure:"tls_client_cert"`
|
||||
TlsClientKey string `mapstructure:"tls_client_key"`
|
||||
TokenUrl string `mapstructure:"token_url"`
|
||||
AllowedDomains []string `mapstructure:"allowed_domains"`
|
||||
AllowedGroups []string `mapstructure:"allowed_groups"`
|
||||
Scopes []string `mapstructure:"scopes"`
|
||||
AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin"`
|
||||
AllowSignup bool `mapstructure:"allow_sign_up"`
|
||||
AutoLogin bool `mapstructure:"auto_login"`
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
RoleAttributeStrict bool `mapstructure:"role_attribute_strict"`
|
||||
TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure"`
|
||||
UsePKCE bool `mapstructure:"use_pkce"`
|
||||
UseRefreshToken bool `mapstructure:"use_refresh_token"`
|
||||
Extra map[string]string `mapstructure:",remain"`
|
||||
}
|
||||
|
||||
func ProvideService(cfg *setting.Cfg,
|
||||
@@ -93,40 +95,11 @@ func ProvideService(cfg *setting.Cfg,
|
||||
for _, name := range allOauthes {
|
||||
sec := cfg.Raw.Section("auth." + name)
|
||||
|
||||
info := &OAuthInfo{
|
||||
ClientId: sec.Key("client_id").String(),
|
||||
ClientSecret: sec.Key("client_secret").String(),
|
||||
Scopes: util.SplitString(sec.Key("scopes").String()),
|
||||
AuthUrl: sec.Key("auth_url").String(),
|
||||
TokenUrl: sec.Key("token_url").String(),
|
||||
ApiUrl: sec.Key("api_url").String(),
|
||||
TeamsUrl: sec.Key("teams_url").String(),
|
||||
Enabled: sec.Key("enabled").MustBool(),
|
||||
EmailAttributeName: sec.Key("email_attribute_name").String(),
|
||||
EmailAttributePath: sec.Key("email_attribute_path").String(),
|
||||
RoleAttributePath: sec.Key("role_attribute_path").String(),
|
||||
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
|
||||
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
|
||||
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
|
||||
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
|
||||
HostedDomain: sec.Key("hosted_domain").String(),
|
||||
AllowSignup: sec.Key("allow_sign_up").MustBool(),
|
||||
Name: sec.Key("name").MustString(name),
|
||||
Icon: sec.Key("icon").String(),
|
||||
TlsClientCert: sec.Key("tls_client_cert").String(),
|
||||
TlsClientKey: sec.Key("tls_client_key").String(),
|
||||
TlsClientCa: sec.Key("tls_client_ca").String(),
|
||||
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
|
||||
UsePKCE: sec.Key("use_pkce").MustBool(),
|
||||
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
|
||||
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
|
||||
AutoLogin: sec.Key("auto_login").MustBool(false),
|
||||
AllowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
}
|
||||
|
||||
// when empty_scopes parameter exists and is true, overwrite scope with empty value
|
||||
if sec.Key("empty_scopes").MustBool() {
|
||||
info.Scopes = []string{}
|
||||
settingsKVs := convertIniSectionToMap(sec)
|
||||
info, err := createOAuthInfoFromKeyValues(settingsKVs)
|
||||
if err != nil {
|
||||
ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name)
|
||||
continue
|
||||
}
|
||||
|
||||
if !info.Enabled {
|
||||
@@ -137,133 +110,13 @@ func ProvideService(cfg *setting.Cfg,
|
||||
name = grafanaCom
|
||||
}
|
||||
|
||||
ss.oAuthProvider[name] = info
|
||||
|
||||
var authStyle oauth2.AuthStyle
|
||||
switch strings.ToLower(sec.Key("auth_style").String()) {
|
||||
case "inparams":
|
||||
authStyle = oauth2.AuthStyleInParams
|
||||
case "inheader":
|
||||
authStyle = oauth2.AuthStyleInHeader
|
||||
case "autodetect", "":
|
||||
authStyle = oauth2.AuthStyleAutoDetect
|
||||
default:
|
||||
ss.log.Warn("Invalid auth style specified, defaulting to auth style AutoDetect", "auth_style", sec.Key("auth_style").String())
|
||||
authStyle = oauth2.AuthStyleAutoDetect
|
||||
conn, err := ss.createOAuthConnector(name, settingsKVs, cfg, features, cache)
|
||||
if err != nil {
|
||||
ss.log.Error("Failed to create OAuth provider", "error", err, "provider", name)
|
||||
}
|
||||
|
||||
config := oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: info.AuthUrl,
|
||||
TokenURL: info.TokenUrl,
|
||||
AuthStyle: authStyle,
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
// GitHub.
|
||||
if name == "github" {
|
||||
ss.socialMap["github"] = &SocialGithub{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamIds: sec.Key("team_ids").Ints(","),
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
|
||||
// GitLab.
|
||||
if name == "gitlab" {
|
||||
ss.socialMap["gitlab"] = &SocialGitlab{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
|
||||
// Google.
|
||||
if name == "google" {
|
||||
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
|
||||
ss.log.Warn("Using legacy Google API URL, please update your configuration")
|
||||
}
|
||||
ss.socialMap["google"] = &SocialGoogle{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
hostedDomain: info.HostedDomain,
|
||||
apiUrl: info.ApiUrl,
|
||||
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
|
||||
// AzureAD.
|
||||
if name == "azuread" {
|
||||
ss.socialMap["azuread"] = &SocialAzureAD{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
cache: cache,
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
|
||||
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
|
||||
}
|
||||
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
appendUniqueScope(&config, OfflineAccessScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Okta
|
||||
if name == "okta" {
|
||||
ss.socialMap["okta"] = &SocialOkta{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
|
||||
}
|
||||
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
|
||||
appendUniqueScope(&config, OfflineAccessScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic - Uses the same scheme as GitHub.
|
||||
if name == "generic_oauth" {
|
||||
ss.socialMap["generic_oauth"] = &SocialGenericOAuth{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
apiUrl: info.ApiUrl,
|
||||
teamsUrl: info.TeamsUrl,
|
||||
emailAttributeName: info.EmailAttributeName,
|
||||
emailAttributePath: info.EmailAttributePath,
|
||||
nameAttributePath: sec.Key("name_attribute_path").String(),
|
||||
groupsAttributePath: info.GroupsAttributePath,
|
||||
loginAttributePath: sec.Key("login_attribute_path").String(),
|
||||
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
|
||||
teamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
|
||||
teamIds: sec.Key("team_ids").Strings(","),
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
|
||||
skipOrgRoleSync: cfg.GenericOAuthSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
|
||||
if name == grafanaCom {
|
||||
config = oauth2.Config{
|
||||
ClientID: info.ClientId,
|
||||
ClientSecret: info.ClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: cfg.GrafanaComURL + "/oauth2/authorize",
|
||||
TokenURL: cfg.GrafanaComURL + "/api/oauth2/token",
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
},
|
||||
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + name,
|
||||
Scopes: info.Scopes,
|
||||
}
|
||||
|
||||
ss.socialMap[grafanaCom] = &SocialGrafanaCom{
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
url: cfg.GrafanaComURL,
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
ss.socialMap[name] = conn
|
||||
ss.oAuthProvider[name] = ss.socialMap[name].GetOAuthInfo()
|
||||
}
|
||||
|
||||
ss.registerSupportBundleCollectors(bundleRegistry)
|
||||
@@ -292,6 +145,8 @@ type SocialConnector interface {
|
||||
IsEmailAllowed(email string) bool
|
||||
IsSignupAllowed() bool
|
||||
|
||||
GetOAuthInfo() *OAuthInfo
|
||||
|
||||
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
|
||||
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
|
||||
Client(ctx context.Context, t *oauth2.Token) *http.Client
|
||||
@@ -301,6 +156,7 @@ type SocialConnector interface {
|
||||
|
||||
type SocialBase struct {
|
||||
*oauth2.Config
|
||||
info *OAuthInfo
|
||||
log log.Logger
|
||||
allowSignup bool
|
||||
allowAssignGrafanaAdmin bool
|
||||
@@ -353,6 +209,7 @@ func newSocialBase(name string,
|
||||
|
||||
return &SocialBase{
|
||||
Config: config,
|
||||
info: info,
|
||||
log: logger,
|
||||
allowSignup: info.AllowSignup,
|
||||
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
|
||||
@@ -553,8 +410,8 @@ func (ss *SocialService) GetOAuthInfoProviders() map[string]*OAuthInfo {
|
||||
return ss.oAuthProvider
|
||||
}
|
||||
|
||||
func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
m := map[string]interface{}{}
|
||||
func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, error) {
|
||||
m := map[string]any{}
|
||||
|
||||
authTypes := map[string]bool{}
|
||||
for provider, enabled := range ss.GetOAuthProviders() {
|
||||
@@ -589,7 +446,7 @@ func (s *SocialBase) isGroupMember(groups []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
||||
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)
|
||||
@@ -611,7 +468,7 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
||||
return nil, fmt.Errorf("error base64 decoding header: %w", err)
|
||||
}
|
||||
|
||||
var header map[string]interface{}
|
||||
var header map[string]any
|
||||
if err := json.Unmarshal(headerBytes, &header); err != nil {
|
||||
return nil, fmt.Errorf("error deserializing header: %w", err)
|
||||
}
|
||||
@@ -645,6 +502,27 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
|
||||
return rawJSON, nil
|
||||
}
|
||||
|
||||
func (ss *SocialService) createOAuthConnector(name string, settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (SocialConnector, error) {
|
||||
switch name {
|
||||
case azureADProviderName:
|
||||
return NewAzureADProvider(settings, cfg, features, cache)
|
||||
case genericOAuthProviderName:
|
||||
return NewGenericOAuthProvider(settings, cfg, features)
|
||||
case gitHubProviderName:
|
||||
return NewGitHubProvider(settings, cfg, features)
|
||||
case gitlabProviderName:
|
||||
return NewGitLabProvider(settings, cfg, features)
|
||||
case googleProviderName:
|
||||
return NewGoogleProvider(settings, cfg, features)
|
||||
case grafanaComProviderName:
|
||||
return NewGrafanaComProvider(settings, cfg, features)
|
||||
case oktaProviderName:
|
||||
return NewOktaProvider(settings, cfg, features)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown oauth provider: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func appendUniqueScope(config *oauth2.Config, scope string) {
|
||||
if !slices.Contains(config.Scopes, OfflineAccessScope) {
|
||||
config.Scopes = append(config.Scopes, OfflineAccessScope)
|
||||
|
||||
@@ -90,6 +90,22 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetOAuthInfo provides a mock function with given fields:
|
||||
func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *social.OAuthInfo
|
||||
if rf, ok := ret.Get(0).(func() *social.OAuthInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*social.OAuthInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IsEmailAllowed provides a mock function with given fields: email
|
||||
func (_m *MockSocialConnector) IsEmailAllowed(email string) bool {
|
||||
ret := _m.Called(email)
|
||||
|
||||
@@ -29,10 +29,11 @@ func (s *OAuthStrategy) IsMatch(provider string) bool {
|
||||
return s.supportedProvidersRegex.MatchString(provider)
|
||||
}
|
||||
|
||||
func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]interface{}, error) {
|
||||
func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]any, error) {
|
||||
section := s.cfg.SectionWithEnvOverrides("auth." + s.provider)
|
||||
|
||||
result := map[string]interface{}{
|
||||
// TODO: load the provider specific keys separately
|
||||
result := map[string]any{
|
||||
"client_id": section.Key("client_id").Value(),
|
||||
"client_secret": section.Key("client_secret").Value(),
|
||||
"scopes": section.Key("scopes").Value(),
|
||||
@@ -52,6 +53,8 @@ func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]int
|
||||
"allow_sign_up": section.Key("allow_sign_up").MustBool(true),
|
||||
"name": section.Key("name").MustString("default name"), // TODO: change this default value
|
||||
"icon": section.Key("icon").Value(),
|
||||
// TODO: @mgyongyosi move skipOrgRoleSync here in a separate PR
|
||||
// "skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false),
|
||||
"tls_client_cert": section.Key("tls_client_cert").Value(),
|
||||
"tls_client_key": section.Key("tls_client_key").Value(),
|
||||
"tls_client_ca": section.Key("tls_client_ca").Value(),
|
||||
|
||||
Reference in New Issue
Block a user