This commit is contained in:
@@ -353,6 +353,9 @@ settings:
|
|||||||
auth_mode: required
|
auth_mode: required
|
||||||
ldap_bind_address: ldaps://ad01.example.com:636
|
ldap_bind_address: ldaps://ad01.example.com:636
|
||||||
ldap_base_dn: DC=example,DC=com
|
ldap_base_dn: DC=example,DC=com
|
||||||
|
# Optional performance scopes; default to ldap_base_dn when omitted.
|
||||||
|
ldap_user_base_dn: OU=Users,DC=example,DC=com
|
||||||
|
ldap_group_base_dn: OU=Groups,DC=example,DC=com
|
||||||
auth_group_role_mappings:
|
auth_group_role_mappings:
|
||||||
"CN=vctp-viewers,OU=Groups,DC=example,DC=com": viewer
|
"CN=vctp-viewers,OU=Groups,DC=example,DC=com": viewer
|
||||||
"CN=vctp-admins,OU=Groups,DC=example,DC=com": admin
|
"CN=vctp-admins,OU=Groups,DC=example,DC=com": admin
|
||||||
@@ -511,6 +514,8 @@ Authentication:
|
|||||||
- `settings.ldap_groups` empty/omitted means no allowlist filter, but mapped-role requirement still applies.
|
- `settings.ldap_groups` empty/omitted means no allowlist filter, but mapped-role requirement still applies.
|
||||||
- `settings.ldap_bind_address`: LDAP/LDAPS URL used for authentication.
|
- `settings.ldap_bind_address`: LDAP/LDAPS URL used for authentication.
|
||||||
- `settings.ldap_base_dn`: LDAP base DN for user/group lookups.
|
- `settings.ldap_base_dn`: LDAP base DN for user/group lookups.
|
||||||
|
- `settings.ldap_user_base_dn`: optional user lookup base DN; defaults to `settings.ldap_base_dn`.
|
||||||
|
- `settings.ldap_group_base_dn`: optional group lookup base DN; defaults to `settings.ldap_base_dn`.
|
||||||
- `settings.ldap_trust_cert_file`: optional CA cert file for LDAP TLS.
|
- `settings.ldap_trust_cert_file`: optional CA cert file for LDAP TLS.
|
||||||
- `settings.ldap_disable_validation`: disables LDAP TLS cert validation.
|
- `settings.ldap_disable_validation`: disables LDAP TLS cert validation.
|
||||||
- `settings.ldap_insecure`: insecure LDAP TLS mode.
|
- `settings.ldap_insecure`: insecure LDAP TLS mode.
|
||||||
|
|||||||
+23
-2
@@ -25,6 +25,8 @@ var (
|
|||||||
type LDAPConfig struct {
|
type LDAPConfig struct {
|
||||||
BindAddress string
|
BindAddress string
|
||||||
BaseDN string
|
BaseDN string
|
||||||
|
UserBaseDN string
|
||||||
|
GroupBaseDN string
|
||||||
TrustCertFile string
|
TrustCertFile string
|
||||||
DisableValidation bool
|
DisableValidation bool
|
||||||
Insecure bool
|
Insecure bool
|
||||||
@@ -45,6 +47,8 @@ type LDAPIdentity struct {
|
|||||||
type LDAPAuthenticator struct {
|
type LDAPAuthenticator struct {
|
||||||
bindAddress string
|
bindAddress string
|
||||||
baseDN string
|
baseDN string
|
||||||
|
userBaseDN string
|
||||||
|
groupBaseDN string
|
||||||
trustCertFile string
|
trustCertFile string
|
||||||
disableValidation bool
|
disableValidation bool
|
||||||
insecure bool
|
insecure bool
|
||||||
@@ -54,6 +58,8 @@ type LDAPAuthenticator struct {
|
|||||||
func NewLDAPAuthenticator(cfg LDAPConfig) (*LDAPAuthenticator, error) {
|
func NewLDAPAuthenticator(cfg LDAPConfig) (*LDAPAuthenticator, error) {
|
||||||
bindAddress := strings.TrimSpace(cfg.BindAddress)
|
bindAddress := strings.TrimSpace(cfg.BindAddress)
|
||||||
baseDN := strings.TrimSpace(cfg.BaseDN)
|
baseDN := strings.TrimSpace(cfg.BaseDN)
|
||||||
|
userBaseDN := strings.TrimSpace(cfg.UserBaseDN)
|
||||||
|
groupBaseDN := strings.TrimSpace(cfg.GroupBaseDN)
|
||||||
trustCertFile := strings.TrimSpace(cfg.TrustCertFile)
|
trustCertFile := strings.TrimSpace(cfg.TrustCertFile)
|
||||||
|
|
||||||
if bindAddress == "" {
|
if bindAddress == "" {
|
||||||
@@ -62,6 +68,12 @@ func NewLDAPAuthenticator(cfg LDAPConfig) (*LDAPAuthenticator, error) {
|
|||||||
if baseDN == "" {
|
if baseDN == "" {
|
||||||
return nil, fmt.Errorf("%w: base DN is required", ErrInvalidLDAPConfig)
|
return nil, fmt.Errorf("%w: base DN is required", ErrInvalidLDAPConfig)
|
||||||
}
|
}
|
||||||
|
if userBaseDN == "" {
|
||||||
|
userBaseDN = baseDN
|
||||||
|
}
|
||||||
|
if groupBaseDN == "" {
|
||||||
|
groupBaseDN = baseDN
|
||||||
|
}
|
||||||
if _, err := url.ParseRequestURI(bindAddress); err != nil {
|
if _, err := url.ParseRequestURI(bindAddress); err != nil {
|
||||||
return nil, fmt.Errorf("%w: bind address must be a valid URL: %v", ErrInvalidLDAPConfig, err)
|
return nil, fmt.Errorf("%w: bind address must be a valid URL: %v", ErrInvalidLDAPConfig, err)
|
||||||
}
|
}
|
||||||
@@ -74,6 +86,8 @@ func NewLDAPAuthenticator(cfg LDAPConfig) (*LDAPAuthenticator, error) {
|
|||||||
return &LDAPAuthenticator{
|
return &LDAPAuthenticator{
|
||||||
bindAddress: bindAddress,
|
bindAddress: bindAddress,
|
||||||
baseDN: baseDN,
|
baseDN: baseDN,
|
||||||
|
userBaseDN: userBaseDN,
|
||||||
|
groupBaseDN: groupBaseDN,
|
||||||
trustCertFile: trustCertFile,
|
trustCertFile: trustCertFile,
|
||||||
disableValidation: cfg.DisableValidation,
|
disableValidation: cfg.DisableValidation,
|
||||||
insecure: cfg.Insecure,
|
insecure: cfg.Insecure,
|
||||||
@@ -119,6 +133,10 @@ func (a *LDAPAuthenticator) AuthenticateAndFetchGroups(ctx context.Context, user
|
|||||||
if rewrittenToUPN {
|
if rewrittenToUPN {
|
||||||
identity.Diagnostics = append(identity.Diagnostics, "bind_username_rewritten_to_upn")
|
identity.Diagnostics = append(identity.Diagnostics, "bind_username_rewritten_to_upn")
|
||||||
}
|
}
|
||||||
|
identity.Diagnostics = append(identity.Diagnostics,
|
||||||
|
"user_lookup_base_dn="+a.userBaseDN,
|
||||||
|
"group_lookup_base_dn="+a.groupBaseDN,
|
||||||
|
)
|
||||||
if whoami, err := conn.WhoAmI(nil); err != nil {
|
if whoami, err := conn.WhoAmI(nil); err != nil {
|
||||||
identity.Diagnostics = append(identity.Diagnostics, fmt.Sprintf("whoami_failed:%v", err))
|
identity.Diagnostics = append(identity.Diagnostics, fmt.Sprintf("whoami_failed:%v", err))
|
||||||
} else if boundDN := parseWhoAmIDN(whoami.AuthzID); boundDN != "" {
|
} else if boundDN := parseWhoAmIDN(whoami.AuthzID); boundDN != "" {
|
||||||
@@ -131,7 +149,7 @@ func (a *LDAPAuthenticator) AuthenticateAndFetchGroups(ctx context.Context, user
|
|||||||
}
|
}
|
||||||
|
|
||||||
userLookupStartedAt := time.Now()
|
userLookupStartedAt := time.Now()
|
||||||
entry, lookupStrategy, err := a.lookupUserEntry(conn, bindUsername, identity.UserDN)
|
entry, lookupStrategy, err := a.lookupUserEntry(conn, inputUsername, identity.UserDN)
|
||||||
identity.UserLookupDuration = time.Since(userLookupStartedAt)
|
identity.UserLookupDuration = time.Since(userLookupStartedAt)
|
||||||
identity.Diagnostics = append(identity.Diagnostics, fmt.Sprintf("user_lookup_duration_ms=%d", identity.UserLookupDuration.Milliseconds()))
|
identity.Diagnostics = append(identity.Diagnostics, fmt.Sprintf("user_lookup_duration_ms=%d", identity.UserLookupDuration.Milliseconds()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,6 +347,9 @@ func (a *LDAPAuthenticator) lookupUserEntry(conn *ldap.Conn, username string, us
|
|||||||
if entry != nil {
|
if entry != nil {
|
||||||
return entry, "principal_upn", nil
|
return entry, "principal_upn", nil
|
||||||
}
|
}
|
||||||
|
// For UPN principals, avoid fallback attribute probes that are unlikely to match
|
||||||
|
// and can be expensive on large directory trees.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := a.searchUserByAttribute(conn, "sAMAccountName", principal)
|
entry, err := a.searchUserByAttribute(conn, "sAMAccountName", principal)
|
||||||
@@ -359,7 +380,7 @@ func (a *LDAPAuthenticator) searchUserByAttribute(conn *ldap.Conn, attribute str
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
searchRes, err := conn.Search(ldap.NewSearchRequest(
|
||||||
a.baseDN,
|
a.userBaseDN,
|
||||||
ldap.ScopeWholeSubtree,
|
ldap.ScopeWholeSubtree,
|
||||||
ldap.NeverDerefAliases,
|
ldap.NeverDerefAliases,
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ type SettingsYML struct {
|
|||||||
LDAPGroups []string `yaml:"ldap_groups"`
|
LDAPGroups []string `yaml:"ldap_groups"`
|
||||||
LDAPBindAddress string `yaml:"ldap_bind_address"`
|
LDAPBindAddress string `yaml:"ldap_bind_address"`
|
||||||
LDAPBaseDN string `yaml:"ldap_base_dn"`
|
LDAPBaseDN string `yaml:"ldap_base_dn"`
|
||||||
|
LDAPUserBaseDN string `yaml:"ldap_user_base_dn"`
|
||||||
|
LDAPGroupBaseDN string `yaml:"ldap_group_base_dn"`
|
||||||
LDAPTrustCertFile string `yaml:"ldap_trust_cert_file"`
|
LDAPTrustCertFile string `yaml:"ldap_trust_cert_file"`
|
||||||
LDAPDisableValidation bool `yaml:"ldap_disable_validation"`
|
LDAPDisableValidation bool `yaml:"ldap_disable_validation"`
|
||||||
LDAPInsecure bool `yaml:"ldap_insecure"`
|
LDAPInsecure bool `yaml:"ldap_insecure"`
|
||||||
@@ -284,6 +286,8 @@ func applyDefaultsAndValidateSettings(cfg *SettingsYML) error {
|
|||||||
s.AuthJWTSigningKey = strings.TrimSpace(s.AuthJWTSigningKey)
|
s.AuthJWTSigningKey = strings.TrimSpace(s.AuthJWTSigningKey)
|
||||||
s.LDAPBindAddress = strings.TrimSpace(s.LDAPBindAddress)
|
s.LDAPBindAddress = strings.TrimSpace(s.LDAPBindAddress)
|
||||||
s.LDAPBaseDN = strings.TrimSpace(s.LDAPBaseDN)
|
s.LDAPBaseDN = strings.TrimSpace(s.LDAPBaseDN)
|
||||||
|
s.LDAPUserBaseDN = strings.TrimSpace(s.LDAPUserBaseDN)
|
||||||
|
s.LDAPGroupBaseDN = strings.TrimSpace(s.LDAPGroupBaseDN)
|
||||||
s.LDAPTrustCertFile = strings.TrimSpace(s.LDAPTrustCertFile)
|
s.LDAPTrustCertFile = strings.TrimSpace(s.LDAPTrustCertFile)
|
||||||
s.LDAPGroups = compactTrimmedStrings(s.LDAPGroups)
|
s.LDAPGroups = compactTrimmedStrings(s.LDAPGroups)
|
||||||
|
|
||||||
@@ -340,6 +344,12 @@ func applyDefaultsAndValidateSettings(cfg *SettingsYML) error {
|
|||||||
if s.LDAPBaseDN == "" {
|
if s.LDAPBaseDN == "" {
|
||||||
return errors.New("settings.ldap_base_dn is required when settings.auth_enabled=true")
|
return errors.New("settings.ldap_base_dn is required when settings.auth_enabled=true")
|
||||||
}
|
}
|
||||||
|
if s.LDAPUserBaseDN == "" {
|
||||||
|
s.LDAPUserBaseDN = s.LDAPBaseDN
|
||||||
|
}
|
||||||
|
if s.LDAPGroupBaseDN == "" {
|
||||||
|
s.LDAPGroupBaseDN = s.LDAPBaseDN
|
||||||
|
}
|
||||||
if len(s.AuthGroupRoleMappings) == 0 {
|
if len(s.AuthGroupRoleMappings) == 0 {
|
||||||
return errors.New("settings.auth_group_role_mappings must define at least one mapping when settings.auth_enabled=true")
|
return errors.New("settings.auth_group_role_mappings must define at least one mapping when settings.auth_enabled=true")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,12 @@ func TestReadYMLSettingsAcceptsValidAuthConfigAndNormalizesMappings(t *testing.T
|
|||||||
if len(got.LDAPGroups) != 1 || got.LDAPGroups[0] != "cn=vctp-viewers,ou=groups,dc=example,dc=com" {
|
if len(got.LDAPGroups) != 1 || got.LDAPGroups[0] != "cn=vctp-viewers,ou=groups,dc=example,dc=com" {
|
||||||
t.Fatalf("expected ldap_groups to be compacted+trimmed, got %#v", got.LDAPGroups)
|
t.Fatalf("expected ldap_groups to be compacted+trimmed, got %#v", got.LDAPGroups)
|
||||||
}
|
}
|
||||||
|
if got.LDAPUserBaseDN != "dc=example,dc=com" {
|
||||||
|
t.Fatalf("expected default ldap_user_base_dn to fall back to ldap_base_dn, got %q", got.LDAPUserBaseDN)
|
||||||
|
}
|
||||||
|
if got.LDAPGroupBaseDN != "dc=example,dc=com" {
|
||||||
|
t.Fatalf("expected default ldap_group_base_dn to fall back to ldap_base_dn, got %q", got.LDAPGroupBaseDN)
|
||||||
|
}
|
||||||
if got.AuthGroupRoleMappings["cn=vctp-admins,ou=groups,dc=example,dc=com"] != authRoleAdmin {
|
if got.AuthGroupRoleMappings["cn=vctp-admins,ou=groups,dc=example,dc=com"] != authRoleAdmin {
|
||||||
t.Fatalf("expected admin mapping to normalize role to %q, got %#v", authRoleAdmin, got.AuthGroupRoleMappings)
|
t.Fatalf("expected admin mapping to normalize role to %q, got %#v", authRoleAdmin, got.AuthGroupRoleMappings)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
"username", username,
|
"username", username,
|
||||||
"ldap_bind_address", cfg.LDAPBindAddress,
|
"ldap_bind_address", cfg.LDAPBindAddress,
|
||||||
"ldap_base_dn", cfg.LDAPBaseDN,
|
"ldap_base_dn", cfg.LDAPBaseDN,
|
||||||
|
"ldap_user_base_dn", cfg.LDAPUserBaseDN,
|
||||||
|
"ldap_group_base_dn", cfg.LDAPGroupBaseDN,
|
||||||
"ldap_group_requirements", limitStrings(cfg.LDAPGroups, maxDebugLogListItems),
|
"ldap_group_requirements", limitStrings(cfg.LDAPGroups, maxDebugLogListItems),
|
||||||
"auth_group_role_mapping_keys", limitStrings(sortedStringMapKeys(cfg.AuthGroupRoleMappings), maxDebugLogListItems),
|
"auth_group_role_mapping_keys", limitStrings(sortedStringMapKeys(cfg.AuthGroupRoleMappings), maxDebugLogListItems),
|
||||||
"ldap_insecure", cfg.LDAPInsecure,
|
"ldap_insecure", cfg.LDAPInsecure,
|
||||||
@@ -95,6 +97,8 @@ func (h *Handler) AuthLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
ldapAuth, err := newLDAPAuthenticator(auth.LDAPConfig{
|
ldapAuth, err := newLDAPAuthenticator(auth.LDAPConfig{
|
||||||
BindAddress: cfg.LDAPBindAddress,
|
BindAddress: cfg.LDAPBindAddress,
|
||||||
BaseDN: cfg.LDAPBaseDN,
|
BaseDN: cfg.LDAPBaseDN,
|
||||||
|
UserBaseDN: cfg.LDAPUserBaseDN,
|
||||||
|
GroupBaseDN: cfg.LDAPGroupBaseDN,
|
||||||
TrustCertFile: cfg.LDAPTrustCertFile,
|
TrustCertFile: cfg.LDAPTrustCertFile,
|
||||||
DisableValidation: cfg.LDAPDisableValidation,
|
DisableValidation: cfg.LDAPDisableValidation,
|
||||||
Insecure: cfg.LDAPInsecure,
|
Insecure: cfg.LDAPInsecure,
|
||||||
|
|||||||
Reference in New Issue
Block a user