ldap

package module
v0.0.0-...-d90a141 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

go-ruby-ldap/ldap

ldap — go-ruby-ldap

Docs License Go Coverage

A pure-Go (no cgo), MRI-faithful reimplementation of the Ruby net-ldap gem's Net::LDAP client surface — the ergonomics and result/error model of the gem's connection object (bind, search, add, modify, delete, rename, Net::LDAP::Filter, Net::LDAP::Entry, get_operation_result) layered over the official pure-Go LDAP client.

It does not reimplement the LDAP protocol. It consumes github.com/go-ldap/ldap/v3 as its transport and maps the gem's API onto it, so a static, CGO=0 binary talks to a real directory (OpenLDAP, Active Directory, 389 Directory Server, …).

It is the LDAP backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-etcd and go-ruby-redis.

Transport is a host seam. A Client drives an injected transport whose method set is satisfied directly by *ldap.Conn (no adapter). This makes every method's request-building and response-mapping logic testable against a deterministic in-memory transport — no external directory, no cgo — so the suite holds 100% coverage on every arch under qemu. A separate live suite drives an in-process, pure-Go LDAP server for real round-trip validation on native lanes.

Features

  • BindBind (simple / anonymous, from the configured credentials) and BindWith (per-call credentials); a failed bind records ErrInvalidCredentials rather than raising.
  • SearchSearch and SearchEach (the block form) over ScopeBase, ScopeSingleLevel and ScopeSubtree, with attribute selection, size / time limits and a base falling back to the client's configured base.
  • Entry — a case-insensitive Entry mirroring Net::LDAP::Entry: DN, Get, First, AttributeNames.
  • Filter — a Filter builder mirroring Net::LDAP::Filter: Eq, Present, Ge, Le, Approx, Contains / Begins / Ends (substrings), And / Or / Not, and Construct (RFC 4515 string parsing), with RFC 4515 value escaping.
  • ModifyAdd, Modify (ModAdd / ModReplace / ModDelete operations), ReplaceAttribute / AddAttribute / DeleteAttribute, Delete, Rename (modify RDN, with an optional new superior) and Compare.
  • Result — an OperationResult mirroring #get_operation_result (code, name, message, matched DN), updated by every operation.
  • Errors — a net-ldap-style error tree (Error + one sentinel per LDAP result code, plus synthetic network / filter-syntax codes) matchable with errors.Is.

Usage

c, err := ldap.New(ldap.Config{
	Host:     "127.0.0.1",
	Port:     389,
	Base:     "dc=example,dc=com",
	Method:   "simple",
	Username: "cn=admin,dc=example,dc=com",
	Password: "secret",
})
if err != nil {
	log.Fatal(err)
}
defer c.Close()

if err := c.Bind(); err != nil {
	log.Fatalf("bind: %v (%+v)", err, c.OperationResult())
}

res, _ := c.Search(&ldap.SearchRequest{
	Scope:  ldap.ScopeSubtree,
	Filter: ldap.And(ldap.Eq("objectClass", "person"), ldap.Begins("cn", "al")),
})
for _, e := range res.Entries {
	fmt.Println(e.DN(), e.First("mail"))
}

Ruby mapping

net-ldap gem go-ruby-ldap/ldap
Net::LDAP.new(host:, port:, base:, ...) ldap.New(ldap.Config{...})
ldap.bind c.Bind()
ldap.search(filter:, base:) { |e| } c.Search(&ldap.SearchRequest{...}) / SearchEach
ldap.add(dn:, attributes:) c.Add(dn, attrs)
ldap.modify(dn:, operations:) c.Modify(dn, ops)
ldap.delete(dn:) c.Delete(dn)
ldap.rename(olddn:, newrdn:) c.Rename(olddn, newrdn, true, "")
Net::LDAP::Filter.eq("cn", "a") ldap.Eq("cn", "a")
Net::LDAP::Filter.construct(str) ldap.Construct(str)
ldap.get_operation_result c.OperationResult()

Tests & coverage

The default suite runs with -race and holds 100% statement coverage on all three host OSes and the six supported 64-bit architectures (amd64, arm64, riscv64, loong64, ppc64le and big-endian s390x), driving the full client logic against a deterministic in-memory transport with no external directory:

go test -race -cover ./...

The live/ nested module validates real round-trip behaviour against an in-process, pure-Go LDAP server (native-only; kept out of the main module so its server dependency never enters this go.mod):

cd live && go test ./...

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-ruby-ldap/ldap authors.

Documentation

Overview

Package ldap is a pure-Go (CGO=0), MRI-faithful reimplementation of the Ruby net-ldap gem's Net::LDAP client surface.

It does not reimplement the LDAP protocol. It consumes the official pure-Go client github.com/go-ldap/ldap/v3 as its transport and layers the ergonomics and result/error model of net-ldap on top: Client.Bind, Search (base / single level / whole subtree scopes), Add, Modify (add / replace / delete operations), Delete, Rename (modify RDN), a Filter builder mirroring Net::LDAP::Filter (eq / present / substrings / ge / le / and / or / not and a Construct string parser), an Entry with case-insensitive attribute access mirroring Net::LDAP::Entry, and an OperationResult mirroring Net::LDAP#get_operation_result.

Transport is a host seam

A Client drives an injected [transport] whose method set is satisfied directly by *ldap.Conn (no adapter). This makes every method's request- building and response-mapping logic testable against a deterministic in-memory transport with no external directory and no cgo, so the suite reaches 100% coverage on every arch under qemu. A separate live suite (the nested ./live module) drives an in-process pure-Go LDAP server for real round-trip validation on native lanes.

Ruby mapping

Net::LDAP.new(host:, port:, base:, auth:)  =>  ldap.New(ldap.Config{...})
ldap.bind                                  =>  c.Bind()
ldap.search(filter:, base:, &block)        =>  c.Search(&ldap.SearchRequest{...})
ldap.add(dn:, attributes:)                 =>  c.Add(dn, attrs)
ldap.modify(dn:, operations:)              =>  c.Modify(dn, ops)
ldap.delete(dn:)                           =>  c.Delete(dn)
ldap.rename(olddn:, newrdn:)               =>  c.Rename(olddn, newrdn, true, "")
Net::LDAP::Filter.eq("cn", "a")            =>  ldap.Eq("cn", "a")
Net::LDAP::Filter.construct("(cn=a)")      =>  ldap.Construct("(cn=a)")
ldap.get_operation_result                  =>  c.OperationResult()

Index

Constants

View Source
const (
	// CodeNetwork is reported when the connection to the directory fails.
	CodeNetwork uint16 = 200
	// CodeFilterCompile is reported when a filter string will not compile.
	CodeFilterCompile uint16 = 201
)

Synthetic client-side codes, above the LDAP protocol range, for errors that never come from the directory: a connection failure and a filter that would not compile. They mirror the go-ldap client-error codes.

Variables

View Source
var (
	ErrOperationsError     = &Error{Code: goldap.LDAPResultOperationsError, Name: "OperationsError"}
	ErrProtocolError       = &Error{Code: goldap.LDAPResultProtocolError, Name: "ProtocolError"}
	ErrTimeLimitExceeded   = &Error{Code: goldap.LDAPResultTimeLimitExceeded, Name: "TimeLimitExceeded"}
	ErrSizeLimitExceeded   = &Error{Code: goldap.LDAPResultSizeLimitExceeded, Name: "SizeLimitExceeded"}
	ErrAuthMethodNotSupp   = &Error{Code: goldap.LDAPResultAuthMethodNotSupported, Name: "AuthMethodNotSupported"}
	ErrStrongAuthRequired  = &Error{Code: goldap.LDAPResultStrongAuthRequired, Name: "StrongAuthRequired"}
	ErrNoSuchAttribute     = &Error{Code: goldap.LDAPResultNoSuchAttribute, Name: "NoSuchAttribute"}
	ErrConstraintViolation = &Error{Code: goldap.LDAPResultConstraintViolation, Name: "ConstraintViolation"}
	ErrAttributeExists     = &Error{Code: goldap.LDAPResultAttributeOrValueExists, Name: "AttributeOrValueExists"}
	ErrInvalidSyntax       = &Error{Code: goldap.LDAPResultInvalidAttributeSyntax, Name: "InvalidAttributeSyntax"}
	ErrNoSuchObject        = &Error{Code: goldap.LDAPResultNoSuchObject, Name: "NoSuchObject"}
	ErrInvalidDNSyntax     = &Error{Code: goldap.LDAPResultInvalidDNSyntax, Name: "InvalidDNSyntax"}
	ErrInappropriateAuth   = &Error{Code: goldap.LDAPResultInappropriateAuthentication, Name: "InappropriateAuthentication"}
	ErrInvalidCredentials  = &Error{Code: goldap.LDAPResultInvalidCredentials, Name: "InvalidCredentials"}
	ErrInsufficientAccess  = &Error{Code: goldap.LDAPResultInsufficientAccessRights, Name: "InsufficientAccessRights"}
	ErrBusy                = &Error{Code: goldap.LDAPResultBusy, Name: "Busy"}
	ErrUnavailable         = &Error{Code: goldap.LDAPResultUnavailable, Name: "Unavailable"}
	ErrUnwillingToPerform  = &Error{Code: goldap.LDAPResultUnwillingToPerform, Name: "UnwillingToPerform"}
	ErrNamingViolation     = &Error{Code: goldap.LDAPResultNamingViolation, Name: "NamingViolation"}
	ErrObjectClassViol     = &Error{Code: goldap.LDAPResultObjectClassViolation, Name: "ObjectClassViolation"}
	ErrNotAllowedOnNonLeaf = &Error{Code: goldap.LDAPResultNotAllowedOnNonLeaf, Name: "NotAllowedOnNonLeaf"}
	ErrNotAllowedOnRDN     = &Error{Code: goldap.LDAPResultNotAllowedOnRDN, Name: "NotAllowedOnRDN"}
	ErrEntryAlreadyExists  = &Error{Code: goldap.LDAPResultEntryAlreadyExists, Name: "EntryAlreadyExists"}
	ErrOther               = &Error{Code: goldap.LDAPResultOther, Name: "Other"}
	// Client-side, non-protocol errors.
	ErrNetwork       = &Error{Code: CodeNetwork, Name: "Network"}
	ErrFilterCompile = &Error{Code: CodeFilterCompile, Name: "FilterSyntax"}
)

The net-ldap error tree: one sentinel per result code this package raises. Compare with errors.Is, e.g. errors.Is(err, ldap.ErrNoSuchObject).

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a Net::LDAP connection bound to a transport seam. It mirrors the net-ldap connection object: it builds requests, drives them over the transport, records each operation's result (see Client.OperationResult) and maps replies to net-ldap's value and error model. Construct one with New; the zero value is not usable.

func New

func New(cfg Config) (*Client, error)

New connects to the directory using cfg and returns a Client. It dials the connection (failing with ErrNetwork when the directory is unreachable) but does not bind: call Client.Bind to authenticate, mirroring net-ldap where the connection opens lazily and #bind authenticates.

func (*Client) Add

func (c *Client) Add(dn string, attributes map[string][]string) error

Add creates the entry dn with the given attributes, mirroring Net::LDAP#add(dn:, attributes:). attributes maps each attribute name to its values. It records the operation result and returns ErrEntryAlreadyExists when dn already exists.

func (*Client) AddAttribute

func (c *Client) AddAttribute(dn, attr string, values []string) error

AddAttribute adds values to attr on dn, mirroring Net::LDAP#add_attribute.

func (*Client) Base

func (c *Client) Base() string

Base returns the default search base the client was configured with.

func (*Client) Bind

func (c *Client) Bind() error

Bind authenticates the connection with the configured credentials. A "simple" method performs a simple bind with the username and password; an "anonymous" method performs an unauthenticated bind with the username. It mirrors net-ldap's #bind and records the result, so a failed bind leaves an ErrInvalidCredentials (or other) result on the client.

func (*Client) BindWith

func (c *Client) BindWith(username, password string) error

BindWith authenticates with an explicit username and password (a simple bind), mirroring net-ldap's #bind(method: :simple, username:, password:) with per-call credentials.

func (*Client) Close

func (c *Client) Close() error

Close releases the connection's resources, mirroring net-ldap closing the connection.

func (*Client) Compare

func (c *Client) Compare(dn, attr, value string) (bool, error)

Compare reports whether dn's attr has the given value, mirroring Net::LDAP#compare. It records the operation result; a false comparison is not an error.

func (*Client) Delete

func (c *Client) Delete(dn string) error

Delete removes the entry dn, mirroring Net::LDAP#delete(dn:). It records the operation result and returns ErrNoSuchObject when dn does not exist.

func (*Client) DeleteAttribute

func (c *Client) DeleteAttribute(dn, attr string) error

DeleteAttribute deletes attr from dn, mirroring Net::LDAP#delete_attribute.

func (*Client) Modify

func (c *Client) Modify(dn string, ops []ModifyOp) error

Modify applies ops to the entry dn, mirroring Net::LDAP#modify(dn:, operations:). Each op adds, replaces or deletes an attribute's values. It records the operation result.

func (*Client) OperationResult

func (c *Client) OperationResult() OperationResult

OperationResult returns the result of the last operation the client performed, mirroring Net::LDAP#get_operation_result. It is a fresh Success before any operation.

func (*Client) Rename

func (c *Client) Rename(dn, newRDN string, deleteOld bool, newSuperior string) error

Rename changes an entry's relative distinguished name, mirroring Net::LDAP#rename / #modify_rdn(olddn:, newrdn:, delete_attributes:, new_superior:). deleteOld removes the old RDN attribute value; newSuperior, if non-empty, moves the entry under a new parent. It records the operation result.

func (*Client) ReplaceAttribute

func (c *Client) ReplaceAttribute(dn, attr string, values []string) error

ReplaceAttribute replaces attr's values on dn, the common single-attribute case of Net::LDAP#replace_attribute.

func (*Client) Search

func (c *Client) Search(req *SearchRequest) (*SearchResult, error)

Search runs an LDAP search and returns the matched entries. It mirrors Net::LDAP#search: an empty req.Base falls back to the client's configured base, the zero filter matches every object, and the result records the operation outcome (see Client.OperationResult). A no-such-object base returns ErrNoSuchObject.

func (*Client) SearchEach

func (c *Client) SearchEach(req *SearchRequest, fn func(*Entry)) (int, error)

SearchEach runs a search and invokes fn with each matched entry, mirroring the block form Net::LDAP#search(...) { |entry| ... }. It returns the number of entries delivered.

type Config

type Config struct {
	// Host is the directory host. Defaults to "127.0.0.1" when empty.
	Host string
	// Port is the directory port. Defaults to 389 (or 636 when TLS is set).
	Port int
	// Base is the default search base DN used when a SearchRequest omits one.
	Base string
	// Method is the bind method; only "simple" (the default) and "anonymous"
	// are supported. "anonymous" performs an unauthenticated bind.
	Method string
	// Username and Password are the simple-bind credentials.
	Username string
	Password string
	// TLS, when set, connects with ldaps:// (LDAP over TLS). nil means plaintext
	// ldap://.
	TLS *tls.Config
	// URL, when set, overrides Host/Port/TLS with an explicit ldap:// or
	// ldaps:// URL, mirroring net-ldap's acceptance of a full URI.
	URL string
}

Config configures a Client. The fields mirror the keywords Net::LDAP.new accepts: the directory host and port, the default search base, the simple-bind credentials (auth: {method: :simple, username:, password:}) and, when the connection is encrypted, a TLS config (encryption: :simple_tls).

type Entry

type Entry struct {
	// contains filtered or unexported fields
}

Entry mirrors Net::LDAP::Entry: a distinguished name and its attributes, with case-insensitive attribute access (LDAP attribute names are case-insensitive).

func (*Entry) AttributeNames

func (e *Entry) AttributeNames() []string

AttributeNames returns the entry's attribute names in their original casing, sorted, mirroring Net::LDAP::Entry#attribute_names.

func (*Entry) DN

func (e *Entry) DN() string

DN returns the entry's distinguished name, mirroring Net::LDAP::Entry#dn.

func (*Entry) First

func (e *Entry) First(attr string) string

First returns the first value of attr (case-insensitive), or "" when the entry has no such attribute, mirroring the common entry[:cn].first idiom.

func (*Entry) Get

func (e *Entry) Get(attr string) []string

Get returns the values of attr (case-insensitive), or nil when the entry has no such attribute. It mirrors Net::LDAP::Entry#[] / #attribute, which are case-insensitive and always return an array.

type Error

type Error struct {
	// Code is the LDAP result code the operation reported.
	Code uint16
	// Name is the net-ldap error-class name for Code (e.g. "NoSuchObject").
	Name string
	// Message is the human-readable detail.
	Message string
	// contains filtered or unexported fields
}

Error is the base of the net-ldap error tree. It carries the LDAP result code the directory returned (or a synthetic client-side code) and mirrors the net-ldap gem, whose exceptions map onto LDAP result codes. Match a specific kind with errors.Is against one of the exported sentinels (for example ErrNoSuchObject); the match is by code, so a wrapped Error compares equal to its sentinel.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Is

func (e *Error) Is(target error) bool

Is reports whether target is an *Error with the same result code, so the exported sentinels match any Error of the same kind regardless of message.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying transport error for errors.Unwrap.

type Filter

type Filter struct {
	// contains filtered or unexported fields
}

Filter is an RFC 4515 search filter, mirroring Net::LDAP::Filter. It wraps the filter's parenthesised string form and composes with And, Or and Not. Build a leaf with Eq, Present, Ge, Le, Contains, Begins, Ends or Approx; parse one from a string with Construct. Attribute values are escaped per RFC 4515, so a value containing "(", ")", "*" or "\" is safe.

func And

func And(filters ...Filter) Filter

And joins filters with a logical AND (&(f1)(f2)...), mirroring Net::LDAP::Filter#& and Filter.join. A single filter is returned unchanged and an empty call yields the zero Filter.

func Approx

func Approx(attr, value string) Filter

Approx builds an approximate-match filter (attr~=value), mirroring Net::LDAP::Filter with the :approx operator.

func Begins

func Begins(attr, value string) Filter

Begins builds a substring filter matching values beginning with value (attr=value*), mirroring Net::LDAP::Filter.begins.

func Construct

func Construct(s string) (Filter, error)

Construct parses an RFC 4515 filter string into a Filter, mirroring Net::LDAP::Filter.construct / from_rfc2254. It validates the string by compiling it, returning ErrFilterCompile wrapping the compile error when the string is not a valid filter.

func Contains

func Contains(attr, value string) Filter

Contains builds a substring filter matching values containing value (attr=*value*), mirroring Net::LDAP::Filter.contains.

func Ends

func Ends(attr, value string) Filter

Ends builds a substring filter matching values ending with value (attr=*value), mirroring Net::LDAP::Filter.ends.

func Eq

func Eq(attr, value string) Filter

Eq builds an equality filter (attr=value), mirroring Net::LDAP::Filter.eq. A value of "*" is preserved so Eq(attr, "*") is the presence filter, matching net-ldap.

func Ge

func Ge(attr, value string) Filter

Ge builds a greater-or-equal filter (attr>=value), mirroring Net::LDAP::Filter.ge.

func Le

func Le(attr, value string) Filter

Le builds a less-or-equal filter (attr<=value), mirroring Net::LDAP::Filter.le.

func Not

func Not(f Filter) Filter

Not negates a filter (!(f)), mirroring Net::LDAP::Filter#~ and Filter.negate.

func Or

func Or(filters ...Filter) Filter

Or joins filters with a logical OR (|(f1)(f2)...), mirroring Net::LDAP::Filter#| and Filter.intersect.

func Present

func Present(attr string) Filter

Present builds a presence filter (attr=*), mirroring Net::LDAP::Filter.present / .pres.

func (Filter) String

func (f Filter) String() string

String returns the filter's RFC 4515 parenthesised string form, mirroring Net::LDAP::Filter#to_s. The zero Filter renders as the present-everything filter "(objectClass=*)", matching net-ldap's default.

type ModType

type ModType int

ModType is the kind of change a ModifyOp applies to an attribute, mirroring the operation symbols of Net::LDAP#modify (:add, :replace, :delete).

const (
	// ModAdd adds values to an attribute (the :add operation).
	ModAdd ModType = iota
	// ModReplace replaces an attribute's values (the :replace operation).
	ModReplace
	// ModDelete deletes values from an attribute, or the whole attribute when
	// Values is empty (the :delete operation).
	ModDelete
)

type ModifyOp

type ModifyOp struct {
	Type   ModType
	Attr   string
	Values []string
}

ModifyOp is one change within a Client.Modify, mirroring an element of the operations: array net-ldap passes to #modify: [type, attribute, values].

type OperationResult

type OperationResult struct {
	Code      uint16
	Name      string
	Message   string
	MatchedDN string
}

OperationResult mirrors Net::LDAP#get_operation_result: the LDAP result code, its net-ldap name, the human-readable message and the matched DN of the last operation a Client performed. Code 0 (Success) means the last operation succeeded.

type Scope

type Scope int

Scope is a search scope, mirroring the values Net::LDAP#search accepts for its scope: keyword.

const (
	// ScopeBase searches only the base object (Net::LDAP::SearchScope_BaseObject).
	ScopeBase Scope = Scope(goldap.ScopeBaseObject)
	// ScopeSingleLevel searches the base's immediate children
	// (Net::LDAP::SearchScope_SingleLevel).
	ScopeSingleLevel Scope = Scope(goldap.ScopeSingleLevel)
	// ScopeSubtree searches the base and its whole subtree, the net-ldap default
	// (Net::LDAP::SearchScope_WholeSubtree).
	ScopeSubtree Scope = Scope(goldap.ScopeWholeSubtree)
)

type SearchRequest

type SearchRequest struct {
	// Base is the search base DN; empty uses the client's configured Base.
	Base string
	// Scope is the search scope; the zero value ScopeBase is net-ldap's
	// SearchScope_BaseObject. Callers commonly set ScopeSubtree.
	Scope Scope
	// Filter is the search filter; the zero Filter is "(objectClass=*)".
	Filter Filter
	// Attributes lists the attributes to return; empty returns all user
	// attributes.
	Attributes []string
	// SizeLimit caps the number of entries returned; 0 means no limit.
	SizeLimit int
	// TimeLimit caps the server-side search time in seconds; 0 means no limit.
	TimeLimit int
	// TypesOnly asks the directory to return attribute names without values.
	TypesOnly bool
}

SearchRequest describes a search, mirroring the keywords of Net::LDAP#search.

type SearchResult

type SearchResult struct {
	Entries []*Entry
}

SearchResult is the reply to Search: the matched entries. It mirrors the array of Net::LDAP::Entry that Net::LDAP#search returns.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL