freeipa-health-metrics/ldap_test.go

357 lines
9.6 KiB
Go
Raw Normal View History

2023-09-05 11:46:19 -05:00
package main
import (
"bufio"
"fmt"
"log"
"os"
"regexp"
"strings"
"testing"
"unicode"
"github.com/jimlambrt/gldap"
"github.com/prometheus/client_golang/prometheus/testutil"
)
// Setup global app variable with test config for tests.
func setupLdapTestApp() {
app = new(App)
app.flags = new(Flags)
app.flags.ConfigPath = "test/test_config.yaml"
app.ReadConfig()
app.ldapExporter = NewLDAPExporter()
}
// Base LDAP entry, using this as the library doesn't export variables that are useful
// for working the way I wanted with a generic parser.
type ldapEntry struct {
dn string
attributes map[string][]string
}
// Prints the ldap entry with all attributes in ldif format.
// Mainly used in debugging.
func (e *ldapEntry) Print() {
fmt.Printf("DN: %s\n", e.dn)
for name, attr := range e.attributes {
for _, v := range attr {
fmt.Printf("%s: %s\n", name, v)
}
}
}
// Parse ldif file and return all entries.
func ParseLDIF(ldifPath string) (res []*ldapEntry) {
// Open the file provided.
ldif, err := os.Open(ldifPath)
if err != nil {
log.Fatal("Error opening tests:", err)
}
defer ldif.Close()
// Basic variables used in parsing.
var dn, fullLine string
attributes := make(map[string][]string)
// Parsing handlers.
scanner := bufio.NewScanner(ldif)
scanner.Split(bufio.ScanLines)
parseRx := regexp.MustCompile(`([a-zA-Z0-9:]+):\s(.*)`)
// Check each line of the file amd parse.
for scanner.Scan() {
line := scanner.Text()
// Ignore comment and blank lines.
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Check if first chracter is a space.
isWrapped := false
for _, c := range line {
isWrapped = unicode.IsSpace(c)
break
}
// If this is wrapped from the last line, append and read next line.
if isWrapped {
// Remove leading spaces from line.
line = strings.TrimLeftFunc(line, unicode.IsSpace)
// Add this string to the full line.
fullLine += line
continue
}
// If the full line has data, parse it.
if fullLine != "" {
// Verify we can parse this line.
if !parseRx.MatchString(fullLine) {
log.Println("Unable to parse ldif line:", fullLine)
fullLine = line
continue
}
// Parse line.
match := parseRx.FindStringSubmatch(fullLine)
// If is a new entry, append the entry.
if match[1] == "dn" {
if dn != "" {
entry := &ldapEntry{
dn: dn,
attributes: attributes,
}
res = append(res, entry)
}
// Clear attributes and change the DN to the newly discovered entry.
attributes = make(map[string][]string)
dn = match[2]
} else {
// This is an attribute, lets add it.
_, ok := attributes[match[1]]
if !ok {
attributes[match[1]] = []string{
match[2],
}
} else {
attributes[match[1]] = append(attributes[match[1]], match[2])
}
}
}
// This is a new line, could have additional lines to add with LDIF wrapping.
fullLine = line
}
// If the full line has data, parse it.
if fullLine != "" {
// Verify we can parse this line.
if !parseRx.MatchString(fullLine) {
log.Println("Unable to parse ldif line:", fullLine)
return
}
// Parse line.
match := parseRx.FindStringSubmatch(fullLine)
// If is a new entry, append the entry.
if match[1] != "dn" {
// This is an attribute, lets add it.
_, ok := attributes[match[1]]
if !ok {
attributes[match[1]] = []string{
match[2],
}
} else {
attributes[match[1]] = append(attributes[match[1]], match[2])
}
}
}
// As this is the end, we need to create the last decoded entry.
if dn != "" {
entry := &ldapEntry{
dn: dn,
attributes: attributes,
}
res = append(res, entry)
}
return
}
const ldapPort = 10389
// LDAP test server.
type LDAPTestServer struct {
server *gldap.Server
responses map[string]string
}
func NewLDAPTestServer() *LDAPTestServer {
s := new(LDAPTestServer)
// Requested DN to response ldif file map.
s.responses = map[string]string{
"cn=users,cn=accounts,dc=example,dc=com": "test/ldap_user_sub.ldif",
"cn=staged users,cn=accounts,cn=provisioning,dc=example,dc=com": "test/ldap_stagged_sub.ldif",
"cn=deleted users,cn=accounts,cn=provisioning,dc=example,dc=com": "test/ldap_deleted_sub.ldif",
"cn=groups,cn=accounts,dc=example,dc=com": "test/ldap_groups.ldif",
"cn=computers,cn=accounts,dc=example,dc=com": "test/ldap_computers.ldif",
"cn=services,cn=accounts,dc=example,dc=com": "test/ldap_services.ldif",
"cn=ng,cn=alt,dc=example,dc=com": "test/ldap_netgroups.ldif",
"cn=hostgroups,cn=accounts,dc=example,dc=com": "test/ldap_hostgroups.ldif",
"cn=hbac,dc=example,dc=com": "test/ldap_hbac.ldif",
"cn=sudorules,cn=sudo,dc=example,dc=com": "test/ldap_sudo.ldif",
"cn=masters,cn=ipa,cn=etc,dc=example,dc=com": "test/ldap_masters.ldif",
"cn=mapping tree,cn=config": "test/ldap_mapping_tree.ldif",
}
return s
}
// Simple ldap bind to verify authentication with the ldap metrics work.
func (s *LDAPTestServer) bindHandler(w *gldap.ResponseWriter, r *gldap.Request) {
// Setup invalid response which will be sent unless the response code is changed by a successful login.
resp := r.NewBindResponse(
gldap.WithResponseCode(gldap.ResultInvalidCredentials),
)
// Send response at the end of the function call.
defer func() {
w.Write(resp)
}()
// Decode bind message from request.
m, err := r.GetSimpleBindMessage()
if err != nil {
log.Printf("not a simple bind message: %s", err)
return
}
// If credentials match config, return success.
if m.UserName == app.config.LDAP.BindDN && string(m.Password) == app.config.LDAP.BindPassword {
resp.SetResultCode(gldap.ResultSuccess)
log.Println("bind success")
return
}
}
// Write LDIF entries from file to LDAP request.
func (s *LDAPTestServer) writeLdif(w *gldap.ResponseWriter, r *gldap.Request, ldifPath string) {
// Parse entries.
entries := ParseLDIF(ldifPath)
// For each entry, write it to the request.
for _, entry := range entries {
// Print debug info.
// entry.Print()
// Make a response entry for this request and write it.
w.Write(r.NewSearchResponseEntry(entry.dn, gldap.WithAttributes(entry.attributes)))
}
}
// Handle LDAP search requests.
func (s *LDAPTestServer) searchHandler(w *gldap.ResponseWriter, r *gldap.Request) {
// Setup general response.
resp := r.NewSearchDoneResponse()
defer func() {
w.Write(resp)
}()
// Get message from request.
m, err := r.GetSearchMessage()
if err != nil {
log.Printf("not a search message: %s", err)
return
}
// Print debug info.
// log.Printf("search base dn: %s", m.BaseDN)
// log.Printf("search scope: %d", m.Scope)
// log.Printf("search filter: %s", m.Filter)
// log.Printf("search attributes: %v", m.Attributes)
// Send test ldif response based on request DN.
ldifFile, ok := s.responses[m.BaseDN]
if ok {
s.writeLdif(w, r, ldifFile)
resp.SetResultCode(gldap.ResultSuccess)
}
}
// Helper to start and wait for server to be running.
func (s *LDAPTestServer) Run() {
go s.Start()
for s.server == nil || !s.server.Ready() {
}
}
// Helper to stop LDAP server.
func (s *LDAPTestServer) Stop() {
if s.server != nil {
s.server.Stop()
}
}
// Setup LDAP test server for verifying metrics.
func (s *LDAPTestServer) Start() {
server, err := gldap.NewServer()
if err != nil {
log.Fatalf("unable to create server: %s", err.Error())
}
// Set global variable for test function access.
s.server = server
// create a router and add a bind handler
r, err := gldap.NewMux()
if err != nil {
log.Fatalf("unable to create router: %s", err.Error())
}
r.Bind(s.bindHandler)
r.Search(s.searchHandler)
server.Router(r)
// Run the LDAP test server.
server.Run(fmt.Sprintf("127.0.0.1:%d", ldapPort))
}
// Main LDAP test function that verifies metrics for LDAP works.
func TestLdap(t *testing.T) {
// Setup configs.
setupLdapTestApp()
// Run the LDAP test server.
server := NewLDAPTestServer()
server.Run()
// Open the expected prometheus metrics.
expected, err := os.Open("test/ldap.metrics")
if err != nil {
t.Fatal("Error opening tests:", err)
}
defer expected.Close()
// Test the LDAP exporter and verify metrics match what's expected.
err = testutil.CollectAndCompare(app.ldapExporter, expected)
// If results are not as expected, fail test with the error.
if err != nil {
t.Fatal("Unexpected metrics returned:", err)
}
// Remove all responses from ldap server to cause failure in all metrics.
server.responses = nil
// Open the expected prometheus metrics.
expected, err = os.Open("test/ldap_fail.metrics")
if err != nil {
t.Fatal("Error opening tests:", err)
}
defer expected.Close()
// Test the LDAP exporter and verify metrics match what's expected.
err = testutil.CollectAndCompare(app.ldapExporter, expected)
// If results are not as expected, fail test with the error.
if err != nil {
t.Fatal("Unexpected metrics returned:", err)
}
// Test failure to connect.
app.config.LDAP.Address = "bad-address"
// Open the expected prometheus metrics.
expected, err = os.Open("test/ldap_fail_connect.metrics")
if err != nil {
t.Fatal("Error opening tests:", err)
}
defer expected.Close()
// Test the LDAP exporter and verify metrics match what's expected.
err = testutil.CollectAndCompare(app.ldapExporter, expected)
// If results are not as expected, fail test with the error.
if err != nil {
t.Fatal("Unexpected metrics returned:", err)
}
// We're done, let's stop serving the test LDAP server.
server.Stop()
}