Commit 907f30f6 authored by ale's avatar ale

Add last password change timestamp to API

Mapped to the shadowLastChange attribute in LDAP.
parent 8facee8c
Pipeline #1544 passed with stages
in 1 minute and 24 seconds
......@@ -6,6 +6,7 @@ import (
"math/rand"
"strconv"
"strings"
"time"
ldaputil "git.autistici.org/ai3/go-common/ldap"
"github.com/tstranex/u2f"
......@@ -16,16 +17,17 @@ import (
const (
// Names of some well-known LDAP attributes.
totpSecretLDAPAttr = "totpSecret"
preferredLanguageLDAPAttr = "preferredLanguage"
recoveryHintLDAPAttr = "recoverQuestion"
recoveryResponseLDAPAttr = "recoverAnswer"
aspLDAPAttr = "appSpecificPassword"
storagePublicKeyLDAPAttr = "storagePublicKey"
storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey"
passwordLDAPAttr = "userPassword"
u2fRegistrationsLDAPAttr = "u2fRegistration"
uidNumberLDAPAttr = "uidNumber"
totpSecretLDAPAttr = "totpSecret"
preferredLanguageLDAPAttr = "preferredLanguage"
recoveryHintLDAPAttr = "recoverQuestion"
recoveryResponseLDAPAttr = "recoverAnswer"
aspLDAPAttr = "appSpecificPassword"
storagePublicKeyLDAPAttr = "storagePublicKey"
storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey"
passwordLDAPAttr = "userPassword"
passwordLastChangeLDAPAttr = "shadowLastChange"
u2fRegistrationsLDAPAttr = "u2fRegistration"
uidNumberLDAPAttr = "uidNumber"
)
// backend is the interface to an LDAP-backed user database.
......@@ -125,12 +127,13 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
uidNumber, _ := strconv.Atoi(entry.GetAttributeValue(uidNumberLDAPAttr)) // nolint
user := &as.RawUser{
User: as.User{
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber,
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
},
// Remove the legacy LDAP {crypt} prefix on old passwords.
Password: strings.TrimPrefix(entry.GetAttributeValue(passwordLDAPAttr), "{crypt}"),
......@@ -158,7 +161,7 @@ func userToLDAP(user *as.User) (attrs []ldap.PartialAttribute) {
{Type: "gecos", Vals: s2l(user.Name)},
{Type: "loginShell", Vals: []string{"/bin/false"}},
{Type: "homeDirectory", Vals: []string{"/var/empty"}},
{Type: "shadowLastChange", Vals: []string{"12345"}},
{Type: passwordLastChangeLDAPAttr, Vals: []string{"12345"}},
{Type: "shadowWarning", Vals: []string{"7"}},
{Type: "shadowMax", Vals: []string{"99999"}},
{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
......@@ -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) {
dn := tx.getUserDN(user)
tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
tx.setAttr(dn, passwordLastChangeLDAPAttr, encodeShadowTimestamp(time.Now()))
for _, r := range user.GetResourcesByType(as.ResourceTypeEmail) {
dn, err = tx.backend.resources.GetDN(r.ID)
if err != nil {
......@@ -552,3 +556,17 @@ func (tx *backendTX) isUIDAvailable(ctx context.Context, uid int) (bool, error)
}
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
}
......@@ -3,6 +3,7 @@ package backend
import (
"context"
"testing"
"time"
"github.com/go-test/deep"
......@@ -79,10 +80,14 @@ func TestModel_GetUser(t *testing.T) {
defer stop()
if user.Name != testUser1 {
t.Fatalf("bad username: expected %s, got %s", testUser1, user.Name)
t.Errorf("bad username: expected %s, got %s", testUser1, user.Name)
}
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).
......
......@@ -16,7 +16,7 @@ sn: Private
homeDirectory: /var/empty
uid: uno@investici.org
givenName: Private
shadowLastChange: 12345
shadowLastChange: 17849
shadowWarning: 7
preferredLanguage: it
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
......@@ -63,7 +63,7 @@ sn: Private
homeDirectory: /home/users/investici.org/uno
uid: uno
creationDate: 01-08-2013
shadowLastChange: 12345
shadowLastChange: 17849
originalHost: host2
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
......
......@@ -27,6 +27,10 @@ type User struct {
// UNIX user id.
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
// mechanism properly set up. In practice, this is the case if either
// HasOTP is true, or len(U2FRegistrations) > 0.
......@@ -36,15 +40,19 @@ type User struct {
HasOTP bool `json:"has_otp"`
// 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"`
// The recovery hint for this account (empty if unset).
AccountRecoveryHint string `json:"account_recovery_hint"`
// List of application-specific passwords (metadata only).
AppSpecificPasswords []*AppSpecificPasswordInfo `json:"app_specific_passwords,omitempty"`
// List of U2F registrations.
U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"`
// All the resources owned by this user.
Resources []*Resource `json:"resources,omitempty"`
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment