diff --git a/devenv/docker/blocks/auth/authentik/ldap_authentik.toml b/devenv/docker/blocks/auth/authentik/ldap_authentik.toml index b8a627a9a44..69aadd8fac5 100644 --- a/devenv/docker/blocks/auth/authentik/ldap_authentik.toml +++ b/devenv/docker/blocks/auth/authentik/ldap_authentik.toml @@ -18,7 +18,7 @@ name = "displayName" surname = "sn" username = "cn" member_of = "memberOf" -email = "mail" +email = "mail" # Map ldap groups to grafana org roles [[servers.group_mappings]] diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 66c943bddce..f3c38d266a7 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -114,11 +114,7 @@ func GetLDAPConfig(cfg *setting.Cfg) *Config { // GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of // the config or it reads it and caches it first. func GetConfig(cfg *Config) (*ServersConfig, error) { - if cfg != nil { - if !cfg.Enabled { - return nil, nil - } - } else if !cfg.Enabled { + if cfg == nil || !cfg.Enabled { return nil, nil } diff --git a/public/app/features/admin/ldap/LdapSettingsPage.tsx b/public/app/features/admin/ldap/LdapSettingsPage.tsx new file mode 100644 index 00000000000..e0217d3c950 --- /dev/null +++ b/public/app/features/admin/ldap/LdapSettingsPage.tsx @@ -0,0 +1,294 @@ +import { css } from '@emotion/css'; +import { useEffect, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { connect } from 'react-redux'; + +import { AppEvents, GrafanaTheme2, NavModelItem } from '@grafana/data'; +import { getBackendSrv, getAppEvents } from '@grafana/runtime'; +import { useStyles2, Alert, Box, Button, Field, Input, Stack, TextLink } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; +import config from 'app/core/config'; +import { t, Trans } from 'app/core/internationalization'; +import { Loader } from 'app/features/plugins/admin/components/Loader'; +import { LdapPayload, StoreState } from 'app/types'; + +const appEvents = getAppEvents(); + +const mapStateToProps = (state: StoreState) => ({ + ldapSsoSettings: state.ldap.ldapSsoSettings, +}); + +const mapDispatchToProps = {}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const pageNav: NavModelItem = { + text: 'LDAP', + icon: 'shield', + id: 'LDAP', +}; + +const emptySettings: LdapPayload = { + id: '', + provider: '', + source: '', + settings: { + activeSyncEnabled: false, + allowSignUp: false, + config: { + servers: [ + { + attributes: {}, + bind_dn: '', + bind_password: '', + client_cert: '', + client_key: '', + group_mappings: [], + group_search_base_dns: [], + group_search_filter: '', + group_search_filter_user_attribute: '', + host: '', + min_tls_version: '', + port: 389, + root_ca_cert: '', + search_base_dns: [], + search_filter: '', + skip_org_role_sync: false, + ssl_skip_verify: false, + start_tls: false, + timeout: 10, + tls_ciphers: [], + tls_skip_verify: false, + use_ssl: false, + }, + ], + }, + enabled: false, + skipOrgRoleSync: false, + syncCron: '', + }, +}; + +export const LdapSettingsPage = () => { + const [isLoading, setIsLoading] = useState(true); + + const methods = useForm({ defaultValues: emptySettings }); + const { getValues, handleSubmit, register, reset } = methods; + + const styles = useStyles2(getStyles); + + useEffect(() => { + async function init() { + const payload = await getBackendSrv().get('/api/v1/sso-settings/ldap'); + if (!payload || !payload.settings || !payload.settings.config) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-fetching', 'Error fetching LDAP settings')], + }); + return; + } + + reset(payload); + setIsLoading(false); + } + init(); + }, [reset]); + + /** + * Display warning if the feature flag is disabled + */ + if (!config.featureToggles.ssoSettingsLDAP) { + return ( + + + This page is only accessible by enabling the ssoSettingsLDAP feature flag. + + + ); + } + + /** + * Save payload to the backend + * @param payload LdapPayload + */ + const putPayload = async (payload: LdapPayload) => { + try { + const result = await getBackendSrv().put('/api/v1/sso-settings/ldap', payload); + if (result) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')], + }); + } + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: [t('ldap-settings-page.alert.saved', 'LDAP settings saved')], + }); + } catch (error) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')], + }); + } + }; + + const onErrors = () => { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-validate-form', 'Error validating LDAP settings')], + }); + }; + + /** + * Button's Actions + */ + const submitAndEnableLdapSettings = (payload: LdapPayload) => { + payload.settings.enabled = true; + putPayload(payload); + }; + const saveForm = () => { + putPayload(getValues()); + }; + const discardForm = async () => { + try { + setIsLoading(true); + await getBackendSrv().delete('/api/v1/sso-settings/ldap'); + const payload = await getBackendSrv().get('/api/v1/sso-settings/ldap'); + if (!payload || !payload.settings || !payload.settings.config) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-update', 'Error updating LDAP settings')], + }); + return; + } + reset(payload); + } catch (error) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [t('ldap-settings-page.alert.error-saving', 'Error saving LDAP settings')], + }); + } finally { + setIsLoading(false); + } + }; + + const documentation = ( + + documentation + + ); + const subTitle = ( + + The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in + our {documentation}. + + ); + + return ( + + + +
+ {isLoading && } + {!isLoading && ( +
+

+ Basic Settings +

+ + + + + + + + + + + + + + + + + + + + + + +
+ )} + +
+
+
+ ); +}; + +function getStyles(theme: GrafanaTheme2) { + return { + form: css({ + width: theme.spacing(68), + }), + }; +} + +export default connector(LdapSettingsPage); diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index b3add89876d..9f0860bda70 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -52,7 +52,7 @@ export const AuthConfigPageUnconnected = ({ reportInteraction('authentication_ui_provider_clicked', { provider: providerType, enabled }); }; - // filter out saml from sso providers because it is already included in availableProviders + // filter out saml and ldap from sso providers because it is already included in availableProviders providers = providers.filter((p) => p.provider !== 'saml'); // temporarily remove LDAP until its configuration form is ready diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index a52aafa5eeb..8bf3abb4f6f 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -298,7 +298,11 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/admin/authentication/ldap', - component: LdapPage, + component: config.featureToggles.ssoSettingsLDAP + ? SafeDynamicImport( + () => import(/* webpackChunkName: "LdapSettingsPage" */ 'app/features/admin/ldap/LdapSettingsPage') + ) + : LdapPage, }, { path: '/admin/authentication/:provider', diff --git a/public/app/types/ldap.ts b/public/app/types/ldap.ts index eecc12fe01a..e4e9c6b6790 100644 --- a/public/app/types/ldap.ts +++ b/public/app/types/ldap.ts @@ -64,6 +64,46 @@ export interface LdapServerInfo { error: string; } +export interface GroupMapping { + group_dn?: string; + org_id?: number; + org_role?: string; + grafana_admin?: boolean; +} + +export interface LdapAttributes { + email?: string; + member_of?: string; + name?: string; + surname?: string; + username?: string; +} + +export interface LdapServerConfig { + attributes: LdapAttributes; + bind_dn: string; + bind_password?: string; + client_cert: string; + client_key: string; + group_mappings: GroupMapping[]; + group_search_base_dns: string[]; + group_search_filter: string; + group_search_filter_user_attribute: string; + host: string; + min_tls_version: string; + port: number; + root_ca_cert: string; + search_base_dns: string[]; + search_filter: string; + skip_org_role_sync: boolean; + ssl_skip_verify: boolean; + start_tls: boolean; + timeout: number; + tls_ciphers: string[]; + tls_skip_verify: boolean; + use_ssl: boolean; +} + export type LdapConnectionInfo = LdapServerInfo[]; export interface LdapState { @@ -73,4 +113,25 @@ export interface LdapState { connectionError?: LdapError; userError?: LdapError; ldapError?: LdapError; + ldapSsoSettings?: LdapServerConfig; +} + +export interface LdapConfig { + servers: LdapServerConfig[]; +} + +export interface LdapSettings { + activeSyncEnabled: boolean; + allowSignUp: boolean; + config: LdapConfig; + enabled: boolean; + skipOrgRoleSync: boolean; + syncCron: string; +} + +export interface LdapPayload { + id: string; + provider: string; + settings: LdapSettings; + source: string; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index a0753371a30..45aff726dd2 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -977,6 +977,53 @@ "refresh": "Refresh" } }, + "ldap-settings-page": { + "alert": { + "error-fetching": "Error fetching LDAP settings", + "error-saving": "Error saving LDAP settings", + "error-update": "Error updating LDAP settings", + "error-validate-form": "Error validating LDAP settings", + "feature-flag-disabled": "This page is only accessible by enabling the <1>ssoSettingsLDAP feature flag.", + "saved": "LDAP settings saved" + }, + "bind-dn": { + "description": "Distinguished name of the account used to bind and authenticate to the LDAP server.", + "label": "Bind DN", + "placeholder": "example: cn=admin,dc=grafana,dc=org" + }, + "bind-password": { + "label": "Bind password" + }, + "buttons-section": { + "discard": { + "button": "Discard" + }, + "save": { + "button": "Save" + }, + "save-and-enable": { + "button": "Save and enable" + } + }, + "documentation": "documentation", + "host": { + "description": "Hostname or IP address of the LDAP server you wish to connect to.", + "label": "Server host", + "placeholder": "example: 127.0.0.1" + }, + "search_filter": { + "description": "LDAP search filter used to locate specific entries within the directory.", + "label": "Search filter*", + "placeholder": "example: cn=%s" + }, + "search-base-dns": { + "description": "An array of base dns to search through; separate by commas or spaces.", + "label": "Search base DNS *", + "placeholder": "example: \"dc=grafana.dc=org\"" + }, + "subtitle": "The LDAP integration in Grafana allows your Grafana users to log in with their LDAP credentials. Find out more in our {documentation}.", + "title": "Basic Settings" + }, "library-panel": { "add-modal": { "cancel": "Cancel", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index ac9ac502962..fe72cd0a0b5 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -977,6 +977,53 @@ "refresh": "Ŗęƒřęşĥ" } }, + "ldap-settings-page": { + "alert": { + "error-fetching": "Ēřřőř ƒęŧčĥįʼnģ ĿĐÅP şęŧŧįʼnģş", + "error-saving": "Ēřřőř şävįʼnģ ĿĐÅP şęŧŧįʼnģş", + "error-update": "Ēřřőř ūpđäŧįʼnģ ĿĐÅP şęŧŧįʼnģş", + "error-validate-form": "Ēřřőř väľįđäŧįʼnģ ĿĐÅP şęŧŧįʼnģş", + "feature-flag-disabled": "Ŧĥįş päģę įş őʼnľy äččęşşįþľę þy ęʼnäþľįʼnģ ŧĥę <1>şşőŜęŧŧįʼnģşĿĐÅP ƒęäŧūřę ƒľäģ.", + "saved": "ĿĐÅP şęŧŧįʼnģş şävęđ" + }, + "bind-dn": { + "description": "Đįşŧįʼnģūįşĥęđ ʼnämę őƒ ŧĥę äččőūʼnŧ ūşęđ ŧő þįʼnđ äʼnđ äūŧĥęʼnŧįčäŧę ŧő ŧĥę ĿĐÅP şęřvęř.", + "label": "ßįʼnđ ĐŃ", + "placeholder": "ęχämpľę: čʼn=äđmįʼn,đč=ģřäƒäʼnä,đč=őřģ" + }, + "bind-password": { + "label": "ßįʼnđ päşşŵőřđ" + }, + "buttons-section": { + "discard": { + "button": "Đįşčäřđ" + }, + "save": { + "button": "Ŝävę" + }, + "save-and-enable": { + "button": "Ŝävę äʼnđ ęʼnäþľę" + } + }, + "documentation": "đőčūmęʼnŧäŧįőʼn", + "host": { + "description": "Ħőşŧʼnämę őř ĨP äđđřęşş őƒ ŧĥę ĿĐÅP şęřvęř yőū ŵįşĥ ŧő čőʼnʼnęčŧ ŧő.", + "label": "Ŝęřvęř ĥőşŧ", + "placeholder": "ęχämpľę: 127.0.0.1" + }, + "search_filter": { + "description": "ĿĐÅP şęäřčĥ ƒįľŧęř ūşęđ ŧő ľőčäŧę şpęčįƒįč ęʼnŧřįęş ŵįŧĥįʼn ŧĥę đįřęčŧőřy.", + "label": "Ŝęäřčĥ ƒįľŧęř*", + "placeholder": "ęχämpľę: čʼn=%ş" + }, + "search-base-dns": { + "description": "Åʼn äřřäy őƒ þäşę đʼnş ŧő şęäřčĥ ŧĥřőūģĥ; şępäřäŧę þy čőmmäş őř şpäčęş.", + "label": "Ŝęäřčĥ þäşę ĐŃŜ *", + "placeholder": "ęχämpľę: \"đč=ģřäƒäʼnä.đč=őřģ\"" + }, + "subtitle": "Ŧĥę ĿĐÅP įʼnŧęģřäŧįőʼn įʼn Ğřäƒäʼnä äľľőŵş yőūř Ğřäƒäʼnä ūşęřş ŧő ľőģ įʼn ŵįŧĥ ŧĥęįř ĿĐÅP čřęđęʼnŧįäľş. Fįʼnđ őūŧ mőřę įʼn őūř {đőčūmęʼnŧäŧįőʼn}.", + "title": "ßäşįč Ŝęŧŧįʼnģş" + }, "library-panel": { "add-modal": { "cancel": "Cäʼnčęľ",