feat(authentication): ldap time replacements (#4483)

This adds and utilizes several time replacements for both specialized LDAP implementations.

Closes #1964, Closes #1284
pull/4498/head^2
James Elliott 2022-12-21 21:31:21 +11:00 committed by GitHub
parent d0d80b4f66
commit d67554ab88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 314 additions and 243 deletions

View File

@ -61,11 +61,14 @@ search.
#### Users filter replacements #### Users filter replacements
| Placeholder | Phase | Replacement | | Placeholder | Phase | Replacement |
|:------------------------:|:-------:|:-------------------------------------:| |:-------------------------:|:-------:|:--------------------------------------------------------------------------------------------------------------:|
| {username_attribute} | startup | The configured username attribute | | {username_attribute} | startup | The configured username attribute |
| {mail_attribute} | startup | The configured mail attribute | | {mail_attribute} | startup | The configured mail attribute |
| {display_name_attribute} | startup | The configured display name attribute | | {display_name_attribute} | startup | The configured display name attribute |
| {input} | search | The input into the username field | | {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 #### Groups filter replacements
@ -92,16 +95,24 @@ Username column.
#### Filter defaults #### Filter defaults
The filters are probably the most important part to get correct when setting up LDAP. You want to exclude disabled The filters are probably the most important part to get correct when setting up LDAP. You want to exclude accounts under
accounts. The active directory example has two attribute filters that accomplish this as an example (more examples would the following conditions:
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. - 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 | | Implementation | Users Filter | Groups Filter |
|:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------:| |:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------:|
| custom | N/A | N/A | | 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))) | | 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))) | (&(member={dn})(objectClass=groupOfNames)) | | 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 ##### Microsoft Active Directory sAMAccountType

View File

@ -73,6 +73,13 @@ const (
ldapPlaceholderInput = "{input}" ldapPlaceholderInput = "{input}"
ldapPlaceholderDistinguishedName = "{dn}" ldapPlaceholderDistinguishedName = "{dn}"
ldapPlaceholderUsername = "{username}" ldapPlaceholderUsername = "{username}"
ldapPlaceholderDateTimeGeneralized = "{date-time:generalized}"
ldapPlaceholderDateTimeMicrosoftNTTimeEpoch = "{date-time:msft-nt-epoch}"
ldapPlaceholderDateTimeUnixEpoch = "{date-time:unix-epoch}"
)
const (
ldapGeneralizedTimeDateTimeFormat = "20060102150405.0Z"
) )
const ( const (

View File

@ -5,6 +5,7 @@ import (
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net" "net"
"strconv"
"strings" "strings"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
@ -23,6 +24,8 @@ type LDAPUserProvider struct {
log *logrus.Logger log *logrus.Logger
factory LDAPClientFactory factory LDAPClientFactory
clock utils.Clock
disableResetPassword bool disableResetPassword bool
// Automatically detected LDAP features. // Automatically detected LDAP features.
@ -32,6 +35,9 @@ type LDAPUserProvider struct {
usersBaseDN string usersBaseDN string
usersAttributes []string usersAttributes []string
usersFilterReplacementInput bool usersFilterReplacementInput bool
usersFilterReplacementDateTimeGeneralized bool
usersFilterReplacementDateTimeUnixEpoch bool
usersFilterReplacementDateTimeMicrosoftNTTimeEpoch bool
// Dynamically generated groups values. // Dynamically generated groups values.
groupsBaseDN string groupsBaseDN string
@ -41,14 +47,15 @@ type LDAPUserProvider struct {
groupsFilterReplacementDN bool 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) { 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 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 { if config.TLS == nil {
config.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS config.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS
} }
@ -74,6 +81,7 @@ func newLDAPUserProvider(config schema.LDAPAuthenticationBackend, disableResetPa
log: logging.Logger(), log: logging.Logger(),
factory: factory, factory: factory,
disableResetPassword: disableResetPassword, disableResetPassword: disableResetPassword,
clock: &utils.RealClock{},
} }
provider.parseDynamicUsersConfiguration() provider.parseDynamicUsersConfiguration()
@ -394,12 +402,24 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p
return &userProfile, nil return &userProfile, nil
} }
func (p *LDAPUserProvider) resolveUsersFilter(username string) (filter string) { func (p *LDAPUserProvider) resolveUsersFilter(input string) (filter string) {
filter = p.config.UsersFilter filter = p.config.UsersFilter
if p.usersFilterReplacementInput { if p.usersFilterReplacementInput {
// The {input} placeholder is replaced by the username input. // 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) p.log.Tracef("Detected user filter is %s", filter)
@ -407,12 +427,12 @@ func (p *LDAPUserProvider) resolveUsersFilter(username string) (filter string) {
return 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 filter = p.config.GroupsFilter
if p.groupsFilterReplacementInput { if p.groupsFilterReplacementInput {
// The {input} placeholder is replaced by the users username input. // 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 { if profile != nil {

View File

@ -120,6 +120,18 @@ func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
p.usersFilterReplacementInput = true 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", p.log.Tracef("Detected user filter replacements that need to be resolved per lookup are: %s=%v",
ldapPlaceholderInput, p.usersFilterReplacementInput) ldapPlaceholderInput, p.usersFilterReplacementInput)
} }

File diff suppressed because it is too large Load Diff

View File

@ -187,7 +187,7 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuth
// DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the LDAPImplementationActiveDirectory Implementation. // DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the LDAPImplementationActiveDirectory Implementation.
var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = LDAPAuthenticationBackend{ var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = LDAPAuthenticationBackend{
UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0)))", UsersFilter: "(&(|({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})))",
UsernameAttribute: "sAMAccountName", UsernameAttribute: "sAMAccountName",
MailAttribute: ldapAttrMail, MailAttribute: ldapAttrMail,
DisplayNameAttribute: ldapAttrDisplayName, DisplayNameAttribute: ldapAttrDisplayName,
@ -201,7 +201,7 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory =
// DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA represents the default LDAP config for the LDAPImplementationFreeIPA Implementation. // DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA represents the default LDAP config for the LDAPImplementationFreeIPA Implementation.
var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAuthenticationBackend{ var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAuthenticationBackend{
UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE)))", UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized})))",
UsernameAttribute: ldapAttrUserID, UsernameAttribute: ldapAttrUserID,
MailAttribute: ldapAttrMail, MailAttribute: ldapAttrMail,
DisplayNameAttribute: ldapAttrDisplayName, DisplayNameAttribute: ldapAttrDisplayName,

View File

@ -702,7 +702,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
past := clock.Now().Add(-1 * time.Hour) past := clock.Now().Add(-1 * time.Hour)
@ -736,7 +736,7 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = time.Second * 10 mock.Ctx.Configuration.Session.Inactivity = time.Second * 10
@ -833,7 +833,7 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
mock.Ctx.Configuration.Session.Inactivity = testInactivity mock.Ctx.Configuration.Session.Inactivity = testInactivity
@ -1055,7 +1055,7 @@ func TestShouldNotRefreshUserGroupsFromBackend(t *testing.T) {
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0) mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -1115,7 +1115,7 @@ func TestShouldNotRefreshUserGroupsFromBackendWhenDisabled(t *testing.T) {
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0) mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -1161,7 +1161,7 @@ func TestShouldDestroySessionWhenUserNotExist(t *testing.T) {
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1) mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1)
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -1222,7 +1222,7 @@ func TestShouldGetRemovedUserGroupsFromBackend(t *testing.T) {
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2) mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2)
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
userSession := mock.Ctx.GetSession() userSession := mock.Ctx.GetSession()
@ -1431,7 +1431,7 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
clock := mocks.TestingClock{} clock := utils.TestingClock{}
clock.Set(time.Now()) clock.Set(time.Now())
past := clock.Now().Add(-1 * time.Hour) past := clock.Now().Add(-1 * time.Hour)

View File

@ -19,6 +19,7 @@ import (
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/templates" "github.com/authelia/authelia/v4/internal/templates"
"github.com/authelia/authelia/v4/internal/utils"
) )
// MockAutheliaCtx a mock of AutheliaCtx. // MockAutheliaCtx a mock of AutheliaCtx.
@ -36,33 +37,13 @@ type MockAutheliaCtx struct {
UserSession *session.UserSession UserSession *session.UserSession
Clock TestingClock Clock utils.TestingClock
}
// TestingClock implementation of clock for tests.
type TestingClock struct {
now time.Time
}
// Now return the stored clock.
func (dc *TestingClock) Now() time.Time {
return dc.now
}
// After return a channel receiving the time after duration has elapsed.
func (dc *TestingClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
// Set set the time of the clock.
func (dc *TestingClock) Set(now time.Time) {
dc.now = now
} }
// NewMockAutheliaCtx create an instance of AutheliaCtx mock. // NewMockAutheliaCtx create an instance of AutheliaCtx mock.
func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx { func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia := new(MockAutheliaCtx) mockAuthelia := new(MockAutheliaCtx)
mockAuthelia.Clock = TestingClock{} mockAuthelia.Clock = utils.TestingClock{}
datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03") datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
mockAuthelia.Clock.Set(datetime) mockAuthelia.Clock.Set(datetime)

View File

@ -13,6 +13,7 @@ import (
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/utils"
) )
type RegulatorSuite struct { type RegulatorSuite struct {
@ -22,7 +23,7 @@ type RegulatorSuite struct {
ctrl *gomock.Controller ctrl *gomock.Controller
storageMock *mocks.MockStorage storageMock *mocks.MockStorage
config schema.RegulationConfiguration config schema.RegulationConfiguration
clock mocks.TestingClock clock utils.TestingClock
} }
func (s *RegulatorSuite) SetupTest() { func (s *RegulatorSuite) SetupTest() {

View File

@ -20,3 +20,23 @@ func (RealClock) Now() time.Time {
func (RealClock) After(d time.Duration) <-chan time.Time { func (RealClock) After(d time.Duration) <-chan time.Time {
return time.After(d) return time.After(d)
} }
// TestingClock implementation of clock for tests.
type TestingClock struct {
now time.Time
}
// Now return the stored clock.
func (dc *TestingClock) Now() time.Time {
return dc.now
}
// After return a channel receiving the time after duration has elapsed.
func (dc *TestingClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
// Set set the time of the clock.
func (dc *TestingClock) Set(now time.Time) {
dc.now = now
}

View File

@ -8,24 +8,9 @@ import (
) )
const ( const (
windows = "windows"
testStringInput = "abcdefghijkl"
// RFC3339Zero is the default value for time.Time.Unix(). // RFC3339Zero is the default value for time.Time.Unix().
RFC3339Zero = int64(-62135596800) RFC3339Zero = int64(-62135596800)
// TLS13 is the textual representation of TLS 1.3.
TLS13 = "1.3"
// TLS12 is the textual representation of TLS 1.2.
TLS12 = "1.2"
// TLS11 is the textual representation of TLS 1.1.
TLS11 = "1.1"
// TLS10 is the textual representation of TLS 1.0.
TLS10 = "1.0"
clean = "clean" clean = "clean"
tagged = "tagged" tagged = "tagged"
unknown = "unknown" unknown = "unknown"
@ -84,10 +69,6 @@ const (
Month = Year / 12 Month = Year / 12
) )
const (
errFmtLinuxNotFound = "open %s: no such file or directory"
)
var ( var (
standardDurationUnits = []string{"ns", "us", "µs", "μs", "ms", "s", "m", "h"} standardDurationUnits = []string{"ns", "us", "µs", "μs", "ms", "s", "m", "h"}
reDurationSeconds = regexp.MustCompile(`^\d+$`) reDurationSeconds = regexp.MustCompile(`^\d+$`)
@ -110,6 +91,12 @@ const (
HoursInYear = HoursInDay * 365 HoursInYear = HoursInDay * 365
) )
const (
// timeUnixEpochAsMicrosoftNTEpoch represents the unix epoch as a Microsoft NT Epoch.
// The Microsoft NT Epoch is ticks since Jan 1, 1601 (1 tick is 100ns).
timeUnixEpochAsMicrosoftNTEpoch uint64 = 116444736000000000
)
const ( const (
// CharSetAlphabeticLower are literally just valid alphabetic lowercase printable ASCII chars. // CharSetAlphabeticLower are literally just valid alphabetic lowercase printable ASCII chars.
CharSetAlphabeticLower = "abcdefghijklmnopqrstuvwxyz" CharSetAlphabeticLower = "abcdefghijklmnopqrstuvwxyz"
@ -155,5 +142,7 @@ var htmlEscaper = strings.NewReplacer(
// ErrTimeoutReached error thrown when a timeout is reached. // ErrTimeoutReached error thrown when a timeout is reached.
var ErrTimeoutReached = errors.New("timeout reached") var ErrTimeoutReached = errors.New("timeout reached")
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied. const (
var ErrTLSVersionNotSupported = errors.New("supplied tls version isn't supported") windows = "windows"
errFmtLinuxNotFound = "open %s: no such file or directory"
)

View File

@ -0,0 +1,5 @@
package utils
const (
testStringInput = "abcdefghijkl"
)

View File

@ -67,3 +67,8 @@ func ParseDurationString(input string) (duration time.Duration, err error) {
return time.ParseDuration(out) return time.ParseDuration(out)
} }
// UnixNanoTimeToMicrosoftNTEpoch converts a unix timestamp in nanosecond format to win32 epoch format.
func UnixNanoTimeToMicrosoftNTEpoch(nano int64) (t uint64) {
return uint64(nano/100) + timeUnixEpochAsMicrosoftNTEpoch
}

View File

@ -122,3 +122,11 @@ func TestShouldTimeIntervalsMakeSense(t *testing.T) {
assert.Equal(t, Year, Day*365) assert.Equal(t, Year, Day*365)
assert.Equal(t, Month, Year/12) assert.Equal(t, Month, Year/12)
} }
func TestShouldConvertKnownUnixNanoTimeToKnownWin32Epoch(t *testing.T) {
exampleNanoTime := int64(1626234411 * 1000000000)
win32Epoch := uint64(132707080110000000)
assert.Equal(t, win32Epoch, UnixNanoTimeToMicrosoftNTEpoch(exampleNanoTime))
assert.Equal(t, timeUnixEpochAsMicrosoftNTEpoch, UnixNanoTimeToMicrosoftNTEpoch(0))
}