Auth: Make domain_hint configurable for the Azure AD/Entra ID connector (#108061)

* Make domain_hint configurable for Entra ID/Azure AD

* Add docs

* Fix + i18n gen

* Add validation to domain hint

* Remove unnecessary change
This commit is contained in:
Misi
2025-07-15 12:53:19 +02:00
committed by GitHub
parent 4954822cbb
commit 92d098fdfd
9 changed files with 185 additions and 11 deletions
@@ -3,6 +3,7 @@ package validation
import (
"fmt"
"net/url"
"regexp"
"slices"
"strings"
@@ -11,6 +12,8 @@ import (
"github.com/grafana/grafana/pkg/services/ssosettings"
)
var domainRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,6}$`)
func AllowAssignGrafanaAdminValidator(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
hasChanged := info.AllowAssignGrafanaAdmin != oldInfo.AllowAssignGrafanaAdmin
@@ -66,6 +69,18 @@ func UrlValidator(value string, name string) ssosettings.ValidateFunc[social.OAu
}
}
func DomainValidator(value string, name string) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
if value == "" {
return nil
}
if !domainRegexp.MatchString(value) {
return ssosettings.ErrInvalidOAuthConfig(fmt.Sprintf("%s contains an invalid domain.", 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 {
@@ -49,6 +49,113 @@ func TestUrlValidator(t *testing.T) {
}
}
func TestDomainValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when domain is valid",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.com"},
},
wantErr: nil,
},
{
name: "passes when domain is empty",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": ""},
},
wantErr: nil,
},
{
name: "passes when domain has subdomain",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "sub.example.com"},
},
wantErr: nil,
},
{
name: "fails when domain is invalid",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "invalid-domain"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
{
name: "fails when domain has invalid characters",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.com!"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
{
name: "fails when TLD is too short (1 character)",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.x"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
{
name: "passes when TLD is minimum length (2 characters)",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.co"},
},
wantErr: nil,
},
{
name: "passes when TLD is maximum length (6 characters)",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.museum"},
},
wantErr: nil,
},
{
name: "fails when TLD is too long (7+ characters)",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "example.toolong"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
{
name: "passes when domain is maximum reasonable length",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "very-long-subdomain-name-that-is-still-valid.example-organization.museum"},
},
wantErr: nil,
},
{
name: "fails when domain segment is too long (over 63 characters)",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "this-is-a-very-long-subdomain-name-that-exceeds-the-maximum-allowed-length-for-a-dns-label.example.com"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
{
name: "passes when domain segment is exactly 63 characters",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "this-is-exactly-sixty-three-characters-long-which-is-the-max.com"},
},
wantErr: nil,
},
{
name: "fails when domain is just a single character",
input: &social.OAuthInfo{
Extra: map[string]string{"domain_hint": "a"},
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Domain Hint contains an invalid domain."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := DomainValidator(tt.input.Extra["domain_hint"], "Domain Hint")(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{
{