ldap

package
v0.8.389 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package ldap is the enterprise auth provider — connects to a corporate LDAP/AD directory, authenticates users with their domain credentials, and resolves their group memberships into Coremetry roles via an admin-configurable mapping.

Designed for enterprise-style on-prem deployments:

  • LDAPS (port 636) is the default; StartTLS (389→TLS upgrade) is supported for legacy AD setups.
  • Custom CA paste field for internal CAs; SkipVerify toggle as last-resort escape hatch for self-signed certs.
  • Group→role mapping is the primary provisioning path. Pre- provisioned users (admin pinned a row) override the group map.
  • Bind password is stored in plaintext in system_settings — that was an explicit deployment-time decision; an env-keyed encrypt path can be bolted on later if needed.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthResult

type AuthResult struct {
	User LDAPUser
	Role string
}

AuthResult bundles the authenticated user + the role we resolved from their group memberships.

type Config

type Config struct {
	Enabled bool `json:"enabled"`

	// Connection
	Host       string `json:"host"`
	Port       int    `json:"port"`
	UseTLS     bool   `json:"useTLS"`   // direct ldaps:// (default port 636)
	StartTLS   bool   `json:"startTLS"` // upgrade plain → TLS on 389
	SkipVerify bool   `json:"skipVerify"`
	CACert     string `json:"caCert"` // PEM bundle for internal CA

	// Service account used to look up users / groups.
	BindDN       string `json:"bindDN"`
	BindPassword string `json:"bindPassword"`

	// Search
	BaseDN           string `json:"baseDN"`
	UserSearchFilter string `json:"userSearchFilter"` // {{username}} placeholder
	UserAttribute    string `json:"userAttribute"`    // sAMAccountName | uid | mail
	EmailAttribute   string `json:"emailAttribute"`
	DisplayAttribute string `json:"displayAttribute"`

	// Group lookup
	GroupSearchBase string `json:"groupSearchBase"`
	GroupFilter     string `json:"groupFilter"` // {{userDN}} placeholder
	// SkipMemberOfFetch drops `memberOf` from the user-search
	// attribute list. AD enforces MaxValRange (default 1500)
	// and MaxReceiveBuffer (1MB on some configs); a senior
	// user with thousands of nested group memberships trips
	// these and the login fails with LDAP_ADMIN_LIMIT_EXCEEDED
	// or a 1MB-cap error. Skipping memberOf moves the
	// authoritative membership lookup to the separate
	// GroupSearchBase + GroupFilter pass (LDAP_MATCHING_RULE_IN_CHAIN
	// recurses through nested groups cleanly), which pulls
	// only DN values without the per-user attribute bloat.
	// Required pre-req: GroupSearchBase must be set; otherwise
	// the auth fall-through has nothing to derive roles from.
	SkipMemberOfFetch bool `json:"skipMemberOfFetch"`

	// Role assignment
	DefaultRole  string             `json:"defaultRole"`  // role for users without group match
	GroupRoleMap []GroupRoleMapping `json:"groupRoleMap"` // first match wins (admin > editor > viewer)
}

Config is the persisted LDAP connection + mapping definition.

Defaults are filled in by Normalize() so the config struct can be built from a half-empty PUT body and still produce sensible probes.

func (*Config) Normalize

func (c *Config) Normalize()

Normalize fills in Active-Directory-friendly defaults so half-filled configs produce a working probe. Mutates in place.

func (*Config) Sanitize

func (c *Config) Sanitize() Config

Sanitize returns a copy with the bind password cleared — used for API responses so the secret never round-trips back to the UI.

type GroupRoleMapping

type GroupRoleMapping struct {
	Group string `json:"group"` // group DN (preferred) or CN — case-insensitive substring match
	Role  string `json:"role"`  // admin | editor | viewer
}

type LDAPUser

type LDAPUser struct {
	DN          string `json:"dn"`
	Username    string `json:"username"`
	Email       string `json:"email"`
	DisplayName string `json:"displayName"`
	// Department / Company — directory org info (v0.8.266, operator:
	// "organizasyon, ad soyad, ekip bilgisi de gelsin"). AD names them
	// department/company; inetOrgPerson uses ou/o — dirText resolves
	// the fallback. Department feeds the users.team column on login,
	// Company the org column.
	Department string   `json:"department,omitempty"`
	Company    string   `json:"company,omitempty"`
	Groups     []string `json:"groups,omitempty"`
	// Photo — raw thumbnailPhoto (AD) / jpegPhoto (inetOrgPerson) bytes
	// (v0.8.238). Never serialized: the directory-search UI JSON must
	// not ship images; the login path persists it to the users row and
	// the photo endpoints serve it from there.
	Photo []byte `json:"-"`
}

LDAPUser is the lightweight projection of a directory entry that the UI consumes (search results + provisioning picker).

type Service

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

Service holds the live config; mutates safely under RWMutex so the admin Settings PUT can swap config while a login is in flight.

func New

func New() *Service

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, username, password string) (*AuthResult, error)

Authenticate runs the standard "search-then-bind" auth pattern:

  1. Service-bind (admin lookup credentials).
  2. Find the user by username (or email — `username` may be either, the configured UserSearchFilter handles it).
  3. Re-bind with the user's DN + entered password — that's the actual credential check.
  4. Resolve groups → role via the configured GroupRoleMap; fall back to DefaultRole.

func (*Service) Configure

func (s *Service) Configure(incoming Config)

Configure swaps the live config. If incoming.BindPassword == "" but a password is already saved, the old one is preserved (matches the "leave empty to keep current" UX).

func (*Service) Enabled

func (s *Service) Enabled() bool

func (*Service) LoadPersisted

func (s *Service) LoadPersisted(ctx context.Context, store SettingsStore) error

func (*Service) SavePersisted

func (s *Service) SavePersisted(ctx context.Context, store SettingsStore, c Config) error

func (*Service) Search

func (s *Service) Search(ctx context.Context, query string, limit int) ([]LDAPUser, error)

Search looks up users matching `query` (substring on username, email or displayName). Used by the admin "pick a user to provision" flow. Returns at most `limit` entries.

func (*Service) Snapshot

func (s *Service) Snapshot() Config

Snapshot returns a sanitized copy (no plain bind password).

func (*Service) StartConfigRefresh added in v0.5.324

func (s *Service) StartConfigRefresh(ctx context.Context, store SettingsStore, interval time.Duration)

StartConfigRefresh — v0.5.324. Background goroutine that re-reads the persisted LDAP config from the shared store every `interval`. Closes the multi-pod gap where one pod wrote new settings but other pods kept serving stale in-memory cfg until restart. interval ≤ 0 → 30s.

func (*Service) TestConnection

func (s *Service) TestConnection(ctx context.Context, override *Config) error

TestConnection establishes + service-binds + closes. Returns nil on success so the UI's "Test connection" button can flip green.

type SettingsStore

type SettingsStore interface {
	GetSetting(ctx context.Context, key string) ([]byte, error)
	PutSetting(ctx context.Context, key string, value []byte) error
}

Jump to

Keyboard shortcuts

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