Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs): Add LDAP input plugin supporting OpenLDAP and 389ds #13995

Merged
merged 2 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugins/inputs/all/ldap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !custom || inputs || inputs.ldap

package all

import _ "github.com/influxdata/telegraf/plugins/inputs/ldap" // register plugin
114 changes: 114 additions & 0 deletions plugins/inputs/ldap/389ds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package ldap

import (
"strconv"
"strings"
"time"

"github.com/go-ldap/ldap/v3"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
)

// Empty mappings are identity mappings
var attrMap389ds = map[string]string{
"addentryops": "add_operations",
"anonymousbinds": "anonymous_binds",
"bindsecurityerrors": "bind_security_errors",
"bytesrecv": "bytes_received",
"bytessent": "bytes_sent",
"cacheentries": "cache_entries",
"cachehits": "cache_hits",
"chainings": "",
"compareops": "compare_operations",
"connections": "",
"connectionsinmaxthreads": "connections_in_max_threads",
"connectionsmaxthreadscount": "connections_max_threads",
"copyentries": "copy_entries",
"currentconnections": "current_connections",
"currentconnectionsatmaxthreads": "current_connections_at_max_threads",
"dtablesize": "",
"entriesreturned": "entries_returned",
"entriessent": "entries_sent",
"errors": "",
"inops": "in_operations",
"listops": "list_operations",
"removeentryops": "delete_operations",
"masterentries": "master_entries",
"maxthreadsperconnhits": "maxthreads_per_conn_hits",
"modifyentryops": "modify_operations",
"modifyrdnops": "modrdn_operations",
"nbackends": "backends",
"onelevelsearchops": "onelevel_search_operations",
"opscompleted": "operations_completed",
"opsinitiated": "operations_initiated",
"readops": "read_operations",
"readwaiters": "read_waiters",
"referrals": "referrals",
"referralsreturned": "referrals_returned",
"searchops": "search_operations",
"securityerrors": "security_errors",
"simpleauthbinds": "simpleauth_binds",
"slavehits": "slave_hits",
"strongauthbinds": "strongauth_binds",
"threads": "",
"totalconnections": "total_connections",
"unauthbinds": "unauth_binds",
"wholesubtreesearchops": "wholesubtree_search_operations",
}

func (l *LDAP) new389dsConfig() []request {
attributes := make([]string, 0, len(attrMap389ds))
for k := range attrMap389ds {
attributes = append(attributes, k)
}

req := ldap.NewSearchRequest(
"cn=Monitor",
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
"(objectClass=*)",
attributes,
nil,
)
return []request{{req, l.convert389ds}}
}

func (l *LDAP) convert389ds(result *ldap.SearchResult, ts time.Time) []telegraf.Metric {
tags := map[string]string{
"server": l.host,
"port": l.port,
}
fields := make(map[string]interface{})
for _, entry := range result.Entries {
for _, attr := range entry.Attributes {
if len(attr.Values[0]) == 0 {
continue
}
// Map the attribute-name to the field-name
name := attrMap389ds[attr.Name]
if name == "" {
name = attr.Name
}
// Reverse the name if requested
if l.ReverseFieldNames {
parts := strings.Split(name, "_")
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
parts[i], parts[j] = parts[j], parts[i]
}
name = strings.Join(parts, "_")
}

// Convert the number
if v, err := strconv.ParseInt(attr.Values[0], 10, 64); err == nil {
fields[name] = v
}
}
}

m := metric.New("389ds", tags, fields, ts)
return []telegraf.Metric{m}
}
81 changes: 81 additions & 0 deletions plugins/inputs/ldap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# LDAP Input Plugin

This plugin gathers metrics from LDAP servers' monitoring (`cn=Monitor`)
backend. Currently this plugin supports [OpenLDAP](https://www.openldap.org/)
and [389ds](https://www.port389.org/) servers.

## Global configuration options <!-- @/docs/includes/plugin_config.md -->

In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.

[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins

## Configuration

```toml @sample.conf
# LDAP monitoring plugin
[[inputs.openldap]]
## Server to monitor
## The scheme determines the mode to use for connection with
## ldap://... -- unencrypted (non-TLS) connection
## ldaps://... -- TLS connection
## starttls://... -- StartTLS connection
## If no port is given, the default ports, 389 for ldap and starttls and
## 636 for ldaps, are used.
server = "ldap://localhost"

## Server dialect, can be "openldap" or "389ds"
# dialect = "openldap"

# DN and password to bind with
## If bind_dn is empty an anonymous bind is performed.
bind_dn = ""
bind_password = ""

## Reverse the field names constructed from the monitoring DN
# reverse_field_names = false

## Optional TLS Config
## Trusted root certificates for server
# tls_ca = "/path/to/cafile"
## Used for TLS client certificate authentication
# tls_cert = "/path/to/certfile"
## Used for TLS client certificate authentication
# tls_key = "/path/to/keyfile"
## Send the specified TLS server name via SNI
# tls_server_name = "kubernetes.example.com"
## Use TLS but skip chain & host verification
# insecure_skip_verify = false
```

To use this plugin you must enable the monitoring backend/plugin of your LDAP
server. See
[OpenLDAP](https://www.openldap.org/devel/admin/monitoringslapd.html) or 389ds
documentation for details.

## Metrics

Depending on the server dialect, different metrics are produced. The metrics
are usually named according to the selected dialect.

### Tags

- server -- Server name or IP
- port -- Port used for connecting

## Example Output

Using the `openldap` dialect

```text
openldap,server=localhost,port=389 operations_bind_initiated=10i,operations_unbind_initiated=6i,operations_modrdn_completed=0i,operations_delete_initiated=0i,operations_add_completed=2i,operations_delete_completed=0i,operations_abandon_completed=0i,statistics_entries=1516i,threads_open=2i,threads_active=1i,waiters_read=1i,operations_modify_completed=0i,operations_extended_initiated=4i,threads_pending=0i,operations_search_initiated=36i,operations_compare_initiated=0i,connections_max_file_descriptors=4096i,operations_modify_initiated=0i,operations_modrdn_initiated=0i,threads_max=16i,time_uptime=6017i,connections_total=1037i,connections_current=1i,operations_add_initiated=2i,statistics_bytes=162071i,operations_unbind_completed=6i,operations_abandon_initiated=0i,statistics_pdu=1566i,threads_max_pending=0i,threads_backload=1i,waiters_write=0i,operations_bind_completed=10i,operations_search_completed=35i,operations_compare_completed=0i,operations_extended_completed=4i,statistics_referrals=0i,threads_starting=0i 1516912070000000000
```

Using the `389ds` dialect

```text
389ds,port=32805,server=localhost add_operations=0i,anonymous_binds=0i,backends=0i,bind_security_errors=0i,bytes_received=0i,bytes_sent=256i,cache_entries=0i,cache_hits=0i,chainings=0i,compare_operations=0i,connections=1i,connections_in_max_threads=0i,connections_max_threads=0i,copy_entries=0i,current_connections=1i,current_connections_at_max_threads=0i,delete_operations=0i,dtablesize=63936i,entries_returned=2i,entries_sent=2i,errors=2i,in_operations=11i,list_operations=0i,maxthreads_per_conn_hits=0i,modify_operations=1i,modrdn_operations=0i,onelevel_search_operations=0i,operations_completed=10i,operations_initiated=11i,read_operations=0i,read_waiters=0i,referrals=0i,referrals_returned=0i,search_operations=3i,security_errors=0i,simpleauth_binds=1i,strongauth_binds=2i,threads=17i,total_connections=4i,unauth_binds=0i,wholesubtree_search_operations=1i 1695637234047087280
```
178 changes: 178 additions & 0 deletions plugins/inputs/ldap/ldap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//go:generate ../../../tools/readme_config_includer/generator
package ldap

import (
"crypto/tls"
_ "embed"
"fmt"
"net/url"
"time"

"github.com/go-ldap/ldap/v3"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
commontls "github.com/influxdata/telegraf/plugins/common/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)

//go:embed sample.conf
var sampleConfig string

type LDAP struct {
Server string `toml:"server"`
Dialect string `toml:"dialect"`
BindDn string `toml:"bind_dn"`
BindPassword config.Secret `toml:"bind_password"`
ReverseFieldNames bool `toml:"reverse_field_names"`
commontls.ClientConfig

tlsCfg *tls.Config
requests []request
mode string
host string
port string
}

type request struct {
query *ldap.SearchRequest
convert func(*ldap.SearchResult, time.Time) []telegraf.Metric
}

func (*LDAP) SampleConfig() string {
return sampleConfig
}

func (l *LDAP) Init() error {
if l.Server == "" {
l.Server = "ldap://localhost:389"
}

u, err := url.Parse(l.Server)
if err != nil {
return fmt.Errorf("parsing server failed: %w", err)
}

// Verify the server setting and set the defaults
var tlsEnable bool
switch u.Scheme {
case "ldap":
if u.Port() == "" {
u.Host = u.Host + ":389"
}
tlsEnable = false
case "starttls":
if u.Port() == "" {
u.Host = u.Host + ":389"
}
tlsEnable = true
case "ldaps":
if u.Port() == "" {
u.Host = u.Host + ":636"
}
tlsEnable = true
default:
return fmt.Errorf("invalid scheme: %q", u.Scheme)
}
l.mode = u.Scheme
l.Server = u.Host
l.host, l.port = u.Hostname(), u.Port()

// Force TLS depending on the selected mode
l.ClientConfig.Enable = &tlsEnable

// Setup TLS configuration
tlsCfg, err := l.ClientConfig.TLSConfig()
if err != nil {
return fmt.Errorf("creating TLS config failed: %w", err)
}
l.tlsCfg = tlsCfg

// Initialize the search request(s)
switch l.Dialect {
case "", "openldap":
l.requests = l.newOpenLDAPConfig()
case "389ds":
l.requests = l.new389dsConfig()
default:
return fmt.Errorf("invalid dialect %q", l.Dialect)
}

return nil
}

func (l *LDAP) Gather(acc telegraf.Accumulator) error {
// Connect
conn, err := l.connect()
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
defer conn.Close()

// Query the server
for _, req := range l.requests {
now := time.Now()
result, err := conn.Search(req.query)
if err != nil {
acc.AddError(err)
continue
}

// Collect metrics
for _, m := range req.convert(result, now) {
acc.AddMetric(m)
}
}

return nil
}

func (l *LDAP) connect() (*ldap.Conn, error) {
var conn *ldap.Conn
switch l.mode {
case "ldap":
var err error
conn, err = ldap.Dial("tcp", l.Server)
if err != nil {
return nil, err
}
case "ldaps":
var err error
conn, err = ldap.DialTLS("tcp", l.Server, l.tlsCfg)
if err != nil {
return nil, err
}
case "starttls":
var err error
conn, err = ldap.Dial("tcp", l.Server)
if err != nil {
return nil, err
}
if err := conn.StartTLS(l.tlsCfg); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid tls_mode: %s", l.mode)
}

if l.BindDn == "" && l.BindPassword.Empty() {
return conn, nil
}

// Bind username and password
passwd, err := l.BindPassword.Get()
if err != nil {
return nil, fmt.Errorf("getting password failed: %w", err)
}
defer passwd.Destroy()

if err := conn.Bind(l.BindDn, passwd.String()); err != nil {
return nil, fmt.Errorf("binding credentials failed: %w", err)
}

return conn, nil
}

func init() {
inputs.Add("ldap", func() telegraf.Input { return &LDAP{} })
}
Loading
Loading