feat(authentication): ldap time replacements (#4483)
This adds and utilizes several time replacements for both specialized LDAP implementations. Closes #1964, Closes #1284pull/4498/head^2
parent
d0d80b4f66
commit
d67554ab88
|
@ -61,11 +61,14 @@ 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 |
|
||||
| {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
|
||||
|
||||
|
@ -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.
|
||||
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))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) |
|
||||
| freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))) | (&(member={dn})(objectClass=groupOfNames)) |
|
||||
| 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
|
||||
|
||||
|
|
|
@ -73,6 +73,13 @@ const (
|
|||
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 (
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
@ -23,6 +24,8 @@ type LDAPUserProvider struct {
|
|||
log *logrus.Logger
|
||||
factory LDAPClientFactory
|
||||
|
||||
clock utils.Clock
|
||||
|
||||
disableResetPassword bool
|
||||
|
||||
// Automatically detected LDAP features.
|
||||
|
@ -32,6 +35,9 @@ type LDAPUserProvider struct {
|
|||
usersBaseDN string
|
||||
usersAttributes []string
|
||||
usersFilterReplacementInput bool
|
||||
usersFilterReplacementDateTimeGeneralized bool
|
||||
usersFilterReplacementDateTimeUnixEpoch bool
|
||||
usersFilterReplacementDateTimeMicrosoftNTTimeEpoch bool
|
||||
|
||||
// Dynamically generated groups values.
|
||||
groupsBaseDN string
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -74,6 +81,7 @@ func newLDAPUserProvider(config schema.LDAPAuthenticationBackend, disableResetPa
|
|||
log: logging.Logger(),
|
||||
factory: factory,
|
||||
disableResetPassword: disableResetPassword,
|
||||
clock: &utils.RealClock{},
|
||||
}
|
||||
|
||||
provider.parseDynamicUsersConfiguration()
|
||||
|
@ -394,12 +402,24 @@ 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)
|
||||
|
@ -407,12 +427,12 @@ func (p *LDAPUserProvider) resolveUsersFilter(username string) (filter string) {
|
|||
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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -187,7 +187,7 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuth
|
|||
|
||||
// DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the LDAPImplementationActiveDirectory Implementation.
|
||||
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",
|
||||
MailAttribute: ldapAttrMail,
|
||||
DisplayNameAttribute: ldapAttrDisplayName,
|
||||
|
@ -201,7 +201,7 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory =
|
|||
|
||||
// DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA represents the default LDAP config for the LDAPImplementationFreeIPA Implementation.
|
||||
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,
|
||||
MailAttribute: ldapAttrMail,
|
||||
DisplayNameAttribute: ldapAttrDisplayName,
|
||||
|
|
|
@ -702,7 +702,7 @@ func TestShouldDestroySessionWhenInactiveForTooLong(t *testing.T) {
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
past := clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
|
@ -736,7 +736,7 @@ func TestShouldDestroySessionWhenInactiveForTooLongUsingDurationNotation(t *test
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = time.Second * 10
|
||||
|
@ -833,7 +833,7 @@ func TestShouldRedirectWhenSessionInactiveForTooLongAndRDParamProvided(t *testin
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
mock.Ctx.Configuration.Session.Inactivity = testInactivity
|
||||
|
@ -1055,7 +1055,7 @@ func TestShouldNotRefreshUserGroupsFromBackend(t *testing.T) {
|
|||
|
||||
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
|
@ -1115,7 +1115,7 @@ func TestShouldNotRefreshUserGroupsFromBackendWhenDisabled(t *testing.T) {
|
|||
|
||||
mock.UserProviderMock.EXPECT().GetDetails("john").Times(0)
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
|
@ -1161,7 +1161,7 @@ func TestShouldDestroySessionWhenUserNotExist(t *testing.T) {
|
|||
|
||||
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(1)
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
|
@ -1222,7 +1222,7 @@ func TestShouldGetRemovedUserGroupsFromBackend(t *testing.T) {
|
|||
|
||||
mock.UserProviderMock.EXPECT().GetDetails("john").Return(user, nil).Times(2)
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
|
||||
userSession := mock.Ctx.GetSession()
|
||||
|
@ -1431,7 +1431,7 @@ func TestShouldNotRedirectRequestsForBypassACLWhenInactiveForTooLong(t *testing.
|
|||
mock := mocks.NewMockAutheliaCtx(t)
|
||||
defer mock.Close()
|
||||
|
||||
clock := mocks.TestingClock{}
|
||||
clock := utils.TestingClock{}
|
||||
clock.Set(time.Now())
|
||||
past := clock.Now().Add(-1 * time.Hour)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/templates"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// MockAutheliaCtx a mock of AutheliaCtx.
|
||||
|
@ -36,33 +37,13 @@ type MockAutheliaCtx struct {
|
|||
|
||||
UserSession *session.UserSession
|
||||
|
||||
Clock 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
|
||||
Clock utils.TestingClock
|
||||
}
|
||||
|
||||
// NewMockAutheliaCtx create an instance of AutheliaCtx mock.
|
||||
func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
|
||||
mockAuthelia := new(MockAutheliaCtx)
|
||||
mockAuthelia.Clock = TestingClock{}
|
||||
mockAuthelia.Clock = utils.TestingClock{}
|
||||
|
||||
datetime, _ := time.Parse("2006-Jan-02", "2013-Feb-03")
|
||||
mockAuthelia.Clock.Set(datetime)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/mocks"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/regulation"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
type RegulatorSuite struct {
|
||||
|
@ -22,7 +23,7 @@ type RegulatorSuite struct {
|
|||
ctrl *gomock.Controller
|
||||
storageMock *mocks.MockStorage
|
||||
config schema.RegulationConfiguration
|
||||
clock mocks.TestingClock
|
||||
clock utils.TestingClock
|
||||
}
|
||||
|
||||
func (s *RegulatorSuite) SetupTest() {
|
||||
|
|
|
@ -20,3 +20,23 @@ func (RealClock) Now() time.Time {
|
|||
func (RealClock) After(d time.Duration) <-chan time.Time {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -8,24 +8,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
windows = "windows"
|
||||
testStringInput = "abcdefghijkl"
|
||||
|
||||
// RFC3339Zero is the default value for time.Time.Unix().
|
||||
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"
|
||||
tagged = "tagged"
|
||||
unknown = "unknown"
|
||||
|
@ -84,10 +69,6 @@ const (
|
|||
Month = Year / 12
|
||||
)
|
||||
|
||||
const (
|
||||
errFmtLinuxNotFound = "open %s: no such file or directory"
|
||||
)
|
||||
|
||||
var (
|
||||
standardDurationUnits = []string{"ns", "us", "µs", "μs", "ms", "s", "m", "h"}
|
||||
reDurationSeconds = regexp.MustCompile(`^\d+$`)
|
||||
|
@ -110,6 +91,12 @@ const (
|
|||
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 (
|
||||
// CharSetAlphabeticLower are literally just valid alphabetic lowercase printable ASCII chars.
|
||||
CharSetAlphabeticLower = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
@ -155,5 +142,7 @@ var htmlEscaper = strings.NewReplacer(
|
|||
// ErrTimeoutReached error thrown when a timeout is reached.
|
||||
var ErrTimeoutReached = errors.New("timeout reached")
|
||||
|
||||
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
|
||||
var ErrTLSVersionNotSupported = errors.New("supplied tls version isn't supported")
|
||||
const (
|
||||
windows = "windows"
|
||||
errFmtLinuxNotFound = "open %s: no such file or directory"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package utils
|
||||
|
||||
const (
|
||||
testStringInput = "abcdefghijkl"
|
||||
)
|
|
@ -67,3 +67,8 @@ func ParseDurationString(input string) (duration time.Duration, err error) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -122,3 +122,11 @@ func TestShouldTimeIntervalsMakeSense(t *testing.T) {
|
|||
assert.Equal(t, Year, Day*365)
|
||||
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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue