Auth: Add skip_org_role_sync setting for GrafanaCom (#60553)
* add frontend settings and setting for grafanacom * removed println * add skip-org-role-sync on login * add deprecation notice for this field * remove println * remove newline * change and renamed variables * fix for reconfiguring the settings for grafanacom * add documentationf or grafanacom setup * WIP tests * added tests * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * updated steps * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * doc: updated the docs to reflect what happens to grafana.com users * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Jo <joao.guerreiro@grafana.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Jo <joao.guerreiro@grafana.com> * Update docs/sources/setup-grafana/configure-security/configure-authentication/grafana-com/index.md Co-authored-by: Jo <joao.guerreiro@grafana.com> * add blankline * rephrase of doc improvements for explaing of the settings * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Jo <joao.guerreiro@grafana.com> * add frontend setting for grafanacom. * WIP tests * refactor docs * frontend to adhere to skipping org role sync for GrafanaCom users * update docs to reflect desired behavior * tests: added test for skip and nonskip * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Jo <joao.guerreiro@grafana.com> Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
co-authored by
Christopher Moyer
Jo
Ieva
parent
e7b8b82c14
commit
91322bebb5
@@ -148,6 +148,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"OAuthSkipOrgRoleUpdateSync": hs.Cfg.OAuthSkipOrgRoleUpdateSync,
|
||||
"SAMLSkipOrgRoleSync": hs.Cfg.SectionWithEnvOverrides("auth.saml").Key("skip_org_role_sync").MustBool(false),
|
||||
"LDAPSkipOrgRoleSync": hs.Cfg.LDAPSkipOrgRoleSync,
|
||||
"GrafanaComSkipOrgRoleSync": hs.Cfg.GrafanaComSkipOrgRoleSync,
|
||||
"DisableSyncLock": hs.Cfg.DisableSyncLock,
|
||||
},
|
||||
"buildInfo": map[string]interface{}{
|
||||
|
||||
+82
-11
@@ -14,14 +14,29 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func setupSocialHTTPServerWithConfig(t *testing.T, cfg *setting.Cfg) *HTTPServer {
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
return &HTTPServer{
|
||||
Cfg: cfg,
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
SQLStore: sqlStore,
|
||||
SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()),
|
||||
HooksService: hooks.ProvideService(),
|
||||
SecretsService: fakes.NewFakeSecretsService(),
|
||||
}
|
||||
}
|
||||
|
||||
func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
|
||||
t.Helper()
|
||||
|
||||
@@ -29,17 +44,7 @@ func setupOAuthTest(t *testing.T, cfg *setting.Cfg) *web.Mux {
|
||||
cfg = setting.NewCfg()
|
||||
}
|
||||
cfg.ErrTemplateName = "error-template"
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
SQLStore: sqlStore,
|
||||
SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures()),
|
||||
HooksService: hooks.ProvideService(),
|
||||
SecretsService: fakes.NewFakeSecretsService(),
|
||||
}
|
||||
hs := setupSocialHTTPServerWithConfig(t, cfg)
|
||||
|
||||
m := web.New()
|
||||
m.Use(getContextHandler(t, cfg).Middleware)
|
||||
@@ -159,3 +164,69 @@ func TestOAuthLogin_UsePKCE(t *testing.T) {
|
||||
base64.RawURLEncoding.EncodeToString(shasum[:]),
|
||||
)
|
||||
}
|
||||
|
||||
func TestOAuthLogin_BuildExternalUserInfo(t *testing.T) {
|
||||
t.Helper()
|
||||
cfgOAuthSkipRoleSync := setting.NewCfg()
|
||||
authOAuthSec := cfgOAuthSkipRoleSync.Raw.Section("auth")
|
||||
_, err := authOAuthSec.NewKey("oauth_skip_org_role_update_sync", "true")
|
||||
require.NoError(t, err)
|
||||
cfgOAuthSkipRoleSync.ErrTemplateName = "error-template"
|
||||
|
||||
cfgOAuthOrgRoleSync := setting.NewCfg()
|
||||
authOAutoWithoutSec := cfgOAuthOrgRoleSync.Raw.Section("auth")
|
||||
_, err = authOAutoWithoutSec.NewKey("oauth_skip_org_role_update_sync", "false")
|
||||
require.NoError(t, err)
|
||||
cfgOAuthOrgRoleSync.ErrTemplateName = "error-template"
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
cfg *setting.Cfg
|
||||
basicUser *social.BasicUserInfo
|
||||
expectedOrgRoles map[int64]org.RoleType
|
||||
}{
|
||||
{
|
||||
name: "should return empty map of org role mapping if the role for the basic info is empty",
|
||||
cfg: cfgOAuthOrgRoleSync,
|
||||
basicUser: &social.BasicUserInfo{
|
||||
Id: "1",
|
||||
Name: "first lastname",
|
||||
Email: "example@github.com",
|
||||
Login: "example",
|
||||
Role: "",
|
||||
},
|
||||
expectedOrgRoles: map[int64]org.RoleType{},
|
||||
},
|
||||
{
|
||||
name: "should set internal role if role exists and we are skipping org role sync",
|
||||
cfg: cfgOAuthSkipRoleSync,
|
||||
basicUser: &social.BasicUserInfo{
|
||||
Id: "1",
|
||||
Name: "first lastname",
|
||||
Email: "example@github.com",
|
||||
Login: "example",
|
||||
Role: roletype.RoleAdmin,
|
||||
},
|
||||
expectedOrgRoles: map[int64]org.RoleType{1: roletype.RoleAdmin},
|
||||
},
|
||||
{
|
||||
name: "should return empty external role, if the role for the basic info is empty",
|
||||
cfg: cfgOAuthSkipRoleSync,
|
||||
basicUser: &social.BasicUserInfo{
|
||||
Id: "1",
|
||||
Name: "first lastname",
|
||||
Email: "example@github.com",
|
||||
Login: "example",
|
||||
Role: "",
|
||||
},
|
||||
expectedOrgRoles: map[int64]org.RoleType{},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Logf("%s", tc.name)
|
||||
cfg := tc.cfg
|
||||
hs := setupSocialHTTPServerWithConfig(t, cfg)
|
||||
externalUser := hs.buildExternalUserInfo(nil, tc.basicUser, "")
|
||||
require.Equal(t, tc.expectedOrgRoles, externalUser.OrgRoles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@@ -14,6 +15,7 @@ type SocialGrafanaCom struct {
|
||||
*SocialBase
|
||||
url string
|
||||
allowedOrganizations []string
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
|
||||
type OrgRecord struct {
|
||||
@@ -40,6 +42,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
|
||||
return false
|
||||
}
|
||||
|
||||
// UserInfo is used for login credentials for the user
|
||||
func (s *SocialGrafanaCom) UserInfo(client *http.Client, _ *oauth2.Token) (*BasicUserInfo, error) {
|
||||
var data struct {
|
||||
Id int `json:"id"`
|
||||
@@ -60,12 +63,17 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, _ *oauth2.Token) (*Basi
|
||||
return nil, fmt.Errorf("Error getting user info: %s", err)
|
||||
}
|
||||
|
||||
// on login we do not want to display the role from the external provider
|
||||
var role roletype.RoleType
|
||||
if !s.skipOrgRoleSync {
|
||||
role = org.RoleType(data.Role)
|
||||
}
|
||||
userInfo := &BasicUserInfo{
|
||||
Id: fmt.Sprintf("%d", data.Id),
|
||||
Name: data.Name,
|
||||
Login: data.Login,
|
||||
Email: data.Email,
|
||||
Role: org.RoleType(data.Role),
|
||||
Role: role,
|
||||
}
|
||||
|
||||
if !s.IsOrganizationMember(data.Orgs) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
UserInfoURL = "/api/oauth2/user"
|
||||
userResponse = `{
|
||||
"id": 123,
|
||||
"name": "grafana name",
|
||||
"login": "grafanalogin",
|
||||
"username": "grafanalogin",
|
||||
"email": "grafana@grafana.com",
|
||||
"role": "Admin",
|
||||
"orgs": [ { "login": "grafana", "role": "Admin" } ]
|
||||
}`
|
||||
)
|
||||
|
||||
func TestSocialGrafanaCom_UserInfo(t *testing.T) {
|
||||
provider := SocialGrafanaCom{
|
||||
SocialBase: &SocialBase{
|
||||
log: newLogger("grafana_com_oauth_test", "debug"),
|
||||
},
|
||||
}
|
||||
|
||||
type conf struct {
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
Cfg conf
|
||||
userInfoResp string
|
||||
want *BasicUserInfo
|
||||
ExpectedError error
|
||||
}{
|
||||
{
|
||||
Name: "should return empty role as userInfo when Skip Org Role Sync Enabled",
|
||||
userInfoResp: userResponse,
|
||||
Cfg: conf{skipOrgRoleSync: true},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1",
|
||||
Name: "Eric Leijonmarck",
|
||||
Email: "octocat@github.com",
|
||||
Login: "octocat",
|
||||
Role: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "should return role as userInfo when Skip Org Role Sync Enabled",
|
||||
userInfoResp: userResponse,
|
||||
Cfg: conf{skipOrgRoleSync: false},
|
||||
want: &BasicUserInfo{
|
||||
Id: "1",
|
||||
Name: "Eric Leijonmarck",
|
||||
Email: "octocat@github.com",
|
||||
Login: "octocat",
|
||||
Role: "Admin",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
provider.skipOrgRoleSync = test.Cfg.skipOrgRoleSync
|
||||
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.RequestURI {
|
||||
case UserInfoURL:
|
||||
_, err := w.Write([]byte(test.userInfoResp))
|
||||
require.NoError(t, err)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
provider.url = ts.URL
|
||||
actualResult, err := provider.UserInfo(ts.Client(), nil)
|
||||
if test.ExpectedError != nil {
|
||||
require.Equal(t, err, test.ExpectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.want.Role, actualResult.Role)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,7 @@ func ProvideService(cfg *setting.Cfg, features *featuremgmt.FeatureManager) *Soc
|
||||
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
|
||||
url: cfg.GrafanaComURL,
|
||||
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
|
||||
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +462,10 @@ type Cfg struct {
|
||||
// in case API is not publicly accessible.
|
||||
// Defaults to GrafanaComURL setting + "/api" if unset.
|
||||
GrafanaComAPIURL string
|
||||
// GrafanaComSkipOrgRoleSync can be set for
|
||||
// letting users set org roles from within Grafana and
|
||||
// skip the org roles coming from GrafanaCom
|
||||
GrafanaComSkipOrgRoleSync bool
|
||||
|
||||
// Geomap base layer config
|
||||
GeomapDefaultBaseLayerConfig map[string]interface{}
|
||||
@@ -1351,6 +1355,11 @@ func readSecuritySettings(iniFile *ini.File, cfg *Cfg) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAuthGrafanaComSettings(iniFile *ini.File, cfg *Cfg) {
|
||||
sec := iniFile.Section("auth.grafana_com")
|
||||
cfg.GrafanaComSkipOrgRoleSync = sec.Key("skip_org_role_sync").MustBool(false)
|
||||
}
|
||||
|
||||
func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
auth := iniFile.Section("auth")
|
||||
|
||||
@@ -1453,6 +1462,7 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
|
||||
cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false)
|
||||
|
||||
readAuthGrafanaComSettings(iniFile, cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user