ldap

package
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 5, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package ldap reads group membership from an LDAP directory.

NE YAPAR: kullanıcının gruplarını sorar. NE YAPMAZ: kimlik doğrulamaz.

Bu ayrım bilinçli bir mimari karar. LDAP'ta kimlik doğrulama "bind" demektir: kullanıcının PAROLASI postern'e verilir ve postern onu dizine sunar. Bu, "parola bastion'a hiç uğramaz" duruşumuzun tam tersi olurdu ve ele geçirilen bir postern kurumun bütün parolalarını toplayabilirdi. Kimliği OIDC'de bırakıyoruz; buraya yalnızca kendi servis hesabımızla bağlanıyor ve yetki soruyoruz.

Kazandığımız şey TAZELİK: OIDC token'ındaki grup claim'i giriş anında dondurulmuştur, dizin ise her sorguda güncel cevap verir.

Index

Constants

View Source
const (
	ScopeDirect  = "direct"
	ScopeSubtree = "subtree"
)

Grup kapsam değerleri.

View Source
const (
	KeyURL    = "ldap.url"
	KeyBindDN = "ldap.bind_dn"
	// #nosec G101 -- kimlik bilgisi değil, settings tablosunun ANAHTAR adı
	KeyBindPassword   = "ldap.bind_password"
	KeyUserBase       = "ldap.user_base"
	KeyUserFilter     = "ldap.user_filter"
	KeyGroupAttribute = "ldap.group_attribute"
	KeyGroupBase      = "ldap.group_base"
	KeyGroupFilter    = "ldap.group_filter"
	KeyGroupNameFrom  = "ldap.group_name_from"
	KeyGroupScope     = "ldap.group_scope"

	// KeyAdminGroup, auth paketine taşındı: grup adı OIDC claim'inden
	// de gelebiliyor ve iki kaynağın paylaştığı bir kavram birinin
	// paketinde duramaz. Bu ad geriye dönük uyumluluk için duruyor.
	KeyAdminGroup = auth.KeyAdminGroup
)

Ayar anahtarları. Noktalı ad alanı settings tablosunun sözleşmesi.

Variables

View Source
var ErrEmptySecret = errors.New("ldap: empty password would be an anonymous bind")

ErrEmptySecret, parolanın boş olduğu.

  • ⚠️ AYRI BİR HATA OLMASI ŞART VE BU BİR GÜVENLİK KONTROLÜ. *
  • LDAP'ta DN verip parolayı boş bırakmak "unauthenticated bind"dir ve
  • sonucu SUNUCUNUN YAPILANDIRMASINA bağlıdır. Ölçtük: bu depodaki
  • OpenLDAP onu reddediyor (Result Code 53, "unauthenticated bind
  • disallowed"). Active Directory ise varsayılan olarak anonim bind
  • gibi ele alıp BAŞARILI döner. *
  • Kontrolün burada olmasının sebebi tam olarak bu belirsizlik:
  • postern, bağlandığı dizinin nasıl yapılandırıldığını bilemez ve
  • "parola alanını boş bırakan herkes içeri girer" hâlini uzaktaki bir
  • ayara emanet edemez. Boş parola bind'e HİÇ ulaşmıyor.
View Source
var ErrNotAUUID = errors.New("ldap: value is not a directory UUID")

ErrNotAUUID: değer, kararlı bir kimlik gibi görünmüyor.

View Source
var ErrNotConfigured = errors.New("ldap: not configured")

ErrNotConfigured: LDAP ayarlanmamış. Hata değil bir DURUM — kurulum grupları OIDC claim'inden okuyor olabilir.

View Source
var SecretKeys = map[string]bool{KeyBindPassword: true}

SecretKeys, şifrelenerek saklanması gereken ayarlar.

Functions

func CheckConnection

func CheckConnection(ctx context.Context, db *store.Store) error

* CheckConnection, YALNIZCA bağlantıyı ve servis hesabını sınar. * * NEDEN AYRI: SourceFromStore dokuz alanın hepsini istiyor (New tam * yapılandırma doğruluyor), dolayısıyla "URL'im ve servis hesabım doğru * mu" sorusu ancak her şey doldurulduktan sonra sorulabiliyordu. * Sihirbazın ilk adımını sınanamaz yapan buydu: dokuz alandan hangisinin * yanlış olduğunu, dokuzu da yazdıktan sonra öğreniyordunuz. * * ⚠️ SAKLANAN DEĞERLERİ okur, gönderileni değil — Test ile aynı sözleşme. * Parola yazılırken sınamak, panelin sunucuya kimlik bilgisi ileten ayrı * bir ucu olması demekti; oysa aynı parola zaten kaydedilirken gidiyor * ve tek doğruluk kaynağı saklanan değer olmalı.

Types

type AuthResult

type AuthResult struct {
	// Presence, kullanıcının dizinde bulunup bulunmadığı.
	Presence Presence

	// Authenticated, parolanın DOĞRU olduğu. Presence present olsa da
	// false olabilir — kullanıcı var, parola yanlış.
	Authenticated bool

	// Disabled, hesabın dizinde kapatılmış olduğu (bkz. liveness.go).
	// Parola doğru olsa bile giriş verilmemeli.
	Disabled       bool
	DisabledReason string

	/*
	 * PasswordExpired, dizin "parola doğru ama süresi dolmuş / önce
	 * değiştirmelisin" dedi.
	 *
	 * ⚠️ Authenticated FALSE kalıyor ve bu doğru: giriş verilmiyor.
	 * Ayrı bir alan olmasının sebebi SÖYLENEN ŞEY: bu hâl "yanlış
	 * parola" olarak gösterildiğinde kullanıcı doğru parolasını
	 * defalarca deniyor, sonra da yanlış yerde — postern'de — arıza
	 * arıyor. Oysa yapılacak iş belli ve postern'de değil: parolayı
	 * kurumun kendi aracıyla değiştirmek.
	 */
	PasswordExpired bool

	Groups     []string
	OutOfScope []string

	/*
	 * Identity, dizinin verdiği KARARLI ve opak kimlik (objectGUID ya
	 * da entryUUID), kanonik biçimde. Boş: bu dizin ya da bu servis
	 * hesabı böyle bir değer vermiyor.
	 *
	 * ⚠️ Eşleştirmenin ASIL anahtarı bu, kullanıcı adı değil. Ad
	 * dizinde değişir (ölçüldü: yeniden adlandırma ve OU taşıma bu
	 * değeri değiştirmiyor) ve yeniden kullanılır (ölçüldü: aynı adla
	 * yeniden açılan kayıt FARKLI kimlik alıyor).
	 */
	Identity string

	// IdentityError, öznitelik geldi ama çözümlenemedi. "Yok" ile aynı
	// şey değil ve teşhis ekranında ayrı görünmeli.
	IdentityError string
}

AuthResult, bir bind denemesinin sonucu.

func AuthenticateFromStore

func AuthenticateFromStore(ctx context.Context, db *store.Store, username, password string) (AuthResult, error)

AuthenticateFromStore, saklanan yapılandırmayla bind dener.

⚠️ SAKLANAN adres kullanılıyor: parola yalnızca kurumun kaydettiği dizine gidiyor, isteğin taşıdığı bir adrese değil.

type Config

type Config struct {
	// URL, "ldaps://host:636" ya da "ldap://host:389".
	URL string

	// BindDN/BindPassword, POSTERN'İN servis hesabı — kullanıcının değil.
	BindDN       string
	BindPassword string

	// UserBase ve UserFilter, kullanıcıyı bulmak için. Filtredeki %s
	// kullanıcı adıyla değiştirilir: "(uid=%s)" ya da
	// "(sAMAccountName=%s)" (Active Directory).
	UserBase   string
	UserFilter string

	// GroupAttribute doluysa gruplar kullanıcı girdisindeki bu
	// öznitelikten okunur (çoğu dizinde "memberOf"). Boşsa GroupBase +
	// GroupFilter ile arama yapılır — eski şemalarda üyelik grubun
	// üstünde durur, kullanıcının değil.
	GroupAttribute string

	GroupBase   string
	GroupFilter string

	// GroupNameFrom, grup adının nasıl çıkarılacağı: "cn" (varsayılan)
	// ya da "dn".
	//
	// "cn" seçilmesinin sebebi eşleme tablosunun OIDC ile ORTAK olması:
	// token'dan gelen "sysadmins" ile dizinden gelen
	// "cn=sysadmins,ou=groups,dc=..." aynı isim uzayına düşmeli. Bedeli:
	// farklı OU'lardaki aynı adlı iki grup birleşir. Bu kabul edilemezse
	// "dn" seçilir ve eşlemeler tam DN yazılır.
	GroupNameFrom string

	/*
	 * GroupScope, grubun taban DN'e göre nerede durabileceği:
	 * "direct" (varsayılan) ya da "subtree".
	 *
	 * ⚠️ VARSAYILAN "direct" VE BU BİR GÜVENLİK KARARI.
	 *
	 * GroupNameFrom="cn" iken grup adı DN'in yalnızca ilk bileşeninden
	 * okunuyor. Ölçüldü:
	 *
	 *     normalize("cn=sysadmins,ou=teams,ou=groups,dc=corp") → "sysadmins"
	 *
	 * LDAP'ta benzersizlik EBEVEYN BAŞINA. Yani cn=sysadmins zaten
	 * varken, bir alt-OU'da aynı adla ikinci bir grup açılabiliyor ve
	 * postern ikisini de aynı role çözüyordu. Grup açma yetkisi
	 * devredilmiş her kurumda (self-servis portal, departman OU'su,
	 * yüklenici alt ağacı) bu, "istediğim rolü kendime basarım"
	 * demekti.
	 *
	 * "direct" bunu kapatır: grup, taban DN'in DOĞRUDAN çocuğu olmak
	 * zorunda, ve orada benzersizliği dizinin kendisi garanti ediyor.
	 *
	 * "subtree" YALNIZCA GroupNameFrom="dn" ile geçerli — orada eşleme
	 * anahtarı tam DN olduğu için çakışma zaten imkânsız. cn ile
	 * birlikte reddediliyor (bkz. New).
	 */
	GroupScope string
}

Config, dizine bağlanmak ve grup aramak için gerekenler.

Değerler veritabanındaki settings tablosundan gelir (S5.1), config dosyasından değil: panelden düzenlenip test edilebilsinler ve servis hesabı parolası düz metin olarak dosyada gezmesin diye.

func LoadConfig

func LoadConfig(ctx context.Context, db *store.Store) (Config, error)

LoadConfig, ayarları veritabanından okur.

ldap.url yoksa ErrNotConfigured döner: çağıran claim kaynağına düşer.

type GroupMembers

type GroupMembers struct {
	// Usernames, user_filter'ın eşleştireceği adlar.
	Usernames []string
	// Truncated, dizinin daha fazla üye bildirdiği.
	Truncated bool
}

GroupMembers, bir grubun üyelerinin KULLANICI ADLARINI döner.

type LookupResult

type LookupResult struct {
	Presence Presence
	Groups   []string

	/*
	 * Disabled, dizinin bu hesabı KAPATTIĞINI söylemesi.
	 *
	 * ⚠️ PresencePresent İLE BİRLİKTE GELEBİLİR ve gelmesi normal:
	 * bir hesabı devre dışı bırakmak girişi silmez, grup üyeliklerini
	 * de kaldırmaz. Yalnızca gruplara bakan bir çağıran o hesabı
	 * "burada ve şu rollere sahip" diye okur — işten ayrılma ve olay
	 * müdahalesinde atılan İLK adımı görmezden gelmiş olur.
	 */
	Disabled       bool
	DisabledReason string

	/*
	 * Identity, dizinin verdiği KARARLI ve opak kimlik: AD'de
	 * objectGUID, RFC 4530 dizinlerinde entryUUID. Kanonik küçük
	 * harfli UUID biçiminde; boşsa dizin (ya da servis hesabı) böyle
	 * bir değer vermiyor.
	 *
	 * ⚠️ Var olma sebebi: kullanıcı adı bir kimlik DEĞİL. Ölçüldü
	 * (OpenLDAP): yeniden adlandırma ve OU taşıma bu değeri
	 * DEĞİŞTİRMİYOR, silinip aynı adla yeniden açmak ise DEĞİŞTİRİYOR
	 * — yani ayrılan çalışanın adını alan kişi eski hesabı devralamaz.
	 * 011 göçünün OIDC için kapattığı açığın dizin karşılığı.
	 */
	Identity string

	// IdentityError, kimlik özniteliği geldi ama çözümlenemedi.
	IdentityError string

	// OutOfScope, kullanıcının üye olduğu ama grup KAPSAMI dışında
	// kaldığı için sayılmayan grupların ham DN'leri.
	//
	// Teşhis için: kapsam varsayılanı "direct" olduğunda, gruplarını bir
	// OU daha derinde tutan bir kurulum yükseltmeden sonra rol
	// kaybeder. Bunu sessizce yapmak, operatörü kaybolan yetkinin
	// sebebini arayarak saatlerce dolaştırırdı.
	OutOfScope []string
}

LookupResult, bir kullanıcının dizindeki durumu.

type Presence

type Presence int
const (
	// PresenceUnknown: dizin cevap veremedi. Hiçbir şey çıkarılamaz.
	PresenceUnknown Presence = iota
	// PresencePresent: dizin cevap verdi ve kullanıcı orada.
	PresencePresent
	// PresenceAbsent: dizin BAŞARIYLA cevap verdi ve kullanıcı orada DEĞİL.
	PresenceAbsent
)

func (Presence) String

func (p Presence) String() string

type Source

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

Source, LDAP'a soran GroupSource gerçekleştirmesi.

func New

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

New, yapılandırmayı doğrular ve kaynağı kurar. Bağlantı BURADA kurulmaz: dizin o an erişilemez olabilir ve postern'in açılmaması için sebep değildir — sorgu anında bağlanılır.

func SourceFromStore

func SourceFromStore(ctx context.Context, db *store.Store) (*Source, error)

SourceFromStore, ayarları okuyup kaynağı kurar. LDAP ayarlanmamışsa ErrNotConfigured.

func (*Source) Authenticate

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

* Authenticate, kullanıcıyı dizin parolasıyla doğrular. * * Sıra önemli: * 1. Boş parola REDDEDİLİR (anonim bind tuzağı). * 2. Servis hesabıyla kullanıcı aranır — DN, grup ve hesap durumu. * 3. Kullanıcının DN'i ile AYRI bir bağlantıda bind denenir. * * Üçüncü adımın ayrı bağlantıda olması şart: bind, bağlantının kimliğini * DEĞİŞTİRİR. Servis hesabının bağlantısı üzerinde kullanıcı bind'i * yapmak, o bağlantıyı sonraki sorgular için kullanıcının yetkisine * düşürürdü.

func (*Source) Groups

func (s *Source) Groups(ctx context.Context, id auth.Identity) (auth.GroupResult, error)

* Groups, kullanıcının gruplarını ÜÇ DEĞERLİ cevapla döner * (auth.GroupSource). * * ⚠️ ARTIK Lookup'IN ÜSTÜNE KURULU. Eskiden burada ayrı bir arama vardı * ve kullanıcı bulunamadığında boş dilim dönüyordu — yorumu "dizin * arızası ile 'bu kişi burada yok' karıştırılmasın" diyordu ama * gerçekleştirme, "burada yok" ile "burada ve hiçbir grupta değil"i * karıştırıyordu. Ölçülen bedeli: adı dizinde tutmayan kullanıcı her * girişte bütün SSO rollerini kaybediyordu. * * Lookup bu ayrımı zaten yapıyor ve senkronizasyon ona dayanıyor; giriş * yolunun ondan farklı bir gerçeğe bakması için bir sebep yoktu.

func (*Source) GroupsBySubject

func (s *Source) GroupsBySubject(ctx context.Context, subject string) (auth.GroupResult, error)

* GroupsBySubject, auth.SubjectResolver'ı uygular. * * Lookup ile aynı üç değerli sözleşme: "bulamadım" ile "cevap * veremedim" ayrı kalıyor, çünkü çağıran ilkini iptale, ikincisini * beklemeye çeviriyor.

func (*Source) Lookup

func (s *Source) Lookup(ctx context.Context, id auth.Identity) (LookupResult, error)

Lookup, kullanıcının dizinde olup olmadığını ve gruplarını döner.

SINIFLANDIRMA KURALI — dar tutulması bilinçli:

PresenceAbsent  YALNIZCA sunucunun BAŞARIYLA cevapladığı ve sıfır
                giriş döndürdüğü arama için.
PresenceUnknown diğer HER ŞEY: bağlantı/TLS/bind hatası, herhangi bir
                LDAP sonuç kodu (yanlış base DN'de gelen 32
                NoSuchObject dahil), ve birden fazla giriş dönmesi.

32 NoSuchObject'in Unknown sayılması özellikle önemli: yanlış ya da yeniden adlandırılmış bir base DN her kullanıcı için o kodu döndürür ve "herkes silinmiş" gibi görünür.

func (*Source) LookupBySubject

func (s *Source) LookupBySubject(ctx context.Context, subject string) (LookupResult, error)

* LookupBySubject, kullanıcıyı KARARLI KİMLİĞİYLE çözer. * * ⚠️ VAR OLMA SEBEBİ: adla arama, yeniden adlandırılan kullanıcıyı * silinmiş kullanıcıdan ayırt edemiyor — ikisi de PresenceAbsent. Ve o * cevabı alan taraflar erişimi kesiyor. Yani dizinde adı değişen kişi, * hiçbir şey yapmadan bütün oturumlarını ve rollerini kaybediyordu. * * Kimliği bağlı olan herkes için ÇAĞRILMASI GEREKEN yol budur.

func (*Source) MembersOf

func (s *Source) MembersOf(ctx context.Context, group string) (GroupMembers, error)

* MembersOf, grubun üyelerini bulur. * * ⚠️ BU LİSTE TEK BAŞINA BİR CEVAP DEĞİL. Kimin yönetici olacağına * karar veren şey, kullanıcı BAŞINA çalışan çözümleme (Lookup) ve onun * kapsam kuralları. Burası yalnızca ADAYLARI çıkarıyor; çağıran her * adayı gerçek yoldan doğrulamak zorunda, yoksa önizleme girişten * farklı bir gerçeği gösterir — onay ekranının yapabileceği en kötü şey.

func (*Source) Probe

func (s *Source) Probe(ctx context.Context) error

Probe, dizinin ŞU AN veri döndürüp döndürmediğini sorar.

Test'ten farkı: Test "yapılandırma doğru mu" (taban nesnesi var mı) der; Probe "dizin şu an kullanıcılarıyla birlikte cevap veriyor mu" der. Sıfır kullanıcı döndüren bir dizin ya arızalıdır ya geri yükleme ortasındadır — herkesin silindiği bir şirket değildir.

Senkronizasyon TEK BİR kullanıcıya bile dokunmadan önce bunu çağırır.

func (*Source) ResolvesByUsername

func (s *Source) ResolvesByUsername() bool

ResolvesByUsername, dizin kaynağı adla sorgulanabilir (auth.UsernameResolver).

Zaten yaptığı tek şey bu: user_filter'daki %s kullanıcı adıyla değiştiriliyor. Token'a ihtiyacı yok, dolayısıyla anahtarla açılan bir oturumda da sorulabiliyor.

func (*Source) Test

func (s *Source) Test(ctx context.Context) error

Test, yapılandırmanın çalıştığını doğrular: bağlan, bind et, kullanıcı tabanını ara. Panelden "bağlantıyı test et" için.

Directories

Path Synopsis
Package ldaptest, testler için küçük bir LDAP sunucusu sağlar.
Package ldaptest, testler için küçük bir LDAP sunucusu sağlar.

Jump to

Keyboard shortcuts

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