Documentation
¶
Overview ¶
Package ldapdir reads people and groups from an LDAP directory.
What LDAP can and cannot prove ¶
This is the thing to understand before choosing it, and it is a fact about protocols rather than about this package.
An LDAP directory authenticates by BIND: you hand it a name and a password and it says yes or no, without ever giving the password up. That is exactly right for anything that RECEIVES a password from the client — HTTP Basic, an ordinary login form — and this package wires it up as a verifier.
It is exactly wrong for NTLMv2, which Windows file sharing uses. There the client never sends the password: it sends a response computed from it, and the server must compute the same thing from MD4(UTF16LE(password)). There is nothing to bind WITH. So an LDAP user can be served over SMB only if the directory publishes `sambaNTPassword` — which is what Samba's own schema exists for — and over SSH only if it publishes `sshPublicKey`, which is OpenSSH's convention and what sssd reads.
Both attributes are read here when they are there, and github.com/go-authn/directory.Identity.Can then reports, per person, which protocols their credentials can actually answer.
Index ¶
- Constants
- type Config
- type Source
- func (s *Source) Describe() string
- func (s *Source) GroupAttributes(group string) (directory.Attributes, error)
- func (s *Source) GroupNames() ([]string, error)
- func (s *Source) GroupObjectClasses(group string) (directory.ObjectClasses, error)
- func (s *Source) Identities() ([]*directory.Identity, error)
- func (s *Source) Members(group string) ([]string, error)
- func (s *Source) ReadGroups() (map[string]directory.GroupData, error)
Constants ¶
const DefaultGroupFilter = "(|(objectClass=posixGroup)(objectClass=groupOfNames))"
DefaultGroupFilter is the GroupFilter a Config that names none gets: a posixGroup (RFC 2307, names in memberUid) or a groupOfNames (RFC 4519 §3.5, DNs in member).
⛔ Until v0.13.0 it was (objectClass=posixGroup) alone, and go-authn/authnd publishes a group without a gidNumber as a groupOfNames, since RFC 2307 makes gidNumber MUST in posixGroup: such a group was "no such group" here, and an @group rule naming it granted nothing. The cost is that the default is WIDER: an institution's groupOfNames groups are now groups here. It adds no PERSON -- a member DN maps only to somebody the people search already imported -- but a group that was invisible can now grant what an @group rule names.
const DefaultUserFilter = "(|(objectClass=posixAccount)(objectClass=account)(objectClass=uidObject))"
DefaultUserFilter is the UserFilter a Config that names none gets: an entry that is a person by any of the three standard classes that carry a uid.
- posixAccount (RFC 2307 §4, AUXILIARY, MUST uid): a person with a uidNumber, a gidNumber and a home directory;
- account (RFC 4524 §3.1, STRUCTURAL, MUST uid): the structural class RFC 2307 itself recommends for a posixAccount, and what a person without the POSIX attributes is published as by go-authn/authnd;
- uidObject (RFC 4519 §3.14, AUXILIARY, MUST uid): the class go-authn/authnd puts on EVERY person it publishes, whatever its structural class (inetOrgPerson or account).
⛔ Until v0.13.0 the default was (objectClass=posixAccount) alone. A person go-authn/authnd publishes without the POSIX attributes is not a posixAccount, because RFC 2307 makes uidNumber, gidNumber and homeDirectory MUST, and would have vanished for every consumer of this package. The cost is that the default is WIDER: an institution's directory holding account or uidObject entries that are not posixAccounts -- service accounts, hosts named by uid, people imported from elsewhere -- gives them now, and gave nothing before. A plain inetOrgPerson with neither class is still left out.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
URL string // ldaps://host, or ldap://host for a plaintext one
BaseDN string // where the people are
// BindDN and BindPassword are how this server reads the directory. An
// anonymous read is allowed by leaving them empty, which some directories
// permit and most do not.
BindDN string
BindPassword string
// UserFilter selects the people. The default, [DefaultUserFilter], is
// (|(objectClass=posixAccount)(objectClass=account)(objectClass=uidObject)).
//
// ⛔ It was (objectClass=posixAccount) up to v0.13.0, and the change
// WIDENS what a directory gives by default: an entry of class account
// (RFC 4524) or uidObject (RFC 4519) that is not a posixAccount was left
// out before and is read now. Set UserFilter to the old value to keep the
// old set.
UserFilter string
UserAttribute string // default uid
// TOTPAttribute is where a one-time-code secret lives, in base32. It has
// NO default on purpose: there is no standard attribute for it -- FreeIPA
// has ipatokenOTPkey, other schemas have oathSecret or something local --
// and a default that guessed wrong would read nothing while looking like
// it had looked.
TOTPAttribute string
// Attributes are what to copy from each person's entry, as the directory
// names them -- ["mail", "uidNumber", "homeDirectory"] -- into
// [github.com/go-authn/directory.Identity.Attributes]. They describe and
// prove nothing, and none of them may be a credential: [New] refuses
// what [github.com/go-authn/directory.CheckAttributeName] refuses, and
// TOTPAttribute, which holds a secret under a name only this
// configuration knows. Nothing is copied by default.
//
// An attribute is copied under the spelling written here, whatever case
// the directory answers in. Subtypes it returns besides -- description;
// lang-fr, for description -- are not: ask for them by name.
Attributes []string
// GroupAttributes are the same for each group: ["gidNumber"]. The
// member attribute is refused, since Members is how membership is read.
GroupAttributes []string
// ObjectClasses are the auxiliary object classes every person this
// source hands out is DECLARED to carry -- ["supannPerson", "eduPerson"]
// -- and GroupObjectClasses the same for every group. [New] refuses a name
// that is not an RFC 4512 descriptor (see
// [github.com/go-authn/directory.CheckObjectClassName]).
//
// ⛔ Declared, never read: an entry's own objectClass values are not
// copied, and objectClass cannot be named in Attributes. What the upstream
// directory files a person under is ITS schema; what this source's people
// are published as is the configuration's promise. See
// [github.com/go-authn/directory.ObjectClasses].
ObjectClasses []string
GroupObjectClasses []string
GroupBaseDN string // default: BaseDN
GroupFilter string // default DefaultGroupFilter: posixGroup or groupOfNames
GroupAttribute string // default cn
// MemberAttribute, when set, is the ONE attribute a group's members are
// read from, and overrides the default. Left empty, both are read:
// memberUid (a posixGroup's, names) and member (a groupOfNames's, DNs).
//
// Either way a value that is a DN is mapped to a person only through
// the people this source imports -- the entries its own people search
// returns -- by distinguishedNameMatch on their DNs. A DN outside them,
// or naming nobody imported, maps to nobody: see [Source.Members].
MemberAttribute string
// TLS is used for ldaps:// and for StartTLS. A nil one means TLS 1.2 and
// the system roots, which is the right default and the one a caller
// overrides to pin a private CA.
TLS *tls.Config
// StartTLS upgrades a plaintext connection before binding. A directory
// reached over ldap:// without it sends the bind password in the clear,
// so [New] refuses ldap:// to anything but loopback without it.
StartTLS bool
}
Config is where the directory is and how it is laid out.
The defaults are a small site's people (posixAccount, account or uidObject: see DefaultUserFilter) and groups (posixGroup or groupOfNames: see DefaultGroupFilter), which is what an OpenLDAP put up for a team, or go-authn/authnd, looks like. A member DN is mapped to the person this source imported under that DN, and to nobody else.
type Source ¶
type Source struct {
// contains filtered or unexported fields
}
A Source reads an LDAP directory.
func New ¶
New connects once, to find out whether the directory is there at all.
It is done here rather than at the first login because a directory that is not answering is a server that cannot authenticate anybody, and that should be said before it starts listening.
func (*Source) GroupAttributes ¶ added in v0.13.0
func (s *Source) GroupAttributes(group string) (directory.Attributes, error)
GroupAttributes copies the configured GroupAttributes from one group's entry -- from every entry, when the filter matches several, as Members reads the members of all of them.
func (*Source) GroupNames ¶ added in v0.3.0
GroupNames is every group under the group base, by the same filter Members uses -- without the name, which is the only difference between asking about one group and asking for all of them.
func (*Source) GroupObjectClasses ¶ added in v0.15.0
func (s *Source) GroupObjectClasses(group string) (directory.ObjectClasses, error)
GroupObjectClasses is the GroupObjectClasses configured, for a group the directory has, and ErrNoSuchGroup for one it has not. The group's own objectClass values are not asked for: the answer is the configuration's.
func (*Source) Identities ¶
Identities reads everybody under the base DN.
func (*Source) Members ¶
Members expands a group.
The values are read from MemberAttribute when it is configured, and otherwise from memberUid (names) and member (DNs) both. A value that is a DN -- every member value, and with an explicit MemberAttribute any value that parses as one -- is mapped to a name ONLY through the people this source imports: the entries its people search returns, compared with the DN by distinguishedNameMatch (RFC 4517 §4.2.15; types case-insensitively, values by caseIgnoreMatch, the equality rule of uid, cn, ou, o and dc, and multi-valued RDNs in any order).
⛔ It is never read out of the DN. This used to take the value of the first RDN, so "uid=alice,ou=elsewhere,dc=example,dc=org" -- an entry the people search never returned, in a part of the tree somebody else may write -- made alice a member. A DN outside the imported people, or naming nobody, maps to nobody.
func (*Source) ReadGroups ¶ added in v0.16.0
ReadGroups is every group, as Members, GroupAttributes and GroupObjectClasses answer for each, from ONE search for the groups and, when a member is a DN, one search for the people: directory.GroupReader.
Asking those three for each group dials and searches three times for it, and Members searches for the people again whenever a member is a DN.
A group is matched as the directory matches it: by the equality rule of the group attribute, which for cn is caseIgnoreMatch, so two entries whose names differ only by case are one group for every spelling, as they are when asked for by name. A DN member is mapped to a person once for each distinct value.