Auth: Validation fixes for SSO Settings (#82252)

* Validation fixes

* Add URL validations + tests

* Add ApiUrl validation

* Refactor validators

* lint

* Clean up

* Improvements
This commit is contained in:
Misi
2024-02-14 16:06:52 +01:00
committed by GitHub
parent 8a7828da48
commit 12b1170631
20 changed files with 696 additions and 43 deletions
+8 -1
View File
@@ -23,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -197,13 +198,19 @@ func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSett
return err
}
return validation.Validate(info, requester,
validateAllowedGroups,
validation.RequiredUrlValidator(info.AuthUrl, "Auth URL"),
validation.RequiredUrlValidator(info.TokenUrl, "Token URL"))
}
func validateAllowedGroups(info *social.OAuthInfo, requester identity.Requester) error {
for _, groupId := range info.AllowedGroups {
_, err := uuid.Parse(groupId)
if err != nil {
return ssosettings.ErrInvalidOAuthConfig("One or more of the Allowed groups are not in the correct format. Allowed groups should be a list of Object Ids.")
}
}
return nil
}
@@ -1003,10 +1003,14 @@ func TestSocialAzureAD_Validate(t *testing.T) {
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allowed_groups": "0bb9c9cc-4945-418f-9b6a-c1d3b81141b0, 6034d328-0e6a-4240-8d03-cb9f2c1f16e4",
"client_id": "client-id",
"allowed_groups": "0bb9c9cc-4945-418f-9b6a-c1d3b81141b0, 6034d328-0e6a-4240-8d03-cb9f2c1f16e4",
"allow_assign_grafana_admin": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@@ -1040,6 +1044,8 @@ func TestSocialAzureAD_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allowed_groups": "abc, def",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
@@ -1051,9 +1057,12 @@ func TestSocialAzureAD_Validate(t *testing.T) {
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -1064,6 +1073,52 @@ func TestSocialAzureAD_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "invalid_url",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "/path",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
+18 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -79,17 +80,33 @@ func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SS
return err
}
err = validation.Validate(info, requester,
validation.UrlValidator(info.AuthUrl, "Auth URL"),
validation.UrlValidator(info.TokenUrl, "Token URL"),
validateTeamsUrlWhenNotEmpty)
if err != nil {
return err
}
if info.Extra[teamIdsKey] != "" && (info.TeamIdsAttributePath == "" || info.TeamsUrl == "") {
return ssosettings.ErrInvalidOAuthConfig("If Team Ids are configured then Team Ids attribute path and Teams URL must be configured.")
}
if info.AllowedGroups != nil && len(info.AllowedGroups) > 0 && info.GroupsAttributePath == "" {
return ssosettings.ErrInvalidOAuthConfig("If Allowed groups are configured then Groups attribute path must be configured.")
return ssosettings.ErrInvalidOAuthConfig("If Allowed groups is configured then Groups attribute path must be configured.")
}
return nil
}
func validateTeamsUrlWhenNotEmpty(info *social.OAuthInfo, requester identity.Requester) error {
if info.TeamsUrl == "" {
return nil
}
return validation.UrlValidator(info.TeamsUrl, "Teams URL")(info, requester)
}
func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
@@ -731,10 +731,25 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "passes when team_url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: nil,
},
{
name: "fails if settings map contains an invalid field",
settings: ssoModels.SSOSettings{
@@ -768,9 +783,12 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -782,6 +800,68 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "invalid_url",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "https://example.com/teams",
"auth_url": "https://example.com/auth",
"token_url": "/path",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if teams url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"teams_url": "file://teams",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
@@ -86,6 +87,14 @@ func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSetti
return err
}
return validation.Validate(info, requester,
validation.MustBeEmptyValidator(info.AuthUrl, "Auth URL"),
validation.MustBeEmptyValidator(info.TokenUrl, "Token URL"),
validation.MustBeEmptyValidator(info.ApiUrl, "API URL"),
teamIdsNumbersValidator)
}
func teamIdsNumbersValidator(info *social.OAuthInfo, requester identity.Requester) error {
teamIdsSplitted := util.SplitString(info.Extra[teamIdsKey])
teamIds := mustInts(teamIdsSplitted)
@@ -359,6 +359,9 @@ func TestSocialGitHub_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"auth_url": "",
"token_url": "",
"api_url": "",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -408,7 +411,8 @@ func TestSocialGitHub_Validate(t *testing.T) {
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -419,7 +423,40 @@ func TestSocialGitHub_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "",
"api_url": "http://example.com/api",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
+5 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
)
@@ -76,7 +77,10 @@ func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSetti
return err
}
return nil
return validation.Validate(info, requester,
validation.MustBeEmptyValidator(info.AuthUrl, "Auth URL"),
validation.MustBeEmptyValidator(info.TokenUrl, "Token URL"),
validation.MustBeEmptyValidator(info.ApiUrl, "API URL"))
}
func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
@@ -477,6 +477,9 @@ func TestSocialGitlab_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"auth_url": "",
"token_url": "",
"api_url": "",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -514,9 +517,12 @@ func TestSocialGitlab_Validate(t *testing.T) {
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -527,7 +533,42 @@ func TestSocialGitlab_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "",
"api_url": "https://example.com/api",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
"api_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "https://example.com/token",
"api_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
+5 -3
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
)
@@ -66,9 +67,10 @@ func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSetti
return err
}
// add specific validation rules for Google
return nil
return validation.Validate(info, requester,
validation.MustBeEmptyValidator(info.AuthUrl, "Auth URL"),
validation.MustBeEmptyValidator(info.TokenUrl, "Token URL"),
validation.MustBeEmptyValidator(info.ApiUrl, "API URL"))
}
func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
@@ -682,6 +682,9 @@ func TestSocialGoogle_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"auth_url": "",
"token_url": "",
"api_url": "",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -719,9 +722,12 @@ func TestSocialGoogle_Validate(t *testing.T) {
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -732,7 +738,55 @@ func TestSocialGoogle_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "",
"api_url": "https://example.com/api",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
"api_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api token url is not empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "https://example.com/token",
"api_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -64,9 +65,10 @@ func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOS
return err
}
// add specific validation rules for GrafanaCom
return nil
return validation.Validate(info, requester,
validation.MustBeEmptyValidator(info.AuthUrl, "Auth URL"),
validation.MustBeEmptyValidator(info.TokenUrl, "Token URL"),
validation.MustBeEmptyValidator(info.TeamsUrl, "Teams URL"))
}
func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
+5 -3
View File
@@ -16,6 +16,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
)
@@ -72,9 +73,10 @@ func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSetting
return err
}
// add specific validation rules for Okta
return nil
return validation.Validate(info, requester,
validation.RequiredUrlValidator(info.AuthUrl, "Auth URL"),
validation.RequiredUrlValidator(info.TokenUrl, "Token URL"),
validation.RequiredUrlValidator(info.ApiUrl, "API URL"))
}
func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
+75 -2
View File
@@ -150,6 +150,9 @@ func TestSocialOkta_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
"api_url": "https://example.com/api",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
@@ -189,7 +192,8 @@ func TestSocialOkta_Validate(t *testing.T) {
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
requester: &user.SignedInUser{IsGrafanaAdmin: true},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
@@ -200,7 +204,76 @@ func TestSocialOkta_Validate(t *testing.T) {
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if auth url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "invalid_url",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if token url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "/path",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api url is empty",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"token_url": "https://example.com/token",
"api_url": "",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if api url is invalid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"auth_url": "https://example.com/auth",
"api_url": "/api",
"token_url": "https://example.com/token",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
+5 -14
View File
@@ -20,7 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -223,17 +223,8 @@ func getRoleFromSearch(role string) (org.RoleType, bool) {
}
func validateInfo(info *social.OAuthInfo, requester identity.Requester) error {
if info.ClientId == "" {
return ssosettings.ErrInvalidOAuthConfig("Client Id is empty.")
}
if info.AllowAssignGrafanaAdmin && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.")
}
if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin and Skip org role sync are both set thus Grafana Admin role will not be synced. Consider setting one or the other.")
}
return nil
return validation.Validate(info, requester,
validation.RequiredValidator(info.ClientId, "Client Id"),
validation.AllowAssignGrafanaAdminValidator,
validation.SkipOrgRoleSyncAllowAssignGrafanaAdminValidator)
}
+2
View File
@@ -66,3 +66,5 @@ type Store interface {
Upsert(ctx context.Context, settings *models.SSOSettings) error
Delete(ctx context.Context, provider string) error
}
type ValidateFunc[T any] func(input *T, requester identity.Requester) error
@@ -0,0 +1,69 @@
package validation
import (
"fmt"
"net/url"
"strings"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ssosettings"
)
func AllowAssignGrafanaAdminValidator(info *social.OAuthInfo, requester identity.Requester) error {
if info.AllowAssignGrafanaAdmin && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.")
}
return nil
}
func SkipOrgRoleSyncAllowAssignGrafanaAdminValidator(info *social.OAuthInfo, requester identity.Requester) error {
if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin and Skip org role sync are both set thus Grafana Admin role will not be synced. Consider setting one or the other.")
}
return nil
}
func RequiredValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if value == "" {
return ssosettings.ErrInvalidOAuthConfig(fmt.Sprintf("%s is required.", name))
}
return nil
}
}
func UrlValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if !isValidUrl(value) {
return ssosettings.ErrInvalidOAuthConfig(fmt.Sprintf("%s is an invalid URL.", name))
}
return nil
}
}
func RequiredUrlValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if err := RequiredValidator(value, name)(info, requester); err != nil {
return err
}
return UrlValidator(value, name)(info, requester)
}
}
func MustBeEmptyValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if value != "" {
return ssosettings.ErrInvalidOAuthConfig(fmt.Sprintf("%s must be empty.", name))
}
return nil
}
}
func isValidUrl(actual string) bool {
parsed, err := url.ParseRequestURI(actual)
if err != nil {
return false
}
return strings.HasPrefix(parsed.Scheme, "http") && parsed.Host != ""
}
@@ -0,0 +1,154 @@
package validation
import (
"testing"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
)
type testCase struct {
name string
input *social.OAuthInfo
requester identity.Requester
wantErr error
}
func TestUrlValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when url is valid",
input: &social.OAuthInfo{
AuthUrl: "https://example.com/auth",
},
wantErr: nil,
},
{
name: "fails when url is invalid",
input: &social.OAuthInfo{
AuthUrl: "file://etc",
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Auth URL is an invalid URL."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := UrlValidator(tt.input.AuthUrl, "Auth URL")(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestRequiredValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when client id is not empty",
input: &social.OAuthInfo{
ClientId: "client-id",
},
wantErr: nil,
},
{
name: "fails when client id is empty",
input: &social.OAuthInfo{
ClientId: "",
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Client Id is required."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := RequiredValidator(tt.input.ClientId, "Client Id")(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestAllowAssignGrafanaAdminValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when user is grafana admin and allow assign grafana admin is true",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
requester: &user.SignedInUser{
IsGrafanaAdmin: true,
},
wantErr: nil,
},
{
name: "fails when user is not grafana admin and allow assign grafana admin is true",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := AllowAssignGrafanaAdminValidator(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestSkipOrgRoleSyncAllowAssignGrafanaAdminValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when allow assign grafana admin is set, but skip org role sync is not set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
SkipOrgRoleSync: false,
},
wantErr: nil,
},
{
name: "passes when allow assign grafana admin is not set, but skip org role sync is set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: false,
SkipOrgRoleSync: true,
},
wantErr: nil,
},
{
name: "fails when both allow assign grafana admin and skip org role sync is set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
SkipOrgRoleSync: true,
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin and Skip org role sync are both set thus Grafana Admin role will not be synced. Consider setting one or the other."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := SkipOrgRoleSyncAllowAssignGrafanaAdminValidator(tt.input, nil)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
@@ -0,0 +1,16 @@
package validation
import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ssosettings"
)
func Validate(info *social.OAuthInfo, requester identity.Requester, validators ...ssosettings.ValidateFunc[social.OAuthInfo]) error {
for _, validatorFunc := range validators {
if err := validatorFunc(info, requester); err != nil {
return err
}
}
return nil
}
+31 -5
View File
@@ -6,6 +6,7 @@ import { contextSrv } from 'app/core/core';
import { FieldData, SSOProvider, SSOSettingsField } from './types';
import { isSelectableValue } from './utils/guards';
import { isUrlValid } from './utils/url';
/** Map providers to their settings */
export const fields: Record<SSOProvider['provider'], Array<keyof SSOProvider['settings']>> = {
@@ -139,7 +140,11 @@ export function fieldMap(provider: string): Record<string, FieldData> {
type: 'text',
description: 'The authorization endpoint of your OAuth2 provider.',
validation: {
required: false,
required: true,
validate: (value) => {
return isUrlValid(value);
},
message: 'This field is required and must be a valid URL.',
},
},
authStyle: {
@@ -159,7 +164,11 @@ export function fieldMap(provider: string): Record<string, FieldData> {
type: 'text',
description: 'The token endpoint of your OAuth2 provider.',
validation: {
required: false,
required: true,
validate: (value) => {
return isUrlValid(value);
},
message: 'This field is required and must be a valid URL.',
},
},
scopes: {
@@ -214,6 +223,18 @@ export function fieldMap(provider: string): Record<string, FieldData> {
),
validation: {
required: false,
validate: (value) => {
if (typeof value !== 'string') {
return false;
}
if (value.length) {
return isUrlValid(value);
}
return true;
},
message: 'This field must be a valid URL if set.',
},
},
roleAttributePath: {
@@ -360,12 +381,17 @@ export function fieldMap(provider: string): Record<string, FieldData> {
type: 'text',
validation: {
validate: (value, formValues) => {
let result = true;
if (formValues.teamIds.length) {
return !!value;
result = !!value;
}
return true;
if (typeof value === 'string' && value.length) {
result = isUrlValid(value);
}
return result;
},
message: 'This field must be set if Team Ids are configured.',
message: 'This field must be set if Team Ids are configured and must be a valid URL.',
},
},
teamIdsAttributePath: {
@@ -4,3 +4,15 @@ import { AuthProviderInfo } from '../types';
export function getProviderUrl(provider: AuthProviderInfo) {
return BASE_PATH + (provider.configPath || provider.id);
}
export const isUrlValid = (url: unknown): boolean => {
if (typeof url !== 'string') {
return false;
}
try {
const parsedUrl = new URL(url);
return parsedUrl.protocol.includes('http');
} catch (_) {
return false;
}
};