mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SSO: Add LDAP fallback strategy for SSO settings service (#88905)
* add root and client certificate value fields for LDAP * update error messages for connection error * add LDAP fallback strategy for SSO settings service * fix params for sso service provider * fix params for sso service provider * sort imports * sort imports * replace json.Number with int64 in config map * remove type assertions
This commit is contained in:
parent
d4b0ac5973
commit
3d40caf819
@ -24,6 +24,7 @@ const (
|
||||
GrafanaNetProviderName = "grafananet"
|
||||
OktaProviderName = "okta"
|
||||
SAMLProviderName = "saml"
|
||||
LDAPProviderName = "ldap"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/login/social/connectors"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
ldap "github.com/grafana/grafana/pkg/services/ldap/service"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
secretsfake "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl"
|
||||
@ -81,6 +82,7 @@ func TestSocialService_ProvideService(t *testing.T) {
|
||||
nil,
|
||||
&setting.OSSImpl{Cfg: cfg},
|
||||
&licensing.OSSLicensingService{},
|
||||
ldap.ProvideService(cfg),
|
||||
)
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -182,7 +184,19 @@ func TestSocialService_ProvideService_GrafanaComGrafanaNet(t *testing.T) {
|
||||
accessControl := acimpl.ProvideAccessControl(featuremgmt.WithFeatures())
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
||||
ssoSettingsSvc := ssosettingsimpl.ProvideService(cfg, sqlStore, accessControl, routing.NewRouteRegister(), featuremgmt.WithFeatures(), secrets, &usagestats.UsageStatsMock{}, nil, nil, &licensing.OSSLicensingService{})
|
||||
ssoSettingsSvc := ssosettingsimpl.ProvideService(
|
||||
cfg,
|
||||
sqlStore,
|
||||
accessControl,
|
||||
routing.NewRouteRegister(),
|
||||
featuremgmt.WithFeatures(),
|
||||
secrets,
|
||||
&usagestats.UsageStatsMock{},
|
||||
nil,
|
||||
nil,
|
||||
&licensing.OSSLicensingService{},
|
||||
ldap.ProvideService(cfg),
|
||||
)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
@ -17,7 +17,7 @@ const defaultTimeout = 10
|
||||
|
||||
// Config holds list of connections to LDAP
|
||||
type Config struct {
|
||||
Servers []*ServerConfig `toml:"servers"`
|
||||
Servers []*ServerConfig `toml:"servers" json:"servers"`
|
||||
}
|
||||
|
||||
// ServerConfig holds connection data to LDAP
|
||||
@ -25,54 +25,54 @@ type ServerConfig struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
|
||||
UseSSL bool `toml:"use_ssl"`
|
||||
StartTLS bool `toml:"start_tls"`
|
||||
SkipVerifySSL bool `toml:"ssl_skip_verify"`
|
||||
MinTLSVersion string `toml:"min_tls_version"`
|
||||
minTLSVersion uint16 `toml:"-"`
|
||||
TLSCiphers []string `toml:"tls_ciphers"`
|
||||
tlsCiphers []uint16 `toml:"-"`
|
||||
UseSSL bool `toml:"use_ssl" json:"use_ssl,omitempty"`
|
||||
StartTLS bool `toml:"start_tls" json:"start_tls,omitempty"`
|
||||
SkipVerifySSL bool `toml:"ssl_skip_verify" json:"ssl_skip_verify,omitempty"`
|
||||
MinTLSVersion string `toml:"min_tls_version" json:"min_tls_version,omitempty"`
|
||||
minTLSVersion uint16 `toml:"-" json:"-"`
|
||||
TLSCiphers []string `toml:"tls_ciphers" json:"tls_ciphers,omitempty"`
|
||||
tlsCiphers []uint16 `toml:"-" json:"-"`
|
||||
|
||||
RootCACert string `toml:"root_ca_cert"`
|
||||
RootCACertValue []string
|
||||
ClientCert string `toml:"client_cert"`
|
||||
ClientCertValue string
|
||||
ClientKey string `toml:"client_key"`
|
||||
ClientKeyValue string
|
||||
BindDN string `toml:"bind_dn"`
|
||||
BindPassword string `toml:"bind_password"`
|
||||
Timeout int `toml:"timeout"`
|
||||
Attr AttributeMap `toml:"attributes"`
|
||||
RootCACert string `toml:"root_ca_cert" json:"root_ca_cert,omitempty"`
|
||||
RootCACertValue []string `json:"root_ca_cert_value,omitempty"`
|
||||
ClientCert string `toml:"client_cert" json:"client_cert,omitempty"`
|
||||
ClientCertValue string `json:"client_cert_value,omitempty"`
|
||||
ClientKey string `toml:"client_key" json:"client_key,omitempty"`
|
||||
ClientKeyValue string `json:"client_key_value,omitempty"`
|
||||
BindDN string `toml:"bind_dn" json:"bind_dn,omitempty"`
|
||||
BindPassword string `toml:"bind_password" json:"bind_password,omitempty"`
|
||||
Timeout int `toml:"timeout" json:"timeout,omitempty"`
|
||||
Attr AttributeMap `toml:"attributes" json:"attributes,omitempty"`
|
||||
|
||||
SearchFilter string `toml:"search_filter"`
|
||||
SearchBaseDNs []string `toml:"search_base_dns"`
|
||||
SearchFilter string `toml:"search_filter" json:"search_filter,omitempty"`
|
||||
SearchBaseDNs []string `toml:"search_base_dns" json:"search_base_dns,omitempty"`
|
||||
|
||||
GroupSearchFilter string `toml:"group_search_filter"`
|
||||
GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute"`
|
||||
GroupSearchBaseDNs []string `toml:"group_search_base_dns"`
|
||||
GroupSearchFilter string `toml:"group_search_filter" json:"group_search_filter,omitempty"`
|
||||
GroupSearchFilterUserAttribute string `toml:"group_search_filter_user_attribute" json:"group_search_filter_user_attribute,omitempty"`
|
||||
GroupSearchBaseDNs []string `toml:"group_search_base_dns" json:"group_search_base_dns,omitempty"`
|
||||
|
||||
Groups []*GroupToOrgRole `toml:"group_mappings"`
|
||||
Groups []*GroupToOrgRole `toml:"group_mappings" json:"group_mappings,omitempty"`
|
||||
}
|
||||
|
||||
// AttributeMap is a struct representation for LDAP "attributes" setting
|
||||
type AttributeMap struct {
|
||||
Username string `toml:"username"`
|
||||
Name string `toml:"name"`
|
||||
Surname string `toml:"surname"`
|
||||
Email string `toml:"email"`
|
||||
MemberOf string `toml:"member_of"`
|
||||
Username string `toml:"username" json:"username,omitempty"`
|
||||
Name string `toml:"name" json:"name,omitempty"`
|
||||
Surname string `toml:"surname" json:"surname,omitempty"`
|
||||
Email string `toml:"email" json:"email,omitempty"`
|
||||
MemberOf string `toml:"member_of" json:"member_of,omitempty"`
|
||||
}
|
||||
|
||||
// GroupToOrgRole is a struct representation of LDAP
|
||||
// config "group_mappings" setting
|
||||
type GroupToOrgRole struct {
|
||||
GroupDN string `toml:"group_dn"`
|
||||
OrgId int64 `toml:"org_id"`
|
||||
GroupDN string `toml:"group_dn" json:"group_dn"`
|
||||
OrgId int64 `toml:"org_id" json:"org_id"`
|
||||
|
||||
// This pointer specifies if setting was set (for backwards compatibility)
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin"`
|
||||
IsGrafanaAdmin *bool `toml:"grafana_admin" json:"grafana_admin,omitempty"`
|
||||
|
||||
OrgRole org.RoleType `toml:"org_role"`
|
||||
OrgRole org.RoleType `toml:"org_role" json:"org_role,omitempty"`
|
||||
}
|
||||
|
||||
// logger for all LDAP stuff
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/service"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
@ -47,9 +48,10 @@ type Service struct {
|
||||
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
|
||||
routeRegister routing.RouteRegister, features featuremgmt.FeatureToggles,
|
||||
secrets secrets.Service, usageStats usagestats.Service, registerer prometheus.Registerer,
|
||||
settingsProvider setting.Provider, licensing licensing.Licensing) *Service {
|
||||
settingsProvider setting.Provider, licensing licensing.Licensing, ldap service.LDAP) *Service {
|
||||
fbStrategies := []ssosettings.FallbackStrategy{
|
||||
strategies.NewOAuthStrategy(cfg),
|
||||
strategies.NewLDAPStrategy(cfg, ldap),
|
||||
}
|
||||
|
||||
configurableProviders := make(map[string]bool)
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/service"
|
||||
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
|
||||
secretsFakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
@ -1555,7 +1556,7 @@ func Test_ProviderService(t *testing.T) {
|
||||
"azuread",
|
||||
"okta",
|
||||
},
|
||||
strategiesLength: 1,
|
||||
strategiesLength: 2,
|
||||
},
|
||||
{
|
||||
name: "should return all fallback strategies and it should return all OAuth providers but not SAML because the licensing feature is enabled but the configurable provider is not setup",
|
||||
@ -1569,7 +1570,7 @@ func Test_ProviderService(t *testing.T) {
|
||||
"azuread",
|
||||
"okta",
|
||||
},
|
||||
strategiesLength: 2,
|
||||
strategiesLength: 3,
|
||||
},
|
||||
{
|
||||
name: "should return all fallback strategies and it should return all OAuth providers and SAML because the licensing feature is enabled and the provider is setup",
|
||||
@ -1585,7 +1586,7 @@ func Test_ProviderService(t *testing.T) {
|
||||
"okta",
|
||||
"saml",
|
||||
},
|
||||
strategiesLength: 2,
|
||||
strategiesLength: 3,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
@ -1647,6 +1648,7 @@ func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies, sam
|
||||
prometheus.NewRegistry(),
|
||||
&setting.OSSImpl{Cfg: cfg},
|
||||
licensing,
|
||||
service.ProvideService(cfg),
|
||||
)
|
||||
|
||||
// overriding values for exposed fields
|
||||
|
136
pkg/services/ssosettings/strategies/ldap_strategy.go
Normal file
136
pkg/services/ssosettings/strategies/ldap_strategy.go
Normal file
@ -0,0 +1,136 @@
|
||||
package strategies
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/services/ldap/service"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type LDAPStrategy struct {
|
||||
cfg *setting.Cfg
|
||||
ldap service.LDAP
|
||||
}
|
||||
|
||||
var _ ssosettings.FallbackStrategy = (*LDAPStrategy)(nil)
|
||||
|
||||
func NewLDAPStrategy(cfg *setting.Cfg, ldap service.LDAP) *LDAPStrategy {
|
||||
return &LDAPStrategy{
|
||||
cfg: cfg,
|
||||
ldap: ldap,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LDAPStrategy) IsMatch(provider string) bool {
|
||||
return provider == social.LDAPProviderName
|
||||
}
|
||||
|
||||
func (s *LDAPStrategy) GetProviderConfig(_ context.Context, _ string) (map[string]any, error) {
|
||||
config, err := s.getLDAPConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
section := s.cfg.Raw.Section("auth.ldap")
|
||||
|
||||
result := map[string]any{
|
||||
"enabled": section.Key("enabled").MustBool(false),
|
||||
"config": config,
|
||||
"allow_sign_up": section.Key("allow_sign_up").MustBool(false),
|
||||
"skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false),
|
||||
"sync_cron": section.Key("sync_cron").Value(),
|
||||
"active_sync_enabled": section.Key("active_sync_enabled").MustBool(false),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *LDAPStrategy) getLDAPConfig() (map[string]any, error) {
|
||||
var configMap map[string]any
|
||||
|
||||
config := s.ldap.Config()
|
||||
configJson, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := json.NewDecoder(bytes.NewReader(configJson))
|
||||
d.UseNumber()
|
||||
err = d.Decode(&configMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// json decodes numbers as json.Number type
|
||||
// this iterates over all items in the map and returns a new map
|
||||
// with all json.Number replaced by int64
|
||||
result, err := replaceNumbersInMap(configMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func replaceNumbersInMap(m map[string]any) (map[string]any, error) {
|
||||
var err error
|
||||
|
||||
result := make(map[string]any)
|
||||
for k, v := range m {
|
||||
switch v := v.(type) {
|
||||
case json.Number:
|
||||
result[k], err = v.Int64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case []any:
|
||||
result[k], err = replaceNumbersInSlice(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case map[string]any:
|
||||
result[k], err = replaceNumbersInMap(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func replaceNumbersInSlice(s []any) ([]any, error) {
|
||||
result := make([]any, 0)
|
||||
for _, v := range s {
|
||||
switch v := v.(type) {
|
||||
case json.Number:
|
||||
number, err := v.Int64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, number)
|
||||
case []any:
|
||||
inner, err := replaceNumbersInSlice(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, inner)
|
||||
case map[string]any:
|
||||
inner, err := replaceNumbersInMap(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, inner)
|
||||
default:
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
87
pkg/services/ssosettings/strategies/ldap_strategy_test.go
Normal file
87
pkg/services/ssosettings/strategies/ldap_strategy_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package strategies
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ldap/service"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
ldapConfig = `[auth.ldap]
|
||||
enabled = true
|
||||
config_file = ./testdata/ldap.toml
|
||||
allow_sign_up = true
|
||||
skip_org_role_sync = false
|
||||
sync_cron = "0 1 * * *"
|
||||
active_sync_enabled = true`
|
||||
)
|
||||
|
||||
var (
|
||||
expectedLdapConfig = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"allow_sign_up": true,
|
||||
"skip_org_role_sync": false,
|
||||
"config": map[string]interface{}{
|
||||
"servers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"Host": "127.0.0.1",
|
||||
"Port": int64(3389),
|
||||
"attributes": map[string]interface{}{
|
||||
"email": "mail",
|
||||
"member_of": "memberOf",
|
||||
"name": "displayName",
|
||||
"surname": "sn",
|
||||
"username": "cn",
|
||||
},
|
||||
"bind_dn": "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io",
|
||||
"bind_password": "grafana",
|
||||
"group_mappings": []interface{}{
|
||||
map[string]interface{}{
|
||||
"group_dn": "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io",
|
||||
"org_id": int64(1),
|
||||
"org_role": "Admin",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"group_dn": "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io",
|
||||
"org_id": int64(1),
|
||||
"org_role": "Editor",
|
||||
},
|
||||
map[string]interface{}{"group_dn": "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io",
|
||||
"org_id": int64(1),
|
||||
"org_role": "Viewer",
|
||||
},
|
||||
},
|
||||
"search_base_dns": []interface{}{
|
||||
"DC=ldap,DC=goauthentik,DC=io",
|
||||
},
|
||||
"search_filter": "(cn=%s)", "ssl_skip_verify": true,
|
||||
"timeout": int64(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
"active_sync_enabled": true,
|
||||
"sync_cron": "0 1 * * *",
|
||||
}
|
||||
)
|
||||
|
||||
func TestGetLDAPConfig(t *testing.T) {
|
||||
iniFile, err := ini.Load([]byte(ldapConfig))
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg, err := setting.NewCfgFromINIFile(iniFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
ldap := service.ProvideService(cfg)
|
||||
|
||||
strategy := NewLDAPStrategy(cfg, ldap)
|
||||
|
||||
result, err := strategy.GetProviderConfig(context.Background(), "ldap")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedLdapConfig, result)
|
||||
}
|
38
pkg/services/ssosettings/strategies/testdata/ldap.toml
vendored
Normal file
38
pkg/services/ssosettings/strategies/testdata/ldap.toml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
[[servers]]
|
||||
host = "127.0.0.1"
|
||||
port = 3389
|
||||
use_ssl = false
|
||||
start_tls = false
|
||||
ssl_skip_verify = true
|
||||
|
||||
bind_dn = "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io"
|
||||
bind_password = 'grafana'
|
||||
timeout = 10
|
||||
|
||||
search_filter = "(cn=%s)"
|
||||
search_base_dns = ["DC=ldap,DC=goauthentik,DC=io"]
|
||||
|
||||
# Specify names of the ldap attributes your ldap uses
|
||||
[servers.attributes]
|
||||
name = "displayName"
|
||||
surname = "sn"
|
||||
username = "cn"
|
||||
member_of = "memberOf"
|
||||
email = "mail"
|
||||
|
||||
# Map ldap groups to grafana org roles
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
org_role = "Admin"
|
||||
org_id = 1
|
||||
|
||||
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
org_role = "Editor"
|
||||
org_id = 1
|
||||
|
||||
[[servers.group_mappings]]
|
||||
group_dn = "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io"
|
||||
org_role = "Viewer"
|
||||
org_id = 1
|
Loading…
Reference in New Issue
Block a user