Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
ai3
accountserver
Commits
907f30f6
Commit
907f30f6
authored
Nov 14, 2018
by
ale
Browse files
Add last password change timestamp to API
Mapped to the shadowLastChange attribute in LDAP.
parent
8facee8c
Changes
4
Hide whitespace changes
Inline
Side-by-side
backend/model.go
View file @
907f30f6
...
@@ -6,6 +6,7 @@ import (
...
@@ -6,6 +6,7 @@ import (
"math/rand"
"math/rand"
"strconv"
"strconv"
"strings"
"strings"
"time"
ldaputil
"git.autistici.org/ai3/go-common/ldap"
ldaputil
"git.autistici.org/ai3/go-common/ldap"
"github.com/tstranex/u2f"
"github.com/tstranex/u2f"
...
@@ -16,16 +17,17 @@ import (
...
@@ -16,16 +17,17 @@ import (
const
(
const
(
// Names of some well-known LDAP attributes.
// Names of some well-known LDAP attributes.
totpSecretLDAPAttr
=
"totpSecret"
totpSecretLDAPAttr
=
"totpSecret"
preferredLanguageLDAPAttr
=
"preferredLanguage"
preferredLanguageLDAPAttr
=
"preferredLanguage"
recoveryHintLDAPAttr
=
"recoverQuestion"
recoveryHintLDAPAttr
=
"recoverQuestion"
recoveryResponseLDAPAttr
=
"recoverAnswer"
recoveryResponseLDAPAttr
=
"recoverAnswer"
aspLDAPAttr
=
"appSpecificPassword"
aspLDAPAttr
=
"appSpecificPassword"
storagePublicKeyLDAPAttr
=
"storagePublicKey"
storagePublicKeyLDAPAttr
=
"storagePublicKey"
storagePrivateKeyLDAPAttr
=
"storageEncryptedSecretKey"
storagePrivateKeyLDAPAttr
=
"storageEncryptedSecretKey"
passwordLDAPAttr
=
"userPassword"
passwordLDAPAttr
=
"userPassword"
u2fRegistrationsLDAPAttr
=
"u2fRegistration"
passwordLastChangeLDAPAttr
=
"shadowLastChange"
uidNumberLDAPAttr
=
"uidNumber"
u2fRegistrationsLDAPAttr
=
"u2fRegistration"
uidNumberLDAPAttr
=
"uidNumber"
)
)
// backend is the interface to an LDAP-backed user database.
// backend is the interface to an LDAP-backed user database.
...
@@ -125,12 +127,13 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
...
@@ -125,12 +127,13 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
uidNumber
,
_
:=
strconv
.
Atoi
(
entry
.
GetAttributeValue
(
uidNumberLDAPAttr
))
// nolint
uidNumber
,
_
:=
strconv
.
Atoi
(
entry
.
GetAttributeValue
(
uidNumberLDAPAttr
))
// nolint
user
:=
&
as
.
RawUser
{
user
:=
&
as
.
RawUser
{
User
:
as
.
User
{
User
:
as
.
User
{
Name
:
entry
.
GetAttributeValue
(
"uid"
),
Name
:
entry
.
GetAttributeValue
(
"uid"
),
Lang
:
entry
.
GetAttributeValue
(
preferredLanguageLDAPAttr
),
Lang
:
entry
.
GetAttributeValue
(
preferredLanguageLDAPAttr
),
UID
:
uidNumber
,
UID
:
uidNumber
,
AccountRecoveryHint
:
entry
.
GetAttributeValue
(
recoveryHintLDAPAttr
),
LastPasswordChangeStamp
:
decodeShadowTimestamp
(
entry
.
GetAttributeValue
(
passwordLastChangeLDAPAttr
)),
U2FRegistrations
:
decodeU2FRegistrations
(
entry
.
GetAttributeValues
(
u2fRegistrationsLDAPAttr
)),
AccountRecoveryHint
:
entry
.
GetAttributeValue
(
recoveryHintLDAPAttr
),
HasOTP
:
entry
.
GetAttributeValue
(
totpSecretLDAPAttr
)
!=
""
,
U2FRegistrations
:
decodeU2FRegistrations
(
entry
.
GetAttributeValues
(
u2fRegistrationsLDAPAttr
)),
HasOTP
:
entry
.
GetAttributeValue
(
totpSecretLDAPAttr
)
!=
""
,
},
},
// Remove the legacy LDAP {crypt} prefix on old passwords.
// Remove the legacy LDAP {crypt} prefix on old passwords.
Password
:
strings
.
TrimPrefix
(
entry
.
GetAttributeValue
(
passwordLDAPAttr
),
"{crypt}"
),
Password
:
strings
.
TrimPrefix
(
entry
.
GetAttributeValue
(
passwordLDAPAttr
),
"{crypt}"
),
...
@@ -158,7 +161,7 @@ func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
...
@@ -158,7 +161,7 @@ func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
{
Type
:
"gecos"
,
Vals
:
s2l
(
user
.
Name
)},
{
Type
:
"gecos"
,
Vals
:
s2l
(
user
.
Name
)},
{
Type
:
"loginShell"
,
Vals
:
[]
string
{
"/bin/false"
}},
{
Type
:
"loginShell"
,
Vals
:
[]
string
{
"/bin/false"
}},
{
Type
:
"homeDirectory"
,
Vals
:
[]
string
{
"/var/empty"
}},
{
Type
:
"homeDirectory"
,
Vals
:
[]
string
{
"/var/empty"
}},
{
Type
:
"shadow
LastChange
"
,
Vals
:
[]
string
{
"12345"
}},
{
Type
:
password
LastChange
LDAPAttr
,
Vals
:
[]
string
{
"12345"
}},
{
Type
:
"shadowWarning"
,
Vals
:
[]
string
{
"7"
}},
{
Type
:
"shadowWarning"
,
Vals
:
[]
string
{
"7"
}},
{
Type
:
"shadowMax"
,
Vals
:
[]
string
{
"99999"
}},
{
Type
:
"shadowMax"
,
Vals
:
[]
string
{
"99999"
}},
{
Type
:
preferredLanguageLDAPAttr
,
Vals
:
s2l
(
user
.
Lang
)},
{
Type
:
preferredLanguageLDAPAttr
,
Vals
:
s2l
(
user
.
Lang
)},
...
@@ -294,6 +297,7 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
...
@@ -294,6 +297,7 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
func
(
tx
*
backendTX
)
SetUserPassword
(
ctx
context
.
Context
,
user
*
as
.
User
,
encryptedPassword
string
)
(
err
error
)
{
func
(
tx
*
backendTX
)
SetUserPassword
(
ctx
context
.
Context
,
user
*
as
.
User
,
encryptedPassword
string
)
(
err
error
)
{
dn
:=
tx
.
getUserDN
(
user
)
dn
:=
tx
.
getUserDN
(
user
)
tx
.
setAttr
(
dn
,
passwordLDAPAttr
,
encryptedPassword
)
tx
.
setAttr
(
dn
,
passwordLDAPAttr
,
encryptedPassword
)
tx
.
setAttr
(
dn
,
passwordLastChangeLDAPAttr
,
encodeShadowTimestamp
(
time
.
Now
()))
for
_
,
r
:=
range
user
.
GetResourcesByType
(
as
.
ResourceTypeEmail
)
{
for
_
,
r
:=
range
user
.
GetResourcesByType
(
as
.
ResourceTypeEmail
)
{
dn
,
err
=
tx
.
backend
.
resources
.
GetDN
(
r
.
ID
)
dn
,
err
=
tx
.
backend
.
resources
.
GetDN
(
r
.
ID
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -552,3 +556,17 @@ func (tx *backendTX) isUIDAvailable(ctx context.Context, uid int) (bool, error)
...
@@ -552,3 +556,17 @@ func (tx *backendTX) isUIDAvailable(ctx context.Context, uid int) (bool, error)
}
}
return
true
,
nil
return
true
,
nil
}
}
const
oneDay
=
86400
func
encodeShadowTimestamp
(
t
time
.
Time
)
string
{
d
:=
t
.
UTC
()
.
Unix
()
/
oneDay
return
strconv
.
FormatInt
(
d
,
10
)
}
func
decodeShadowTimestamp
(
s
string
)
(
t
time
.
Time
)
{
if
i
,
err
:=
strconv
.
ParseInt
(
s
,
10
,
64
);
err
==
nil
{
t
=
time
.
Unix
(
i
*
oneDay
,
0
)
.
UTC
()
}
return
}
backend/model_test.go
View file @
907f30f6
...
@@ -3,6 +3,7 @@ package backend
...
@@ -3,6 +3,7 @@ package backend
import
(
import
(
"context"
"context"
"testing"
"testing"
"time"
"github.com/go-test/deep"
"github.com/go-test/deep"
...
@@ -79,10 +80,14 @@ func TestModel_GetUser(t *testing.T) {
...
@@ -79,10 +80,14 @@ func TestModel_GetUser(t *testing.T) {
defer
stop
()
defer
stop
()
if
user
.
Name
!=
testUser1
{
if
user
.
Name
!=
testUser1
{
t
.
Fatal
f
(
"bad username: expected %s, got %s"
,
testUser1
,
user
.
Name
)
t
.
Error
f
(
"bad username: expected %s, got %s"
,
testUser1
,
user
.
Name
)
}
}
if
len
(
user
.
Resources
)
!=
5
{
if
len
(
user
.
Resources
)
!=
5
{
t
.
Fatalf
(
"expected 5 resources, got %d"
,
len
(
user
.
Resources
))
t
.
Errorf
(
"expected 5 resources, got %d"
,
len
(
user
.
Resources
))
}
expectedPwChangeStamp
:=
time
.
Date
(
2018
,
11
,
14
,
0
,
0
,
0
,
0
,
time
.
UTC
)
if
user
.
LastPasswordChangeStamp
!=
expectedPwChangeStamp
{
t
.
Errorf
(
"bad last password change timestamp: expected %s, got %s"
,
expectedPwChangeStamp
,
user
.
LastPasswordChangeStamp
)
}
}
// Test a specific resource (the database).
// Test a specific resource (the database).
...
...
backend/testdata/test1.ldif
View file @
907f30f6
...
@@ -16,7 +16,7 @@ sn: Private
...
@@ -16,7 +16,7 @@ sn: Private
homeDirectory: /var/empty
homeDirectory: /var/empty
uid: uno@investici.org
uid: uno@investici.org
givenName: Private
givenName: Private
shadowLastChange: 1
2345
shadowLastChange: 1
7849
shadowWarning: 7
shadowWarning: 7
preferredLanguage: it
preferredLanguage: it
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
...
@@ -63,7 +63,7 @@ sn: Private
...
@@ -63,7 +63,7 @@ sn: Private
homeDirectory: /home/users/investici.org/uno
homeDirectory: /home/users/investici.org/uno
uid: uno
uid: uno
creationDate: 01-08-2013
creationDate: 01-08-2013
shadowLastChange: 1
2345
shadowLastChange: 1
7849
originalHost: host2
originalHost: host2
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
...
...
types.go
View file @
907f30f6
...
@@ -27,6 +27,10 @@ type User struct {
...
@@ -27,6 +27,10 @@ type User struct {
// UNIX user id.
// UNIX user id.
UID
int
`json:"uid"`
UID
int
`json:"uid"`
// Timestamp of last password change. This is serialized as a
// RFC3339 string in JSON.
LastPasswordChangeStamp
time
.
Time
`json:"last_password_change_stamp"`
// Has2FA is true if the user has a second-factor authentication
// Has2FA is true if the user has a second-factor authentication
// mechanism properly set up. In practice, this is the case if either
// mechanism properly set up. In practice, this is the case if either
// HasOTP is true, or len(U2FRegistrations) > 0.
// HasOTP is true, or len(U2FRegistrations) > 0.
...
@@ -36,15 +40,19 @@ type User struct {
...
@@ -36,15 +40,19 @@ type User struct {
HasOTP
bool
`json:"has_otp"`
HasOTP
bool
`json:"has_otp"`
// HasEncryptionKeys is true if encryption keys are properly set up for
// HasEncryptionKeys is true if encryption keys are properly set up for
// this user.
TODO: consider disabling it.
// this user.
HasEncryptionKeys
bool
`json:"has_encryption_keys"`
HasEncryptionKeys
bool
`json:"has_encryption_keys"`
// The recovery hint for this account (empty if unset).
AccountRecoveryHint
string
`json:"account_recovery_hint"`
AccountRecoveryHint
string
`json:"account_recovery_hint"`
// List of application-specific passwords (metadata only).
AppSpecificPasswords
[]
*
AppSpecificPasswordInfo
`json:"app_specific_passwords,omitempty"`
AppSpecificPasswords
[]
*
AppSpecificPasswordInfo
`json:"app_specific_passwords,omitempty"`
// List of U2F registrations.
U2FRegistrations
[]
*
U2FRegistration
`json:"u2f_registrations,omitempty"`
U2FRegistrations
[]
*
U2FRegistration
`json:"u2f_registrations,omitempty"`
// All the resources owned by this user.
Resources
[]
*
Resource
`json:"resources,omitempty"`
Resources
[]
*
Resource
`json:"resources,omitempty"`
}
}
...
...
ale
@ale
mentioned in issue
#4 (closed)
·
Nov 14, 2018
mentioned in issue
#4 (closed)
mentioned in issue #4
Toggle commit list
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment