Commit 8b735ede authored by ale's avatar ale
Browse files

Improve the UpdateUser API with support for partial updates

The API change is semantically backwards-incompatible, i.e. it will
default to no change unless you set the new set_foo attributes.
parent 5e354f1e
......@@ -2,6 +2,7 @@ package accountserver
import (
"errors"
"strings"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
umdb "git.autistici.org/id/usermetadb"
......@@ -157,7 +158,6 @@ func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error {
return nil
}
// TODO: call out to auth-server for rate limiting and other features.
// Authenticate the secret recovery password.
if err := rctx.authorizeAccountRecovery(rctx.Context, rctx.User.Name, r.RecoveryPassword, r.RemoteAddr); err != nil {
return err
......@@ -403,23 +403,28 @@ func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) {
// UpdateUserRequest allows the caller to update a (very limited) selected set
// of fields on a User object. It is a catch-all function for very simple
// changes that don't justify their own specialized method.
// changes that don't justify their own specialized method. Fields are
// associated with a "set_field" attribute to allow for selective updates.
type UpdateUserRequest struct {
UserRequestBase
Lang string `json:"lang,omitempty"`
U2FRegistrations []*ct.U2FRegistration `json:"u2f_registrations,omitempty"`
Lang string `json:"lang,omitempty"`
SetLang bool `json:"set_lang"`
U2FRegistrations []*ct.U2FRegistration `json:"u2f_registrations,omitempty"`
SetU2FRegistrations bool `json:"set_u2f_registrations"`
}
const maxU2FRegistrations = 20
// Validate the request.
func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
if len(r.U2FRegistrations) > maxU2FRegistrations {
if r.SetU2FRegistrations && len(r.U2FRegistrations) > maxU2FRegistrations {
return newValidationError(nil, "u2f_registrations", "too many U2F registrations")
}
// TODO: better validation of the language code!
if len(r.Lang) > 2 {
if r.SetLang && (r.Lang == "" || len(r.Lang) > 2) {
return newValidationError(nil, "lang", "invalid language code")
}
......@@ -428,16 +433,22 @@ func (r *UpdateUserRequest) Validate(rctx *RequestContext) error {
// Serve the request.
func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
if r.Lang != "" {
rctx.User.Lang = r.Lang
if r.SetLang {
rctx.User.Lang = strings.ToLower(r.Lang)
}
// TODO: check if setU2FRegistration calls tx.UpdateUser, this is a bug otherwise.
return nil, rctx.User.setU2FRegistrations(rctx.Context, rctx.TX, r.U2FRegistrations)
if r.SetU2FRegistrations {
rctx.User.U2FRegistrations = r.U2FRegistrations
if err := rctx.User.check2FAState(rctx.Context, rctx.TX); err != nil {
return nil, err
}
}
return nil, rctx.TX.UpdateUser(rctx.Context, &rctx.User.User)
}
// AdminUpdateUserRequest is the privileged version of UpdateUser and
// allows to update many more attributes. It is a catch-all function
// allows to update privileged attributes. It is a catch-all function
// for very simple changes that don't justify their own specialized
// method.
type AdminUpdateUserRequest struct {
......
......@@ -152,6 +152,7 @@ func startServiceWithConfigAndCache(t testing.TB, svcConfig as.Config, enableCac
"testdata/test1.ldif",
"testdata/test2.ldif",
"testdata/test3.ldif",
"testdata/test4.ldif",
},
})
......
dn: uid=quattro@investici.org,ou=People,dc=example,dc=com
cn: quattro@investici.org
gecos: quattro@investici.org
gidNumber: 2000
givenName: Private
homeDirectory: /var/empty
loginShell: /bin/false
objectClass: top
objectClass: person
objectClass: posixAccount
objectClass: shadowAccount
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: investiciUser
shadowLastChange: 12345
shadowMax: 99999
shadowWarning: 7
sn: quattro@investici.org
uid: quattro@investici.org
uidNumber: 23801
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
u2fRegistration:: BLbpGj8p0EaIWWbA6DiG4bQSxDPGW6J5U1ZV4C5Al2MIIrDIdMos5yqqvWZGCgl0zn0DgjvILPX5Wqy1uMlRrrbuJtcRvBQ9DEZZJmMP5CJAJqdKLG07kezOPeLQRNTjhKnW0Zixqzc8jIlqMX/+no675UeHYXr7VSmKALYekyVk
u2fRegistration:: BCCBvjcPNk4xn7Vi2YbJA8alBwIL7pkIkmtdZJwZ9Bcz4EzyE9As/9x43WwvNzaFHvqiB34hncw6IHq/SQrAq/XpdfSnqSm9tYskcbgWcNwsrXhpjTu9Pi9UyWNZtEG4nFGGFRmuNNpjA5C/P2A9V/DIat17nWE4hndFupMU2kVG
totpSecret: ABCDEF
host: host2
originalHost: host2
status: active
dn: mail=quattro@investici.org,uid=quattro@investici.org,ou=People,dc=example,dc=com
creationDate: 01-08-2013
gidNumber: 2000
host: host2
mail: quattro@investici.org
mailAlternateAddress: quattroalias@investici.org
mailMessageStore: investici.org/quattro/
objectClass: top
objectClass: virtualMailUser
originalHost: host2
status: active
uidNumber: 23801
userPassword:: JGEyJDQkMzI3NjgkMSQwZDgyMzU1YjQ0Mzg0M2NmZDY4MjU1MzE4ZTVjYTdiZSRmNTQ0ODkxOTFiNWZlYzk2MDRlNWQ2ODZjMDQxZjJkNTFmOTgxOGY4ZTFmM2E4MDYzY2U3ZTEwMTE3OTc2OGI0
package integrationtest
import (
"bytes"
"testing"
as "git.autistici.org/ai3/accountserver"
ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
)
func TestIntegration_UpdateUser_Lang(t *testing.T) {
stop, _, c := startService(t)
defer stop()
err := c.request("/api/user/update", &as.UpdateUserRequest{
UserRequestBase: as.UserRequestBase{
RequestBase: as.RequestBase{
SSO: c.ssoTicket("quattro@investici.org"),
},
Username: "quattro@investici.org",
},
SetLang: true,
Lang: "zh",
}, nil)
if err != nil {
t.Fatalf("UpdateUser(): %v", err)
}
var user as.User
err = c.request("/api/user/get", &as.GetUserRequest{
UserRequestBase: as.UserRequestBase{
RequestBase: as.RequestBase{
SSO: c.ssoTicket("quattro@investici.org"),
},
Username: "quattro@investici.org",
},
}, &user)
if err != nil {
t.Fatalf("GetUser(): %v", err)
}
if user.Lang != "zh" {
t.Fatalf("UpdateUser(lang=zh) failed, found lang=%s", user.Lang)
}
}
func countRegistrations(t *testing.T, c *testClient) (*as.User, int) {
var user as.User
err := c.request("/api/user/get", &as.GetUserRequest{
UserRequestBase: as.UserRequestBase{
RequestBase: as.RequestBase{
SSO: c.ssoTicket("quattro@investici.org"),
},
Username: "quattro@investici.org",
},
}, &user)
if err != nil {
t.Fatalf("GetUser(): %v", err)
}
return &user, len(user.U2FRegistrations)
}
func TestIntegration_UpdateUser_U2F(t *testing.T) {
stop, _, c := startService(t)
defer stop()
// Ensure we have loaded the existing registrations correctly.
if _, n := countRegistrations(t, c); n != 2 {
t.Fatalf("didn't load existing U2F registrations correctly, expected 2 found %d", n)
}
reg := ct.U2FRegistration{
PublicKey: []byte("haha"),
KeyHandle: []byte("woof"),
Comment: "heya",
}
err := c.request("/api/user/update", &as.UpdateUserRequest{
UserRequestBase: as.UserRequestBase{
RequestBase: as.RequestBase{
SSO: c.ssoTicket("quattro@investici.org"),
},
Username: "quattro@investici.org",
},
SetU2FRegistrations: true,
U2FRegistrations: []*ct.U2FRegistration{&reg},
}, nil)
if err != nil {
t.Fatalf("UpdateUser(): %v", err)
}
user, n := countRegistrations(t, c)
if n != 1 {
t.Fatalf("didn't update U2F registrations correctly, expected 1 found %d", n)
}
if newReg := user.U2FRegistrations[0]; !registrationEqual(newReg, &reg) {
t.Errorf("deserialization of registration failed, value=%+v", *newReg)
}
}
func registrationEqual(a, b *ct.U2FRegistration) bool {
return (bytes.Equal(a.PublicKey, b.PublicKey) &&
bytes.Equal(a.KeyHandle, b.KeyHandle) &&
a.Comment == b.Comment)
}
......@@ -198,15 +198,6 @@ func (u *RawUser) setTOTPSecret(ctx context.Context, tx TX, totpSecret string) e
return nil
}
// Update the list of U2F registrations for the user. The list may be empty.
func (u *RawUser) setU2FRegistrations(ctx context.Context, tx TX, regs []*ct.U2FRegistration) error {
u.U2FRegistrations = regs
if err := tx.UpdateUser(ctx, &u.User); err != nil {
return err
}
return u.check2FAState(ctx, tx)
}
// Whenever one of OTP or U2F is modified, we'd like to check if it was the
// last 2FA method available: in that case, 2FA has been disabled and we also
// want to clear all application-specific passwords.
......
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