checker-ldap/checker/rules_bind.go

129 lines
4.6 KiB
Go

package checker
import (
"context"
"strconv"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rules in this file cover the optional bind-credentials workflow (driven
// by the bind_dn / bind_password / base_dn options) plus the cross-checker
// TLS-quality fold. Like the other rules, they read raw EndpointProbe
// fields and downstream observations directly.
// bindCredentialsRule: optional authenticated bind. Skipped when bind_dn
// isn't supplied, and reported as "not tested" when no encrypted endpoint
// was available to attempt it on.
type bindCredentialsRule struct{}
func (r *bindCredentialsRule) Name() string { return "ldap.bind_credentials" }
func (r *bindCredentialsRule) Description() string {
return "Verifies the supplied bind credentials are accepted by the directory (only runs when bind_dn is set)."
}
func (r *bindCredentialsRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
if optString(opts, "bind_dn") == "" {
return []sdk.CheckState{notTestedState("ldap.bind_credentials.skipped", "Authenticated bind not tested (no bind_dn supplied).")}
}
data, errSt := loadLDAPData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
var states []sdk.CheckState
attempted := false
for _, ep := range data.Endpoints {
if !ep.BindAttempted {
continue
}
attempted = true
if ep.BindOK {
states = append(states, infoState(
CodeBindOK,
"Bind on "+ep.Address+" succeeded with the provided credentials.",
ep.Address,
"",
))
} else {
states = append(states, critState(
CodeBindFailed,
"Bind on "+ep.Address+" failed: "+ep.BindError,
ep.Address,
"Verify the bind DN exists and the password is current. On AD, check the account is not locked/expired.",
))
}
}
if !attempted {
return []sdk.CheckState{notTestedState("ldap.bind_credentials.skipped", "Bind not attempted on any endpoint (no encrypted endpoint reachable).")}
}
if len(states) == 0 {
return []sdk.CheckState{passState("ldap.bind_credentials.ok", "Bind succeeded with the provided credentials.")}
}
return states
}
// baseDNReadRule: optional baseObject read on the supplied base_dn.
type baseDNReadRule struct{}
func (r *baseDNReadRule) Name() string { return "ldap.base_dn_read" }
func (r *baseDNReadRule) Description() string {
return "Verifies the bound account can read the supplied base DN (only runs when base_dn is set and bind succeeded)."
}
func (r *baseDNReadRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
if optString(opts, "base_dn") == "" {
return []sdk.CheckState{notTestedState("ldap.base_dn_read.skipped", "Base DN read not tested (no base_dn supplied).")}
}
data, errSt := loadLDAPData(ctx, obs)
if errSt != nil {
return []sdk.CheckState{*errSt}
}
var states []sdk.CheckState
attempted := false
for _, ep := range data.Endpoints {
if !ep.BaseReadAttempted {
continue
}
attempted = true
if ep.BaseReadOK {
states = append(states, infoState(
CodeBaseReadOK,
"Base DN read succeeded on "+ep.Address+" (entries="+strconv.Itoa(ep.BaseReadEntries)+").",
ep.Address,
"",
))
} else {
states = append(states, critState(
CodeBaseReadFailed,
"Bind succeeded but baseObject read on "+data.BaseDN+" failed: "+ep.BaseReadError,
ep.Address,
"Verify the bind DN has read access to the base DN -- typically granted via an ACL entry such as `access to dn.subtree=\"<base>\" by dn.exact=\"<bind>\" read`.",
))
}
}
if !attempted {
return []sdk.CheckState{notTestedState("ldap.base_dn_read.skipped", "Base DN read not attempted (bind did not succeed on any endpoint).")}
}
return states
}
// tlsQualityRule: folds downstream TLS checker findings onto the LDAP
// service. Consumes a related observation (not LDAPData).
type tlsQualityRule struct{}
func (r *tlsQualityRule) Name() string { return "ldap.tls_quality" }
func (r *tlsQualityRule) Description() string {
return "Folds the downstream TLS checker findings (certificate chain, hostname match, expiry) onto the LDAP service."
}
func (r *tlsQualityRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
related, _ := obs.GetRelated(ctx, TLSRelatedKey)
if len(related) == 0 {
return []sdk.CheckState{notTestedState("ldap.tls_quality.skipped", "No related TLS observation available (no TLS checker downstream, or no probe yet).")}
}
states := tlsStatesFromRelated(related)
if len(states) == 0 {
return []sdk.CheckState{passState("ldap.tls_quality.ok", "Downstream TLS checker reports no issues on the LDAP endpoints.")}
}
return states
}