diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 634257b5886..ca52ce578c1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -195,4 +195,5 @@ export interface FeatureToggles { pinNavItems?: boolean; authZGRPCServer?: boolean; openSearchBackendFlowEnabled?: boolean; + ssoSettingsLDAP?: boolean; } diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index 6ac7811f3c8..1984704104f 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -14,7 +14,6 @@ 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" @@ -82,7 +81,6 @@ func TestSocialService_ProvideService(t *testing.T) { nil, &setting.OSSImpl{Cfg: cfg}, &licensing.OSSLicensingService{}, - ldap.ProvideService(cfg), ) for _, tc := range testCases { @@ -195,7 +193,6 @@ func TestSocialService_ProvideService_GrafanaComGrafanaNet(t *testing.T) { nil, nil, &licensing.OSSLicensingService{}, - ldap.ProvideService(cfg), ) for _, tc := range testCases { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3626238d106..de3a47a560c 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1323,6 +1323,14 @@ var ( Stage: FeatureStagePublicPreview, Owner: awsDatasourcesSquad, }, + { + Name: "ssoSettingsLDAP", + Description: "Use the new SSO Settings API to configure LDAP", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromDocs: true, + HideFromAdminPage: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 6e189240da4..c920feb800e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -176,3 +176,4 @@ azureMonitorPrometheusExemplars,experimental,@grafana/partner-datasources,false, pinNavItems,experimental,@grafana/grafana-frontend-platform,false,false,false authZGRPCServer,experimental,@grafana/identity-access-team,false,false,false openSearchBackendFlowEnabled,preview,@grafana/aws-datasources,false,false,false +ssoSettingsLDAP,experimental,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 420c12da06f..0cd93f4753f 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -714,4 +714,8 @@ const ( // FlagOpenSearchBackendFlowEnabled // Enables the backend query flow for Open Search datasource plugin FlagOpenSearchBackendFlowEnabled = "openSearchBackendFlowEnabled" + + // FlagSsoSettingsLDAP + // Use the new SSO Settings API to configure LDAP + FlagSsoSettingsLDAP = "ssoSettingsLDAP" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 70cda830cc7..731316282d3 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2126,6 +2126,20 @@ "allowSelfServe": true } }, + { + "metadata": { + "name": "ssoSettingsLDAP", + "resourceVersion": "1718624569822", + "creationTimestamp": "2024-06-17T11:42:49Z" + }, + "spec": { + "description": "Use the new SSO Settings API to configure LDAP", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "ssoSettingsSAML", diff --git a/pkg/services/ldap/service/helpers.go b/pkg/services/ldap/service/helpers.go index 6e2c10d5eb6..d2b487c4289 100644 --- a/pkg/services/ldap/service/helpers.go +++ b/pkg/services/ldap/service/helpers.go @@ -1,8 +1,10 @@ package service import ( + "encoding/json" "fmt" "os" + "strconv" "github.com/BurntSushi/toml" @@ -85,3 +87,27 @@ func assertNotEmptyCfg(val any, propName string) error { } return nil } + +func resolveBool(input any, defaultValue bool) bool { + strInput := fmt.Sprintf("%v", input) + result, err := strconv.ParseBool(strInput) + if err != nil { + return defaultValue + } + return result +} + +func resolveServerConfig(input any) (*ldap.ServersConfig, error) { + var ldapCfg ldap.ServersConfig + + inputJson, err := json.Marshal(input) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(inputJson, &ldapCfg); err != nil { + return nil, err + } + + return &ldapCfg, nil +} diff --git a/pkg/services/ldap/service/ldap.go b/pkg/services/ldap/service/ldap.go index 1380b65a9b3..2125b322cb6 100644 --- a/pkg/services/ldap/service/ldap.go +++ b/pkg/services/ldap/service/ldap.go @@ -1,13 +1,19 @@ package service import ( + "context" "errors" "sync" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ldap/multildap" "github.com/grafana/grafana/pkg/services/login" + "github.com/grafana/grafana/pkg/services/ssosettings" + "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/setting" ) @@ -29,39 +35,82 @@ type LDAP interface { } type LDAPImpl struct { - client multildap.IMultiLDAP - cfg *ldap.Config - ldapCfg *ldap.ServersConfig - log log.Logger + client multildap.IMultiLDAP + cfg *ldap.Config + ldapCfg *ldap.ServersConfig + log log.Logger + features featuremgmt.FeatureToggles + ssoSettings ssosettings.Service // loadingMutex locks the reading of the config so multiple requests for reloading are sequential. loadingMutex *sync.Mutex } -func ProvideService(cfg *setting.Cfg) *LDAPImpl { +func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, ssoSettings ssosettings.Service) *LDAPImpl { s := &LDAPImpl{ - client: nil, - ldapCfg: nil, - cfg: ldap.GetLDAPConfig(cfg), log: log.New("ldap.service"), loadingMutex: &sync.Mutex{}, + features: features, + ssoSettings: ssoSettings, } - if !cfg.LDAPAuthEnabled { - return s - } + if s.features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) && s.features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { + s.ssoSettings.RegisterReloadable(social.LDAPProviderName, s) - ldapCfg, err := multildap.GetConfig(s.cfg) - if err != nil { - s.log.Error("Failed to get LDAP config", "error", err) + ldapSettings, err := s.ssoSettings.GetForProvider(context.Background(), social.LDAPProviderName) + if err != nil { + s.log.Error("Failed to retrieve LDAP settings from SSO settings service", "error", err) + return s + } + + err = s.Reload(context.Background(), *ldapSettings) + if err != nil { + s.log.Error("Failed to load LDAP settings", "error", err) + return s + } } else { - s.ldapCfg = ldapCfg - s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + s.cfg = ldap.GetLDAPConfig(cfg) + if !cfg.LDAPAuthEnabled { + return s + } + + ldapCfg, err := multildap.GetConfig(s.cfg) + if err != nil { + s.log.Error("Failed to get LDAP config", "error", err) + } else { + s.ldapCfg = ldapCfg + s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + } } return s } +func (s *LDAPImpl) Reload(ctx context.Context, settings models.SSOSettings) error { + cfg := &ldap.Config{} + cfg.Enabled = resolveBool(settings.Settings["enabled"], false) + cfg.SkipOrgRoleSync = resolveBool(settings.Settings["skip_org_role_sync"], false) + cfg.AllowSignUp = resolveBool(settings.Settings["allow_sign_up"], true) + + ldapCfg, err := resolveServerConfig(settings.Settings["config"]) + if err != nil { + return err + } + + s.loadingMutex.Lock() + defer s.loadingMutex.Unlock() + + s.cfg = cfg + s.ldapCfg = ldapCfg + s.client = multildap.New(s.ldapCfg.Servers, s.cfg) + + return nil +} + +func (s *LDAPImpl) Validate(ctx context.Context, settings models.SSOSettings, oldSettings models.SSOSettings, requester identity.Requester) error { + return nil +} + func (s *LDAPImpl) ReloadConfig() error { if !s.cfg.Enabled { return nil diff --git a/pkg/services/ldap/service/ldap_test.go b/pkg/services/ldap/service/ldap_test.go new file mode 100644 index 00000000000..74cb4ab1a8d --- /dev/null +++ b/pkg/services/ldap/service/ldap_test.go @@ -0,0 +1,299 @@ +package service + +import ( + "context" + "sync" + "testing" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/ldap" + "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/stretchr/testify/require" +) + +const ( + validCert = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYVENDQWtXZ0F3SUJBZ0lKQUxtVlZ1RFd1NE5ZTUEwR0NTcUdTSWIzRFFFQkN +3VUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSekl +GQjBlU0JNZEdRd0hoY05NVFl4TWpNeE1UUXpORFEzV2hjTk5EZ3dOakkxTVRRek5EUTNXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI +5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUTh +BTUlJQgpDZ0tDQVFFQXpVQ0ZvemdOYjFoMU0wanpOUlNDamhPQm5SK3VWYlZwYVdmWFlJUitBaFdEZEVlNXJ5WStDZ2F2Ck9nOGJmTHlieXpGZGVobFlkRFJ +na2VkRUIvR2pHOGFKdzA2bDBxRjRqRE9BdzBrRXlnV0N1Mm1jSDdYT3hSdCsKWUFIM1RWSGEvSHUxVzNXanprb2JxcXFMUThna0tXV00yN2ZPZ0FaNkdpZWF +KQk42VkJTTU1jUGV5M0hXTEJtYworVFlKbXYxZGJhTzJqSGhLaDhwZkt3MFcxMlZNOFAxUElPOGd2NFBodS91dUpZaWVCV0tpeEJFeXkwbEhqeWl4CllGQ1I +xMnhkaDRDQTQ3cTk1OFpSR25uRFVHRlZFMVFoZ1JhY0pDT1o5YmQ1dDltcjhLTGFWQllUQ0pvNUVSRTgKanltYWI1ZFBxZTVxS2ZKc0NaaXFXZ2xialVvOXR +3SURBUUFCbzFBd1RqQWRCZ05WSFE0RUZnUVV4cHV3Y3MvQwpZUU95dWkrcjFHKzNLeEJOaHhrd0h3WURWUjBqQkJnd0ZvQVV4cHV3Y3MvQ1lRT3l1aStyMUc +rM0t4Qk5oeGt3CkRBWURWUjBUQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQWlXVUtzLzJ4L3ZpTkNLaTNZNmIKbEV1Q3RBR2h6T09aOUV +qcnZKOCtDT0gzUmFnM3RWQldyY0JaMy91aGhQcTVneTlscXc0T2t2RXdzOTkvNWpGcwpYMUZKNk1LQmdxZnV5N3loNXMxWWZNMEFOSFljek1tWXBaZUFjUWY +yQ0dBYVZmd1RUZlNsek5Mc0YybFcvbHk3CnlhcEZ6bFlTSkxHb1ZFK09IRXU4ZzVTbE5BQ1VFZmtYdys1RWdoaCtLemxJTjdSNlE3cjJpeFdORkJDL2pXZjc +KTktVZkp5WDhxSUc1bWQxWVVlVDZHQlc5Qm0yLzEvUmlPMjRKVGFZbGZMZEtLOVRZYjhzRzVCK09MYWIyREltRwo5OUNKMjVSa0FjU29iV05GNXpEME82bGd +PbzNjRWRCL2tzQ3EzaG10bEMvRGxMWi9EOENKKzdWdVpuUzFyUjJuCmFRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==` + validKey = `LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFv +SUJBUUROUUlXak9BMXZXSFV6ClNQTTFGSUtPRTRHZEg2NVZ0V2xwWjlkZ2hINENGWU4wUjdtdkpqNEtCcTg2RHh0OHZKdkxNVjE2R1ZoME5HQ1IKNTBRSDhh +TWJ4b25EVHFYU29YaU1NNEREU1FUS0JZSzdhWndmdGM3RkczNWdBZmROVWRyOGU3VmJkYVBPU2h1cQpxb3REeUNRcFpZemJ0ODZBQm5vYUo1b2tFM3BVRkl3 +eHc5N0xjZFlzR1p6NU5nbWEvVjF0bzdhTWVFcUh5bDhyCkRSYlhaVXp3L1U4Zzd5Qy9nK0c3KzY0bGlKNEZZcUxFRVRMTFNVZVBLTEZnVUpIWGJGMkhnSURq +dXIzbnhsRWEKZWNOUVlWVVRWQ0dCRnB3a0k1bjF0M20zMmF2d290cFVGaE1JbWprUkVUeVBLWnB2bDArcDdtb3A4bXdKbUtwYQpDVnVOU2oyM0FnTUJBQUVD +Z2dFQUJuNEkvQjIweHhYY056QVNpVlpKdnVhOURkUkh0bXhUbGtMem5CajB4Mm9ZCnkxL05iczNkM29GUm41dUV1aEJaT1RjcGhzZ3dkUlNIRFhac1AzZ1VP +YmV3K2QyTi96aWVVSWo4aExEVmx2SlAKclUvczRVL2w1M1EwTGlOQnlFOVRodkwrekpMUENLSnRkNXVIWmpCNWZGbTY5K1E3Z3U4eGc0eEhJdWIrMHBQNQpQ +SGFubUhDRHJiZ05OL29xbGFyNEZaMk1YVGdla1c2QW15Yy9rb0U5aEluNEJhYTJLZS9CL0FVR1k0cE1STHFwClRBcnQrR1RWZVdlb0ZZOVFBQ1VwYUhwSmhH +Yi9QaW91NnRsVTU3ZTQyY0xva2kxZjArU0FSc0JCS3lYQTdCQjEKMWZNSDEwS1FZRkE2OGRUWVdsS3pRYXUvSzR4YXFnNEZLbXR3RjY2R1FRS0JnUUQ5T3BO +VVM3b1J4TUhWSmFCUgpUTldXK1YxRlh5Y3FvamVrRnBEaWpQYjJYNUNXVjE2b2VXZ2FYcDBuT0hGZHk5RVdzM0d0R3BmWmFzYVJWSHNYClNIdFBoNE5iOEpx +SGRHRTAvQ0Q2dDArNERuczhCbjljU3F0ZFFCN1IzSm43SU1YaTlYL1U4TERLbytBMTgvSnEKVjhWZ1VuZ01ueTlZak1rUUliSzhUUldrWVFLQmdRRFBmNG54 +TzZqdSt0T0hIT1JRdHkzYllERDArT1YzSTArTAoweXowdVByZXJ5QlZpOW5ZNDNLYWtINTJEN1VaRXd3c0JqakdYRCtXSDh4RXNtQldzR05YSnUwMjVQdnpJ +Sm96CmxBRWlYdk1wL05tWXArdFk0ckRtTzhSaHlWb2NCcVdIemgzOG0wSUZPZDRCeUZENW5MRURyQTNwRFZvMGFOZ1kKbjBHd1J5c1pGd0tCZ1FEa0NqM202 +Wk1Vc1VXRXR5K2FSMEVKaG1LeU9EQkRPblkwOUlWaEgyUy9GZXhWRnpVTgpMdGZLOTIwNmhwL0F3ZXozTG4ydVQ0WnpxcTVLN2ZNelVuaUpkQldkVkIwMDRs +OHZvZVhwSWU5T1p1d2ZjQko5CmdGaTF6eXB4L3VGRHY0MjFCelFwQk4rUWZPZEtidmJkUVZGam5xQ3hiU0RyODB5VmxHTXJJNWZid1FLQmdHMDkKb1JyZXBP +N0VJTzhHTi9HQ3J1TEsvcHRLR2t5aHkzUTZ4blZFbWRiNDdoWDduY0pBNUlvWlBtcmJsQ1ZTVU5zdwpuMTFYSGFia3NMOE9CZ2c5cnQ4b1FFVGhRdi9hRHpU +T1c5YURsSk5yYWdlamlCVHdxOTlhWWVaMWdqbzFDWnE0CjJqS3VicENmeVpDNHJHRHRySWZaWWkxcStTMlVjUWh0ZDhEZGh3UWJBb0dBQU00RXBEQTR5SEI1 +eWllazFwL28KQ2JxUkN0YS9EeDZFeW8wS2xOQXlQdUZQQXNodXBHNE5CeDdtVDJBU2ZMKzJWQkhvaTZtSFNyaStCRFg1cnlZRgpmTVl2cDdVUllvcTd3N3Fp +dlJsdnZFZzV5b1lySzEzRjIrR2o2eEo0akVOOW0wS2RNL2czbUpHcTBIQlRJUXJwClNtNzVXWHNmbE94dVRuMDhMYmdHYzRzPQotLS0tLUVORCBSU0EgUFJJ +VkFURSBLRVktLS0tLQ==` +) + +var ( + isAdmin = true +) + +func TestReload(t *testing.T) { + testCases := []struct { + description string + settings models.SSOSettings + expectedServersConfig *ldap.ServersConfig + expectedConfig *ldap.Config + }{ + { + description: "basic flow with minimal config", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "complete set of server parameters", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + "port": 3389, + "bind_dn": "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io", + "bind_password": "grafana", + "search_filter": "(cn=%s)", + "ssl_skip_verify": false, + "use_ssl": true, + "start_tls": true, + "min_tls_version": "TLS1.3", + "timeout": 10, + "root_ca_cert": "/path/to/certificate.crt", + "root_ca_cert_value": []string{validCert}, + "client_cert": "/path/to/client.crt", + "client_cert_value": validCert, + "client_key": "/path/to/client.key", + "client_key_value": validKey, + "group_search_filter": "(&(objectClass=posixGroup)(memberUid=%s))", + "group_search_filter_user_attribute": "distinguishedName", + "group_search_base_dns": []string{"ou=groups,dc=grafana,dc=org"}, + "tls_ciphers": []string{ + "TLS_AES_256_GCM_SHA384", + }, + "attributes": map[string]string{ + "email": "mail", + "member_of": "memberOf", + "name": "displayName", + "surname": "sn", + "username": "cn", + }, + "search_base_dns": []string{ + "DC=ldap,DC=goauthentik,DC=io", + }, + "group_mappings": []any{ + map[string]any{ + "group_dn": "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Admin", + "grafana_admin": true, + }, + map[string]any{ + "group_dn": "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Editor", + }, + map[string]any{ + "group_dn": "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io", + "org_id": 1, + "org_role": "Viewer", + }, + }, + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + Port: 3389, + UseSSL: true, + StartTLS: true, + SkipVerifySSL: false, + MinTLSVersion: "TLS1.3", + TLSCiphers: []string{ + "TLS_AES_256_GCM_SHA384", + }, + RootCACert: "/path/to/certificate.crt", + RootCACertValue: []string{validCert}, + ClientCert: "/path/to/client.crt", + ClientCertValue: validCert, + ClientKey: "/path/to/client.key", + ClientKeyValue: validKey, + BindDN: "cn=ldapservice,ou=users,dc=ldap,dc=goauthentik,dc=io", + BindPassword: "grafana", + Timeout: 10, + Attr: ldap.AttributeMap{ + Username: "cn", + Name: "displayName", + Surname: "sn", + Email: "mail", + MemberOf: "memberOf", + }, + SearchFilter: "(cn=%s)", + SearchBaseDNs: []string{"DC=ldap,DC=goauthentik,DC=io"}, + GroupSearchFilter: "(&(objectClass=posixGroup)(memberUid=%s))", + GroupSearchFilterUserAttribute: "distinguishedName", + GroupSearchBaseDNs: []string{"ou=groups,dc=grafana,dc=org"}, + Groups: []*ldap.GroupToOrgRole{ + { + GroupDN: "cn=admin,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Admin", + IsGrafanaAdmin: &isAdmin, + }, + { + GroupDN: "cn=editor,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Editor", + }, + { + GroupDN: "cn=viewer,ou=groups,dc=ldap,dc=goauthentik,dc=io", + OrgId: 1, + OrgRole: "Viewer", + }, + }, + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "no servers config", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": true, + "skip_org_role_sync": false, + "allow_sign_up": true, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{Servers: nil}, + expectedConfig: &ldap.Config{ + Enabled: true, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + { + description: "invalid settings", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "invalid": "some-value", + "another_one": true, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{Servers: nil}, + expectedConfig: &ldap.Config{ + AllowSignUp: true, + }, + }, + { + description: "config disabled", + settings: models.SSOSettings{ + Provider: "ldap", + Settings: map[string]any{ + "enabled": false, + "skip_org_role_sync": false, + "allow_sign_up": true, + "config": map[string]any{ + "servers": []any{ + map[string]any{ + "host": "127.0.0.1", + }, + }, + }, + }, + IsDeleted: false, + }, + expectedServersConfig: &ldap.ServersConfig{ + Servers: []*ldap.ServerConfig{ + { + Host: "127.0.0.1", + }, + }, + }, + expectedConfig: &ldap.Config{ + Enabled: false, + AllowSignUp: true, + SkipOrgRoleSync: false, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.description, func(t *testing.T) { + ldapImpl := &LDAPImpl{ + features: featuremgmt.WithManager(featuremgmt.FlagSsoSettingsApi), + loadingMutex: &sync.Mutex{}, + } + + err := ldapImpl.Reload(context.Background(), tt.settings) + require.NoError(t, err) + require.Equal(t, *tt.expectedServersConfig, *ldapImpl.ldapCfg) + require.Equal(t, *tt.expectedConfig, *ldapImpl.cfg) + }) + } +} diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index a7d7b837cb0..90f3732b235 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -32,8 +32,8 @@ type ServersConfig struct { // ServerConfig holds connection data to LDAP type ServerConfig struct { - Host string `toml:"host"` - Port int `toml:"port"` + Host string `toml:"host" json:"host"` + Port int `toml:"port" json:"port,omitempty"` UseSSL bool `toml:"use_ssl" json:"use_ssl,omitempty"` StartTLS bool `toml:"start_tls" json:"start_tls,omitempty"` diff --git a/pkg/services/ssosettings/ssosettingsimpl/service.go b/pkg/services/ssosettings/ssosettingsimpl/service.go index 0de3874e1fb..ad80c0dbfbb 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/login/social" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "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" @@ -48,10 +47,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, ldap service.LDAP) *Service { + settingsProvider setting.Provider, licensing licensing.Licensing) *Service { fbStrategies := []ssosettings.FallbackStrategy{ strategies.NewOAuthStrategy(cfg), - strategies.NewLDAPStrategy(cfg, ldap), + strategies.NewLDAPStrategy(cfg), } configurableProviders := make(map[string]bool) @@ -60,6 +59,11 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl, } providersList := ssosettings.AllOAuthProviders + + if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsLDAP) { + providersList = append(providersList, social.LDAPProviderName) + } + if licensing.FeatureEnabled(social.SAMLProviderName) { fbStrategies = append(fbStrategies, strategies.NewSAMLStrategy(settingsProvider)) diff --git a/pkg/services/ssosettings/ssosettingsimpl/service_test.go b/pkg/services/ssosettings/ssosettingsimpl/service_test.go index 4b2997c2ae1..094105c00b4 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service_test.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service_test.go @@ -22,7 +22,6 @@ 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" @@ -1648,7 +1647,6 @@ func setupTestEnv(t *testing.T, isLicensingEnabled, keepFallbackStratergies, sam prometheus.NewRegistry(), &setting.OSSImpl{Cfg: cfg}, licensing, - service.ProvideService(cfg), ) // overriding values for exposed fields diff --git a/pkg/services/ssosettings/strategies/ldap_strategy.go b/pkg/services/ssosettings/strategies/ldap_strategy.go index afbde023a13..00447121682 100644 --- a/pkg/services/ssosettings/strategies/ldap_strategy.go +++ b/pkg/services/ssosettings/strategies/ldap_strategy.go @@ -6,22 +6,20 @@ import ( "encoding/json" "github.com/grafana/grafana/pkg/login/social" - "github.com/grafana/grafana/pkg/services/ldap/service" + "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/setting" ) type LDAPStrategy struct { - cfg *setting.Cfg - ldap service.LDAP + cfg *setting.Cfg } var _ ssosettings.FallbackStrategy = (*LDAPStrategy)(nil) -func NewLDAPStrategy(cfg *setting.Cfg, ldap service.LDAP) *LDAPStrategy { +func NewLDAPStrategy(cfg *setting.Cfg) *LDAPStrategy { return &LDAPStrategy{ - cfg: cfg, - ldap: ldap, + cfg: cfg, } } @@ -52,8 +50,13 @@ func (s *LDAPStrategy) GetProviderConfig(_ context.Context, _ string) (map[strin func (s *LDAPStrategy) getLDAPConfig() (map[string]any, error) { var configMap map[string]any - config := s.ldap.Config() - configJson, err := json.Marshal(config) + config := ldap.GetLDAPConfig(s.cfg) + ldapConfig, err := ldap.GetConfig(config) + if err != nil { + return nil, err + } + + configJson, err := json.Marshal(ldapConfig) if err != nil { return nil, err } diff --git a/pkg/services/ssosettings/strategies/ldap_strategy_test.go b/pkg/services/ssosettings/strategies/ldap_strategy_test.go index 726411b44a0..ae4bb82405a 100644 --- a/pkg/services/ssosettings/strategies/ldap_strategy_test.go +++ b/pkg/services/ssosettings/strategies/ldap_strategy_test.go @@ -7,7 +7,6 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/ini.v1" - "github.com/grafana/grafana/pkg/services/ldap/service" "github.com/grafana/grafana/pkg/setting" ) @@ -29,8 +28,8 @@ var ( "config": map[string]interface{}{ "servers": []interface{}{ map[string]interface{}{ - "Host": "127.0.0.1", - "Port": int64(3389), + "host": "127.0.0.1", + "port": int64(3389), "attributes": map[string]interface{}{ "email": "mail", "member_of": "memberOf", @@ -76,9 +75,7 @@ func TestGetLDAPConfig(t *testing.T) { cfg, err := setting.NewCfgFromINIFile(iniFile) require.NoError(t, err) - ldap := service.ProvideService(cfg) - - strategy := NewLDAPStrategy(cfg, ldap) + strategy := NewLDAPStrategy(cfg) result, err := strategy.GetProviderConfig(context.Background(), "ldap") require.NoError(t, err)