Skip to content

Commit

Permalink
feat(authentication): ldap time replacements (#4483)
Browse files Browse the repository at this point in the history
This adds and utilizes several time replacements for both specialized LDAP implementations.

Closes #1964, Closes #1284
  • Loading branch information
james-d-elliott committed Dec 21, 2022
1 parent d0d80b4 commit d67554a
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 244 deletions.
43 changes: 27 additions & 16 deletions docs/content/en/reference/guides/ldap.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ search.

#### Users filter replacements

| Placeholder | Phase | Replacement |
|:------------------------:|:-------:|:-------------------------------------:|
| {username_attribute} | startup | The configured username attribute |
| {mail_attribute} | startup | The configured mail attribute |
| {display_name_attribute} | startup | The configured display name attribute |
| {input} | search | The input into the username field |
| Placeholder | Phase | Replacement |
|:-------------------------:|:-------:|:--------------------------------------------------------------------------------------------------------------:|
| {username_attribute} | startup | The configured username attribute |
| {mail_attribute} | startup | The configured mail attribute |
| {display_name_attribute} | startup | The configured display name attribute |
| {input} | search | The input into the username field |
| {date-time:generalized} | search | The current UTC time formatted as a LDAP generalized time in the format of `20060102150405.0Z` |
| {date-time:unix-epoch} | search | The current time formatted as a Unix epoch |
| {date-time:msft-nt-epoch} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft Active Directory attributes |

#### Groups filter replacements

Expand All @@ -92,16 +95,24 @@ Username column.

#### Filter defaults

The filters are probably the most important part to get correct when setting up LDAP. You want to exclude disabled
accounts. The active directory example has two attribute filters that accomplish this as an example (more examples would
be appreciated). The userAccountControl filter checks that the account is not disabled and the pwdLastSet makes sure that
value is not 0 which means the password requires changing at the next login.

| Implementation | Users Filter | Groups Filter |
|:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------:|
| custom | N/A | N/A |
| activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) |
| freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))) | (&(member={dn})(objectClass=groupOfNames)) |
The filters are probably the most important part to get correct when setting up LDAP. You want to exclude accounts under
the following conditions:

- The account is disabled or locked:
- The Active Directory implementation achieves this via the `(!(userAccountControl:1.2.840.113556.1.4.803:=2))` filter.
- The FreeIPA implementation achieves this via the `(!(nsAccountLock=TRUE))` filter.
- Their password is expired:
- The Active Directory implementation achieves this via the `(!(pwdLastSet=0))` filter.
- The FreeIPA implementation achieves this via the `(krbPasswordExpiration>={date-time:generalized})` filter.
- Their account is expired:
- The Active Directory implementation achieves this via the `(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:msft-nt-epoch}))` filter.
- The FreeIPA implementation achieves this via the `(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized}))` filter.

| Implementation | Users Filter | Groups Filter |
|:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------:|
| custom | N/A | N/A |
| activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:msft-nt-epoch}))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) |
| freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized}))) | (&(member={dn})(objectClass=groupOfNames)) |

##### Microsoft Active Directory sAMAccountType

Expand Down
13 changes: 10 additions & 3 deletions internal/authentication/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ const (
)

const (
ldapPlaceholderInput = "{input}"
ldapPlaceholderDistinguishedName = "{dn}"
ldapPlaceholderUsername = "{username}"
ldapPlaceholderInput = "{input}"
ldapPlaceholderDistinguishedName = "{dn}"
ldapPlaceholderUsername = "{username}"
ldapPlaceholderDateTimeGeneralized = "{date-time:generalized}"
ldapPlaceholderDateTimeMicrosoftNTTimeEpoch = "{date-time:msft-nt-epoch}"
ldapPlaceholderDateTimeUnixEpoch = "{date-time:unix-epoch}"
)

const (
ldapGeneralizedTimeDateTimeFormat = "20060102150405.0Z"
)

const (
Expand Down
40 changes: 30 additions & 10 deletions internal/authentication/ldap_user_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/x509"
"fmt"
"net"
"strconv"
"strings"

"github.com/go-ldap/ldap/v3"
Expand All @@ -23,15 +24,20 @@ type LDAPUserProvider struct {
log *logrus.Logger
factory LDAPClientFactory

clock utils.Clock

disableResetPassword bool

// Automatically detected LDAP features.
features LDAPSupportedFeatures

// Dynamically generated users values.
usersBaseDN string
usersAttributes []string
usersFilterReplacementInput bool
usersBaseDN string
usersAttributes []string
usersFilterReplacementInput bool
usersFilterReplacementDateTimeGeneralized bool
usersFilterReplacementDateTimeUnixEpoch bool
usersFilterReplacementDateTimeMicrosoftNTTimeEpoch bool

// Dynamically generated groups values.
groupsBaseDN string
Expand All @@ -41,14 +47,15 @@ type LDAPUserProvider struct {
groupsFilterReplacementDN bool
}

// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
// NewLDAPUserProvider creates a new instance of LDAPUserProvider with the ProductionLDAPClientFactory.
func NewLDAPUserProvider(config schema.AuthenticationBackend, certPool *x509.CertPool) (provider *LDAPUserProvider) {
provider = newLDAPUserProvider(*config.LDAP, config.PasswordReset.Disable, certPool, nil)
provider = NewLDAPUserProviderWithFactory(*config.LDAP, config.PasswordReset.Disable, certPool, NewProductionLDAPClientFactory())

return provider
}

func newLDAPUserProvider(config schema.LDAPAuthenticationBackend, disableResetPassword bool, certPool *x509.CertPool, factory LDAPClientFactory) (provider *LDAPUserProvider) {
// NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with the specified LDAPClientFactory.
func NewLDAPUserProviderWithFactory(config schema.LDAPAuthenticationBackend, disableResetPassword bool, certPool *x509.CertPool, factory LDAPClientFactory) (provider *LDAPUserProvider) {
if config.TLS == nil {
config.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS
}
Expand All @@ -74,6 +81,7 @@ func newLDAPUserProvider(config schema.LDAPAuthenticationBackend, disableResetPa
log: logging.Logger(),
factory: factory,
disableResetPassword: disableResetPassword,
clock: &utils.RealClock{},
}

provider.parseDynamicUsersConfiguration()
Expand Down Expand Up @@ -394,25 +402,37 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p
return &userProfile, nil
}

func (p *LDAPUserProvider) resolveUsersFilter(username string) (filter string) {
func (p *LDAPUserProvider) resolveUsersFilter(input string) (filter string) {
filter = p.config.UsersFilter

if p.usersFilterReplacementInput {
// The {input} placeholder is replaced by the username input.
filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(username))
filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(input))
}

if p.usersFilterReplacementDateTimeGeneralized {
filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeGeneralized, p.clock.Now().UTC().Format(ldapGeneralizedTimeDateTimeFormat))
}

if p.usersFilterReplacementDateTimeUnixEpoch {
filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeUnixEpoch, strconv.Itoa(int(p.clock.Now().Unix())))
}

if p.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch {
filter = strings.ReplaceAll(filter, ldapPlaceholderDateTimeMicrosoftNTTimeEpoch, strconv.Itoa(int(utils.UnixNanoTimeToMicrosoftNTEpoch(p.clock.Now().UnixNano()))))
}

p.log.Tracef("Detected user filter is %s", filter)

return filter
}

func (p *LDAPUserProvider) resolveGroupsFilter(username string, profile *ldapUserProfile) (filter string) {
func (p *LDAPUserProvider) resolveGroupsFilter(input string, profile *ldapUserProfile) (filter string) {
filter = p.config.GroupsFilter

if p.groupsFilterReplacementInput {
// The {input} placeholder is replaced by the users username input.
filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(username))
filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(input))
}

if profile != nil {
Expand Down
12 changes: 12 additions & 0 deletions internal/authentication/ldap_user_provider_startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
p.usersFilterReplacementInput = true
}

if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeGeneralized) {
p.usersFilterReplacementDateTimeGeneralized = true
}

if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeUnixEpoch) {
p.usersFilterReplacementDateTimeUnixEpoch = true
}

if strings.Contains(p.config.UsersFilter, ldapPlaceholderDateTimeMicrosoftNTTimeEpoch) {
p.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch = true
}

p.log.Tracef("Detected user filter replacements that need to be resolved per lookup are: %s=%v",
ldapPlaceholderInput, p.usersFilterReplacementInput)
}
Expand Down

0 comments on commit d67554a

Please sign in to comment.