ldap

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: BSD-3-Clause Imports: 13 Imported by: 0

README

go-authn/ldap

A library, not a daemon. It is the LDAP protocol half — the wire, the filter, the operations — that go-authn/authnd runs. authnd stays the server: it owns the configuration, the directory sources, the bind policy and the MFA. This owns the bytes.

The same split as go-filesystems/nfs under go-fileshare/fileshare, and go-authn/kdc under authnd. There is no second server here, and authnd never had an LDAP implementation of its own to merge — it had zero lines of wire code and rented the protocol from a fork of glauth/ldap. This replaces the rental.

Written from RFC 4511, 4513, 4515 and 7628. No cgo, no client, no global state.

It exists because go-authn/authnd was built on a fork of glauth/ldap, and six defects turned up in the parts of it that authnd actually used — five in the fifty lines of the bind path alone. The one that decided this: (uid=svc-*-prod) was evaluated as (uid=svc-*) and returned svc-web-stage. In a directory, answering a broader question than the one you were asked is disclosure, and it is silent, because every entry returned is a real entry.

What is different here

One filter, three renderings. The library this replaces had three separate implementations — decode-to-string, encode-from-string, and match-against-wire — and they drifted apart. Here a filter is parsed or decoded once into a tree, and String, EncodeFilter and Matches all read that tree. They cannot disagree about what a filter means, because only one thing means it. The tests assert that as a property: a filter must answer the same about every entry in a corpus after a round trip through the string form and through the wire.

Types that carry the protocol. Result has MatchedDN and Diagnostic as fields, because RFC 4511 4.1.9 says every result does. A server that cannot set MatchedDN cannot tell a client how far along a DN its search base stopped existing — the difference between "you have a typo in ou=" and "that whole tree is gone".

Writes that can be written correctly. ModifyRequest.Changes is an ordered list, because RFC 4511 4.6 says the modifications are "performed in the order listed". Held as three buckets — add, delete, replace — the requests

delete member=alice; add member=alice     (alice stays)
add member=alice; delete member=alice     (alice goes)

arrive as the same thing, and the server cannot tell which was asked. That is how a client reconciles a group membership.

Bounded before it is trusted. The frame length is read here rather than left to the BER library, whose limit is a package-level global that two servers in one process cannot set differently. A five-byte header claiming four gigabytes is refused before anything is reserved for it, and filter nesting is bounded because a stack overflow in Go kills the process — one unauthenticated packet must not take the directory down.

Status

The server answers binds (simple and SASL), searches, compares, extended operations, StartTLS, abandon and all four writes, at 100% coverage.

Still to come: the root DSE, the paged-results control (RFC 2696), and the read controls (RFC 4527) that let a client see what its write actually did.

OpenLDAP's own ldapsearch is the judge in CI. A server tested only by a client from this same module can agree with it about a misreading of the protocol and both be wrong — which is exactly how the substring bug went unnoticed.

Licence

BSD-3-Clause.

Documentation

Index

Constants

View Source
const (
	// OIDPaging is the simple paged results control (RFC 2696).
	OIDPaging = "1.2.840.113556.1.4.319"
	// OIDManageDsaIT says to act on an alias or referral entry itself rather
	// than following it (RFC 3296).
	OIDManageDsaIT = "2.16.840.1.113730.3.4.2"
	// OIDPreRead and OIDPostRead return the entry as it was before, or as it
	// became after, a write (RFC 4527). They are how a client learns what it
	// actually wrote.
	OIDPreRead  = "1.3.6.1.1.13.1"
	OIDPostRead = "1.3.6.1.1.13.2"
)

The controls this server knows by name.

View Source
const (
	// OIDStartTLS is RFC 4511 4.14.
	OIDStartTLS = "1.3.6.1.4.1.1466.20037"
	// OIDWhoAmI is RFC 4532: "who does this connection say I am".
	OIDWhoAmI = "1.3.6.1.4.1.4203.1.11.3"
	// OIDPasswordModify is RFC 3062, the one write every directory is
	// expected to offer even when it offers no other.
	OIDPasswordModify = "1.3.6.1.4.1.4203.1.11.1"
	// OIDCancel is RFC 3909, an abandon that gets an answer.
	OIDCancel = "1.3.6.1.1.8"
)

The extended operations named in this package.

View Source
const DefaultMaxMessageSize = 4 << 20 // 4 MiB

DefaultMaxMessageSize is how large one LDAPMessage may be.

It is generous enough for a search result carrying a certificate and small enough that a client cannot ask this process to allocate its way out of memory. A deployment that publishes something larger raises it on the Server.

Variables

This section is empty.

Functions

func EncodeFilter

func EncodeFilter(f Filter) (*ber.Packet, error)

EncodeFilter writes a Filter as its BER packet.

func EqualDN

func EqualDN(a, b string) bool

EqualDN reports whether two DNs name the same entry.

⛔ It compares RDN by RDN with case folded, and trims the space either side of each separator, because `uid=a, ou=people` and `uid=a,ou=people` are the same DN (RFC 4514 2). Comparing the whole strings makes them different, and then an entry is in scope or not depending on how the client happened to type its base.

It does NOT normalise attribute values by their matching rules, which needs a schema this package does not have: `cn=Bob` and `cn=bob ` are treated as equal (case, and trailing space) but `cn=B\6fb` is not unescaped to `cn=Bob`. A deployment whose DNs differ only by escaping needs a schema- aware comparison, and should say so rather than discover it.

func EscapeValue

func EscapeValue(v string) string

EscapeValue writes an assertion value the way RFC 4515 3 requires.

⛔ The five that MUST be escaped are `*`, `(`, `)`, `\` and NUL, and the reason is not cosmetic: an unescaped `*` in a value turns an equality filter into a substring one, and an unescaped `)` ends the filter early and leaves the rest of the value as syntax. A name containing either -- `O'Brien (contractor)`, or a password reset token -- becomes a different question from the one that was asked.

func InScope

func InScope(dn, base string, scope Scope) bool

InScope reports whether an entry's DN is within a search's scope (RFC 4511 4.5.1.2).

  • ScopeBaseObject is the base entry itself.
  • ScopeSingleLevel is its IMMEDIATE subordinates: the base is not in it, and neither is anything two levels down.
  • ScopeWholeSubtree is the base and everything beneath it.

An EMPTY base is the root of the DIT: everything is beneath it, its single level is the entries with exactly one RDN, and at base scope it is the root DSE -- which a server answers itself and never asks a handler about.

func Unsupported

func Unsupported(f Filter) error

Unsupported reports whether this filter asks for something this server cannot do, so a search can answer inappropriateMatching instead of silently returning fewer entries than the client asked about.

Types

type Abandoner

type Abandoner interface {
	Abandon(ctx context.Context, s Session, messageID int)
}

An Abandoner is told a client gave up on an operation (RFC 4511 4.11).

There is no response to an abandon, ever -- which is why this returns nothing. A server that answered one would be sending a message the client has no way to interpret.

type AddRequest

type AddRequest struct {
	DN         string
	Attributes []*Attribute
}

An AddRequest is RFC 4511 4.7.

type Adder

type Adder interface {
	Add(ctx context.Context, s Session, req *AddRequest) (Result, error)
}

An Adder adds an entry (RFC 4511 4.7).

type And

type And struct{ Filters []Filter }

And is `(&(a)(b))`. An EMPTY And is TRUE -- RFC 4526: the "absolute true" filter `(&)` matches every entry.

func (*And) Matches

func (f *And) Matches(e *Entry) bool

func (*And) String

func (f *And) String() string

type Approx

type Approx struct{ Attribute, Value string }

Approx is `(type~=value)`. Nothing here does phonetic matching, so it is answered as an equality -- which RFC 4511 4.5.1.7.6 permits: "the semantics of the approxMatch are implementation defined", and equality is an implementation of "approximately".

func (*Approx) Matches

func (f *Approx) Matches(e *Entry) bool

func (*Approx) String

func (f *Approx) String() string

type Attribute

type Attribute struct {
	Name   string
	Values [][]byte

	// Operational marks an attribute the directory maintains rather than a
	// person (RFC 4512 4.1.3): createTimestamp, entryUUID, and everything on
	// the root DSE.
	//
	// ⛔ It changes what a search RETURNS, which is why it is on the model
	// and not a detail of one server. RFC 4512 5.1: operational attributes
	// "are not returned in search requests unless requested by name" -- so
	// `*` (or an empty selection) brings the user attributes only, and `+`
	// (RFC 3673) brings these. A server that cannot tell them apart either
	// hides an attribute a client asked for or hands out one it did not.
	Operational bool
}

An Attribute is a PartialAttribute: a type and its values (RFC 4511 4.1.7).

⛔ Values are [][]byte and not []string. An attribute value is an OCTET STRING, and several that a directory carries are not text at all -- userCertificate;binary, jpegPhoto, objectSid, and the NT hash a Samba deployment reads. Typing them as string works until the first byte that is not valid UTF-8, which Go replaces with U+FFFD on conversion: the value that reaches the client is then not the value the directory holds, and nothing anywhere reports an error.

func OperationalAttribute

func OperationalAttribute(name string, values ...string) *Attribute

OperationalAttribute is StringAttribute for an attribute the directory maintains.

func StringAttribute

func StringAttribute(name string, values ...string) *Attribute

StringAttribute is the common case spelled once, since most attributes ARE text and writing the conversion at every call site is how one of them ends up different.

type BindRequest

type BindRequest struct {
	Version int
	Name    string
	Simple  []byte
	SASL    *SASLCredentials
}

A BindRequest is RFC 4511 4.2. Exactly one of Simple and SASL is set.

type Binder

type Binder interface {
	Bind(ctx context.Context, s Session, req *BindRequest) (Result, error)
}

A Binder answers a simple bind.

⛔ The password is []byte, not string. It goes to a constant-time comparison or to a KDF, both of which take bytes, and a string cannot be wiped -- Go strings are immutable, so a password that arrives as one stays in memory until the collector happens to reach it. It is also not necessarily UTF-8.

type Change

type Change struct {
	Operation ModifyOperation
	Attribute *Attribute
}

A Change is one modification: an operation and what it applies to.

type CompareRequest

type CompareRequest struct {
	DN        string
	Attribute string
	Value     []byte
}

A CompareRequest is RFC 4511 4.10.

type Comparer

type Comparer interface {
	Compare(ctx context.Context, s Session, req *CompareRequest) (Result, error)
}

A Comparer answers a compare (RFC 4511 4.10).

⛔ It answers CompareTrue or CompareFalse, and neither is an error. A handler returning Success is saying something the protocol has no word for, and a client reading `code == 0` as "they matched" would be wrong about every entry.

type Control

type Control struct {
	Type        string
	Criticality bool
	// Value is absent as nil, which is different from present and empty.
	Value []byte
}

A Control is RFC 4511 4.1.11.

⛔ Criticality is not decoration. A control marked critical that the server does not recognise means the operation MUST be refused with unavailableCriticalExtension -- because the client has said the operation is not the one it wants unless the control is honoured. Ignoring it performs a DIFFERENT operation and reports success: a client that sent the ManageDsaIT control critically and had it dropped is editing the alias rather than what it points at.

func Find

func Find(controls []Control, oid string) *Control

Find returns the named control, or nil.

func UnhandledCritical

func UnhandledCritical(controls []Control, known ...string) *Control

UnhandledCritical returns the first control that is marked critical and is not in known, or nil.

A server calls it for every operation and refuses when it returns something. Getting this wrong is silent in exactly the direction that matters: the operation succeeds, and it is not the operation that was asked for.

type DNModifier

type DNModifier interface {
	ModifyDN(ctx context.Context, s Session, req *ModifyDNRequest) (Result, error)
}

A DNModifier renames or moves one (RFC 4511 4.9).

type DeleteRequest

type DeleteRequest struct{ DN string }

A DeleteRequest is RFC 4511 4.8. It is a DN and nothing else.

type Deleter

type Deleter interface {
	Delete(ctx context.Context, s Session, req *DeleteRequest) (Result, error)
}

A Deleter deletes one (RFC 4511 4.8).

type DerefAliases

type DerefAliases uint8

A DerefAliases says what to do with alias entries (RFC 4511 4.5.1.3).

Nothing here creates aliases, so nothing here dereferences them. The value is carried and reported rather than ignored, because a client that asked for dereferencing and silently did not get it has been given an answer to a question it did not ask.

const (
	NeverDerefAliases   DerefAliases = 0
	DerefInSearching    DerefAliases = 1
	DerefFindingBaseObj DerefAliases = 2
	DerefAlways         DerefAliases = 3
)

type Disconnecter

type Disconnecter interface {
	Disconnect(s Session)
}

A Disconnecter is told a connection ended, however it ended.

type Entry

type Entry struct {
	DN         string
	Attributes []*Attribute
}

An Entry is a SearchResultEntry: a DN and what is published about it.

func (*Entry) Attribute

func (e *Entry) Attribute(name string) *Attribute

Attribute returns the named attribute, or nil. The name is matched without regard to case, as RFC 4512 2.5 requires of an attribute description.

func (*Entry) Values

func (e *Entry) Values(name string) []string

Values returns the named attribute's values as strings, for the callers that know their attribute is text.

type EntryWriter

type EntryWriter interface {
	// Entry sends one. The error is the client going away or a limit being
	// reached, and a handler that ignores it keeps walking a directory
	// nobody is reading any more.
	Entry(*Entry) error
	// Reference sends a continuation reference (RFC 4511 4.5.3): other
	// servers to ask for the part of the tree this one does not hold.
	Reference(uris ...string) error
}

An EntryWriter takes the entries a search found.

type Equality

type Equality struct{ Attribute, Value string }

Equality is `(type=value)`.

func (*Equality) Matches

func (f *Equality) Matches(e *Entry) bool

func (*Equality) String

func (f *Equality) String() string

type Error

type Error struct{ Result }

An Error is a result that refused.

func (*Error) Error

func (e *Error) Error() string

type ExtendedRequest

type ExtendedRequest struct {
	Name  string // the OID
	Value []byte // absent as nil
}

An ExtendedRequest is RFC 4511 4.12.

type ExtendedResult

type ExtendedResult struct {
	Result
	Name  string // responseName, usually the request's OID or empty
	Value []byte // responseValue, absent as nil
}

An ExtendedResult is an extended response (RFC 4511 4.12).

type Extender

type Extender interface {
	// ExtendedNames is the OIDs this handler answers, for the root DSE to
	// advertise as supportedExtension.
	ExtendedNames() []string
	Extended(ctx context.Context, s Session, req *ExtendedRequest) (ExtendedResult, error)
}

An Extender answers an extended operation (RFC 4511 4.12).

StartTLS is NOT routed here: it changes the connection under the protocol and the server does it itself. A handler that received it could not perform the handshake without racing the server's own reader.

type ExtensibleMatch

type ExtensibleMatch struct {
	Attribute    string
	MatchingRule string
	Value        string
	DNAttributes bool
}

ExtensibleMatch is `(type:dn:=value)` and its relatives (RFC 4511 4.5.1.7.7). Nothing here implements a matching rule, so one that NAMES a rule is refused rather than guessed at -- see Matches.

func (*ExtensibleMatch) Matches

func (f *ExtensibleMatch) Matches(e *Entry) bool

Matches an extensible match, or refuses to.

⛔ A filter naming a matching rule this server does not implement matches NOTHING, and says so through Unsupported rather than falling back to equality. RFC 4511 4.5.1.7.7 is explicit: a server that does not recognise the rule returns inappropriateMatching. Falling back would answer a different question -- `(memberOf:1.2.840.113556.1.4.1941:=cn=admins,...)` asks for a TRANSITIVE group search, and answering it as an equality would quietly say "not a member" about people who are.

func (*ExtensibleMatch) String

func (f *ExtensibleMatch) String() string

type Filter

type Filter interface {
	// Matches reports whether an entry satisfies this filter.
	Matches(e *Entry) bool
	// String is the RFC 4515 form, parenthesised.
	String() string
	// contains filtered or unexported methods
}

A Filter is the question a search asked (RFC 4511 4.5.1.7, and its string form in RFC 4515).

⛔ ONE representation, three renderings. The library this replaces had three separate implementations -- one that decoded a filter off the wire into a string, one that encoded a string onto the wire, and one that matched an entry against the wire form -- and they drifted apart. The result was that `(uid=svc-*-prod)` was decoded as `(uid=svc-*)`, encoded as an equality match on a literal '*', and matched on its first component alone. Three answers to one question, all different, and a directory answering a BROADER question than it was asked is disclosure.

So a filter is parsed or decoded ONCE into this tree, and String, Encode and Matches all read the same tree. They cannot disagree about what a filter means, because there is only one thing that means it.

func DecodeFilter

func DecodeFilter(p *ber.Packet) (Filter, error)

DecodeFilter reads a Filter from its BER packet.

func ParseFilter

func ParseFilter(s string) (Filter, error)

ParseFilter reads the RFC 4515 string form into the tree.

It is here for tests, for a configuration that writes a filter down, and for a log line that has to render one back. A SERVER does not parse strings off the wire -- DecodeFilter reads the BER directly -- and that is deliberate: a server that decoded a filter to a string and re-parsed it would be back to two representations that can disagree, which is the defect this whole file exists to prevent.

type GreaterOrEqual

type GreaterOrEqual struct{ Attribute, Value string }

GreaterOrEqual is `(type>=value)`.

func (*GreaterOrEqual) Matches

func (f *GreaterOrEqual) Matches(e *Entry) bool

func (*GreaterOrEqual) String

func (f *GreaterOrEqual) String() string

type LessOrEqual

type LessOrEqual struct{ Attribute, Value string }

LessOrEqual is `(type<=value)`.

func (*LessOrEqual) Matches

func (f *LessOrEqual) Matches(e *Entry) bool

func (*LessOrEqual) String

func (f *LessOrEqual) String() string

type Modifier

type Modifier interface {
	Modify(ctx context.Context, s Session, req *ModifyRequest) (Result, error)
}

A Modifier modifies one (RFC 4511 4.6).

⛔ The changes are applied IN ORDER and ATOMICALLY -- see ModifyRequest. A handler that applies half of them and fails has left the directory in a state the client never asked for, and must answer as though it applied none.

type ModifyDNRequest

type ModifyDNRequest struct {
	DN           string
	NewRDN       string
	DeleteOldRDN bool
	// NewSuperior moves the entry to another parent. Empty means "leave it
	// where it is", which is a rename rather than a move.
	NewSuperior string
}

A ModifyDNRequest is RFC 4511 4.9.

type ModifyOperation

type ModifyOperation uint8

A ModifyOperation is one of RFC 4511 4.6's three.

const (
	// AddValues adds values, leaving any already there.
	AddValues ModifyOperation = 0
	// DeleteValues removes the listed values, or the whole attribute when
	// none are listed.
	DeleteValues ModifyOperation = 1
	// ReplaceValues replaces every value, and with none listed removes the
	// attribute -- which is how a client clears one.
	ReplaceValues ModifyOperation = 2
)

func (ModifyOperation) String

func (o ModifyOperation) String() string

type ModifyRequest

type ModifyRequest struct {
	DN      string
	Changes []Change
}

A ModifyRequest is RFC 4511 4.6.

⛔ Changes is an ORDERED LIST, and that is the whole point of this type. RFC 4511 4.6: "the modification operations are performed in the order listed", and the whole list is "performed as an atomic operation".

The library this replaces held three buckets -- AddAttributes, DeleteAttributes, ReplaceAttributes -- which throws the order away. Then

delete member=alice; add member=alice     (alice stays)
add member=alice; delete member=alice     (alice goes)

arrive as the same request, and a server cannot tell which was asked. That is not an edge case: it is how a client that reconciles a group membership writes one, and getting it backwards removes somebody's access or grants it. A shape that cannot express the difference produces a write that is wrong INVISIBLY, which is why writes could not simply be bolted onto that library.

Atomicity is the implementation's to provide: a handler that applies half this list and then fails has left the directory in a state the client never asked for, and must answer as though it had applied none.

type Not

type Not struct{ Filter Filter }

Not is `(!(a))`.

func (*Not) Matches

func (f *Not) Matches(e *Entry) bool

func (*Not) String

func (f *Not) String() string

type Or

type Or struct{ Filters []Filter }

Or is `(|(a)(b))`. An EMPTY Or is FALSE -- RFC 4526's "absolute false" filter `(|)` matches nothing. The asymmetry is the specification's and is the identity element of each operation; getting it backwards turns a filter that should match nothing into one that matches the directory.

func (*Or) Matches

func (f *Or) Matches(e *Entry) bool

func (*Or) String

func (f *Or) String() string

type Present

type Present struct{ Attribute string }

Present is `(type=*)`.

func (*Present) Matches

func (f *Present) Matches(e *Entry) bool

Present is about the ATTRIBUTE, not its values: `(mail=*)` asks whether this entry has a mail at all.

func (*Present) String

func (f *Present) String() string

Present is the one place a bare '*' is not escaped, because there it is the syntax rather than a value.

type Result

type Result struct {
	Code ResultCode
	// MatchedDN is the part of the requested DN that DOES exist. RFC 4511
	// 4.1.9 says it is set for noSuchObject, aliasProblem, invalidDNSyntax
	// and aliasDereferencingProblem, and is empty otherwise.
	MatchedDN string
	// Diagnostic is for a person reading a log, and MAY be empty. It is not a
	// machine-readable field and nothing should parse it.
	Diagnostic string
	// Referral is the URIs to ask instead, and is set only with the referral
	// code (RFC 4511 4.1.10).
	Referral []string
}

A Result is an LDAPResult: what every operation answers (RFC 4511 4.1.9).

⛔ MatchedDN and Diagnostic are fields here and not an afterthought, because they are fields of the protocol. A server that cannot set MatchedDN cannot tell a client how far along a DN the search base stopped existing -- which is the difference between "you have a typo in ou=" and "that whole tree is gone". The library this replaces hardcoded both to the empty string at every call site, so no server built on it could say either.

func Refuse

func Refuse(code ResultCode, format string, args ...any) Result

Refuse is the short way to build a refusal with a reason.

func (Result) Err

func (r Result) Err() error

Err makes an error of a result that is not a success, and nil of one that is.

type ResultCode

type ResultCode uint16

A ResultCode is the resultCode of an LDAPResult (RFC 4511 4.1.9, and the enumeration in Appendix B).

It is a named type and not an int, because the difference between invalidCredentials and authMethodNotSupported is the difference between "try again" and "not this way", and a client acts on it. A field typed int lets any number through and reads the same at every call site.

const (
	Success                      ResultCode = 0
	OperationsError              ResultCode = 1
	ProtocolError                ResultCode = 2
	TimeLimitExceeded            ResultCode = 3
	SizeLimitExceeded            ResultCode = 4
	CompareFalse                 ResultCode = 5
	CompareTrue                  ResultCode = 6
	AuthMethodNotSupported       ResultCode = 7
	StrongerAuthRequired         ResultCode = 8
	Referral                     ResultCode = 10
	AdminLimitExceeded           ResultCode = 11
	UnavailableCriticalExtension ResultCode = 12
	ConfidentialityRequired      ResultCode = 13
	SaslBindInProgress           ResultCode = 14
	NoSuchAttribute              ResultCode = 16
	UndefinedAttributeType       ResultCode = 17
	InappropriateMatching        ResultCode = 18
	ConstraintViolation          ResultCode = 19
	AttributeOrValueExists       ResultCode = 20
	InvalidAttributeSyntax       ResultCode = 21
	NoSuchObject                 ResultCode = 32
	AliasProblem                 ResultCode = 33
	InvalidDNSyntax              ResultCode = 34
	AliasDereferencingProblem    ResultCode = 36
	InappropriateAuthentication  ResultCode = 48
	InvalidCredentials           ResultCode = 49
	InsufficientAccessRights     ResultCode = 50
	Busy                         ResultCode = 51
	Unavailable                  ResultCode = 52
	UnwillingToPerform           ResultCode = 53
	LoopDetect                   ResultCode = 54
	NamingViolation              ResultCode = 64
	ObjectClassViolation         ResultCode = 65
	NotAllowedOnNonLeaf          ResultCode = 66
	NotAllowedOnRDN              ResultCode = 67
	EntryAlreadyExists           ResultCode = 68
	ObjectClassModsProhibited    ResultCode = 69
	AffectsMultipleDSAs          ResultCode = 71
	Other                        ResultCode = 80
)

The result codes. The gaps are the specification's: 9, 15, 22-31, 35, 37-47, 55-63, 70 and 72-79 are reserved or unused, and are left out rather than filled in, so that a reader can see the enumeration IS the RFC's.

func (ResultCode) OK

func (c ResultCode) OK() bool

OK reports whether this code means the operation succeeded.

⛔ compareTrue and compareFalse are both answers to a Compare and neither is a failure; saslBindInProgress is a bind that has not finished. A caller that tests `code == Success` treats all three as errors, which is why this exists rather than that comparison.

func (ResultCode) String

func (c ResultCode) String() string

String is the specification's own name for the code, which is what a log line and a client's error message should both say.

type SASLBinder

type SASLBinder interface {
	// Mechanisms is what this server offers, for the root DSE to advertise.
	// A client discovers what it may use by reading
	// supportedSASLMechanisms, so a mechanism missing here is one nothing
	// will ever try.
	Mechanisms() []string
	// BindSASL answers one step. A Result with code SaslBindInProgress and a
	// non-nil ServerCreds is a challenge; one with Success must set BoundDN,
	// because the name field of a SASL bind is ignored (RFC 4513 5.2.1) and
	// the mechanism is the only thing that knows who this is.
	BindSASL(ctx context.Context, s Session, req *BindRequest) (SASLResult, error)
}

A SASLBinder answers a SASL bind, which may take several round trips.

It is separate from Binder because a server may implement one and not the other, and because the two carry different things: a SASL exchange has a challenge to send back and an identity of its own, and a simple bind has neither.

type SASLCredentials

type SASLCredentials struct {
	Mechanism string
	// Credentials is absent as nil and present-and-empty as an empty non-nil
	// slice. RFC 4511 gives the two different meanings and several mechanisms
	// use a zero-length response as a message in its own right.
	Credentials []byte
}

SASLCredentials is RFC 4511 4.2's SaslCredentials.

type SASLResult

type SASLResult struct {
	Result
	// ServerCreds is serverSaslCreds, sent whenever it is non-nil INCLUDING
	// empty: RFC 4511 4.2.2 gives absent and zero-length different meanings.
	ServerCreds []byte
	// BoundDN is who the exchange proved, read only when the code is
	// Success. Empty binds the connection as anonymous, which is a
	// legitimate thing for a mechanism to decide and never an accident the
	// server can spot.
	BoundDN string
}

A SASLResult is what one step of a SASL bind decided.

type Scope

type Scope uint8

A Scope is a search scope (RFC 4511 4.5.1.2).

const (
	ScopeBaseObject   Scope = 0
	ScopeSingleLevel  Scope = 1
	ScopeWholeSubtree Scope = 2
)

func (Scope) String

func (s Scope) String() string

type SearchRequest

type SearchRequest struct {
	BaseObject   string
	Scope        Scope
	DerefAliases DerefAliases
	// SizeLimit and TimeLimit are 0 for "no limit from the client". The
	// server may have its own, and the SMALLER of the two applies -- a client
	// asking for more than the server allows does not raise the server's.
	SizeLimit int
	TimeLimit int
	// TypesOnly asks for attribute descriptions WITHOUT their values.
	TypesOnly bool
	Filter    Filter
	// Attributes is the selection (RFC 4511 4.5.1.8). Empty means "all user
	// attributes", which is NOT the same as "everything": operational
	// attributes are returned only when named, or by "+".
	Attributes []string
}

A SearchRequest is RFC 4511 4.5.1.

type Searcher

type Searcher interface {
	Search(ctx context.Context, s Session, req *SearchRequest, w EntryWriter) (Result, error)
}

A Searcher answers a search.

⛔ Entries are written to w as they are found, not returned as a slice. A directory that materialises every match before sending any has decided that its largest answer fits in memory, and the search that finds out it does not is the one a client ran by accident. Writing as it goes also lets the server stop at the size limit without the handler having to know what the limit is.

type Server

type Server struct {
	// Bind answers a simple bind. With none, every simple bind is refused --
	// a server with no way to prove anybody should not let anybody in.
	Bind Binder
	// SASL answers a SASL bind, and names the mechanisms the root DSE
	// advertises.
	SASL SASLBinder
	// Search answers a search. With none, only the root DSE is answerable.
	Search Searcher

	Compare  Comparer
	Extended Extender

	// The writes. Each is independently optional.
	Add      Adder
	Modify   Modifier
	Delete   Deleter
	ModifyDN DNModifier

	Abandon      Abandoner
	Disconnected Disconnecter

	// TLSConfig, when set, makes StartTLS available (RFC 4511 4.14). It is
	// also what an ldaps:// listener is wrapped with, but that is the
	// caller's to do -- a tls.Listener is a listener like any other.
	TLSConfig *tls.Config

	// RequireTLS refuses every operation on a connection that is not
	// protected, with confidentialityRequired.
	//
	// ⛔ It exists because StartTLS is asked for by the CLIENT: a server that
	// merely OFFERS it has promised nothing, and a client that does not ask
	// sends its bind password in the clear while everything looks normal at
	// both ends. This is what turns "we have a certificate" from an offer
	// into a guarantee.
	RequireTLS bool

	// MaxMessageSize bounds one message. Zero means DefaultMaxMessageSize.
	MaxMessageSize int
	// MaxEntries bounds what one search may return, whatever the client
	// asked for. Zero means no server-side limit.
	//
	// The SMALLER of this and the client's sizeLimit applies: a client
	// asking for more than the server allows does not raise the server's.
	MaxEntries int
	// Timeout is how long one operation may take. Zero means no limit.
	Timeout time.Duration
	// IdleTimeout closes a connection nothing has been sent on. Zero means
	// never.
	IdleTimeout time.Duration

	// NamingContexts is what this server holds, published on the root DSE so
	// that a client can find out where to search (RFC 4512 5.1.2).
	NamingContexts []string
	// AltServers are other servers to try when this one is unavailable.
	AltServers []string
	// Vendor and VendorVersion identify the implementation (RFC 3045).
	Vendor        string
	VendorVersion string

	// Log receives what happened. Nil discards it.
	//
	// ⛔ Nothing here logs an assertion VALUE. A search filter carries what
	// somebody typed -- a name, an email address, sometimes a token pasted
	// into the wrong box -- and a directory that writes them to disk has
	// made a record of every question anybody asked about anybody. The
	// filter's SHAPE is logged instead.
	Log *slog.Logger
	// contains filtered or unexported fields
}

A Server answers LDAP on a listener.

⛔ The handlers are fields rather than one big interface, and a nil one means "this server does not do that". An operation with no handler is answered unwillingToPerform, NOT insufficientAccessRights: "I do not do that" and "you may not do that" send an administrator to two different places, and saying the second when the first is true starts an argument about permissions that do not exist.

func (*Server) Close

func (s *Server) Close() error

Close stops the listeners and closes every connection.

⛔ It closes the CONNECTIONS too, not only the listeners. A server that stopped accepting and left the established ones running looks stopped and is still answering, which is the shape of a restart that does not take.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(addr string) error

ListenAndServe listens on addr and serves it.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve answers connections until the listener is closed.

type Session

type Session interface {
	// BoundDN is who this connection proved itself to be, and is empty for
	// anonymous. RFC 4511 4.2.1: a failed bind leaves it empty, and so does
	// a bind still in progress.
	BoundDN() string
	// TLS is the connection's TLS state, and ok is false on a connection that
	// was never protected -- including one where StartTLS was offered and
	// never asked for.
	TLS() (tls.ConnectionState, bool)
	// RemoteAddr is where the client is.
	RemoteAddr() net.Addr
	// Conn is the underlying connection, for the handler that genuinely needs
	// it. Reading or writing it corrupts the LDAP stream; it is here for
	// peer certificates and addresses.
	Conn() net.Conn
	// Controls are the request's controls (RFC 4511 4.1.11).
	Controls() []Control
}

A Session is what a handler knows about the client it is answering.

⛔ BoundDN is here rather than passed as a string, and TLS is here at all, because the two questions a write has to ask are "who is this" and "over what". A handler given only a DN cannot refuse a password change sent in the clear, and every server that wants to must reach for the raw net.Conn and type-assert it -- which is a thing each of them then gets slightly differently.

type Substrings

type Substrings struct {
	Attribute string
	Initial   string // empty means absent
	Any       []string
	Final     string // empty means absent
}

Substrings is `(type=ini*any*any*fin)`.

⛔ Initial and Final are at most one each and Any is any number of them, in order, which is what RFC 4511 4.5.1.7.2 says and what the three-way drift above got wrong. They are separate fields rather than a list of components so that "at most once" is a thing the type says rather than a thing a check has to remember.

func (*Substrings) Matches

func (f *Substrings) Matches(e *Entry) bool

Matches walks the components in order and WITHOUT OVERLAP, which is the whole of the defect this type exists to prevent.

`(uid=svc-*-prod)` is initial "svc-", final "-prod": the value must start with one, end with the other, and they must not overlap -- "svc-prod" is 8 characters and the two components need 9, so it does not match, and a check that tested them independently would say it does.

func (*Substrings) String

func (f *Substrings) String() string

Directories

Path Synopsis
Package ldaptest drives an LDAP server over the wire, for the questions a real client cannot express.
Package ldaptest drives an LDAP server over the wire, for the questions a real client cannot express.

Jump to

Keyboard shortcuts

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