Commit 907f30f6 authored by ale's avatar ale
Browse files

Add last password change timestamp to API

Mapped to the shadowLastChange attribute in LDAP.
parent 8facee8c
...@@ -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"
...@@ -24,6 +25,7 @@ const ( ...@@ -24,6 +25,7 @@ const (
storagePublicKeyLDAPAttr = "storagePublicKey" storagePublicKeyLDAPAttr = "storagePublicKey"
storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey" storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey"
passwordLDAPAttr = "userPassword" passwordLDAPAttr = "userPassword"
passwordLastChangeLDAPAttr = "shadowLastChange"
u2fRegistrationsLDAPAttr = "u2fRegistration" u2fRegistrationsLDAPAttr = "u2fRegistration"
uidNumberLDAPAttr = "uidNumber" uidNumberLDAPAttr = "uidNumber"
) )
...@@ -128,6 +130,7 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) { ...@@ -128,6 +130,7 @@ func newUser(entry *ldap.Entry) (*as.RawUser, error) {
Name: entry.GetAttributeValue("uid"), Name: entry.GetAttributeValue("uid"),
Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr), Lang: entry.GetAttributeValue(preferredLanguageLDAPAttr),
UID: uidNumber, UID: uidNumber,
LastPasswordChangeStamp: decodeShadowTimestamp(entry.GetAttributeValue(passwordLastChangeLDAPAttr)),
AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr), AccountRecoveryHint: entry.GetAttributeValue(recoveryHintLDAPAttr),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)), U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "", HasOTP: entry.GetAttributeValue(totpSecretLDAPAttr) != "",
...@@ -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: "shadowLastChange", Vals: []string{"12345"}}, {Type: passwordLastChangeLDAPAttr, 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
}
...@@ -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.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 { 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).
......
...@@ -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: 12345 shadowLastChange: 17849
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: 12345 shadowLastChange: 17849
originalHost: host2 originalHost: host2
userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT userPassword:: JDYkbXBXN1NkdlE4bnY4UlpsTyRJNGZCV2RVSkV5VWxvR2l1WmdibzI1OVVUWkkyL3JWTlA4N1lT
TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA== TjNUTS53YXkyZHZSd1g2YTQ0dVVXZ2tYL1pzbkc4YXdHRFhYVGYwNU1VeE1saWdIMA==
......
...@@ -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"`
} }
......
Supports Markdown
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