Skip to content

Commit

Permalink
feat(inputs.ldap): Add LDAP input plugin supporting OpenLDAP and 389ds (
Browse files Browse the repository at this point in the history
  • Loading branch information
srebhan committed Oct 10, 2023
1 parent 474aff5 commit 0c1e213
Show file tree
Hide file tree
Showing 8 changed files with 1,106 additions and 0 deletions.
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

0 comments on commit 0c1e213

Please sign in to comment.