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
- Variables
- func CheckConnection(ctx context.Context, db *store.Store) error
- type AuthResult
- type Config
- type GroupMembers
- type LookupResult
- type Presence
- type Source
- func (s *Source) Authenticate(ctx context.Context, username, password string) (AuthResult, error)
- func (s *Source) Groups(ctx context.Context, id auth.Identity) (auth.GroupResult, error)
- func (s *Source) GroupsBySubject(ctx context.Context, subject string) (auth.GroupResult, error)
- func (s *Source) Lookup(ctx context.Context, id auth.Identity) (LookupResult, error)
- func (s *Source) LookupBySubject(ctx context.Context, subject string) (LookupResult, error)
- func (s *Source) MembersOf(ctx context.Context, group string) (GroupMembers, error)
- func (s *Source) Probe(ctx context.Context) error
- func (s *Source) ResolvesByUsername() bool
- func (s *Source) Test(ctx context.Context) error
Constants ¶
const ( ScopeDirect = "direct" ScopeSubtree = "subtree" )
Grup kapsam değerleri.
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 ¶
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.
var ErrNotAUUID = errors.New("ldap: value is not a directory UUID")
ErrNotAUUID: değer, kararlı bir kimlik gibi görünmüyor.
var ErrNotConfigured = errors.New("ldap: not configured")
ErrNotConfigured: LDAP ayarlanmamış. Hata değil bir DURUM — kurulum grupları OIDC claim'inden okuyor olabilir.
var SecretKeys = map[string]bool{KeyBindPassword: true}
SecretKeys, şifrelenerek saklanması gereken ayarlar.
Functions ¶
func CheckConnection ¶
* 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.
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 Source ¶
type Source struct {
// contains filtered or unexported fields
}
Source, LDAP'a soran GroupSource gerçekleştirmesi.
func New ¶
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 ¶
SourceFromStore, ayarları okuyup kaynağı kurar. LDAP ayarlanmamışsa ErrNotConfigured.
func (*Source) Authenticate ¶
* 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 ¶
* 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 ¶
* 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 ¶
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 ¶
* 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 ¶
* 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 ¶
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 ¶
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.