Commit 09f924a6 authored by ale's avatar ale

Add U2F registrations to the user type and an API method to update them

Still doesn't entirely solve the problem of encoding the U2F
registration requests on the client side (we should probably just
accept a bytestring), but all the important functionality is there.
parent 377d120b
......@@ -314,6 +314,21 @@ Request parameters:
* `sso` - SSO ticket
* `asp_id` - ID of the app-specific password
### `/api/user/update`
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.
Non-authentication request attributes are all optional.
Request parameters:
* `username` - user to fetch
* `sso` - SSO ticket
* `lang` - set the preferred language for this user
* `u2f_registrations` - set the list of U2F registrations
### `/api/user/create`
Create a new user (admin-only). Will also create all the resources
......
......@@ -11,6 +11,7 @@ import (
"git.autistici.org/ai3/go-common/pwhash"
"github.com/pquerna/otp/totp"
"github.com/sethvargo/go-password/password"
"github.com/tstranex/u2f"
)
// RequestBase contains parameters shared by all request types.
......@@ -839,6 +840,50 @@ func (s *AccountService) CreateUser(ctx context.Context, tx TX, req *CreateUserR
return &resp, err
}
// UpdateUserRequest is the request type for AccountService.UpdateUser().
type UpdateUserRequest struct {
RequestBase
Lang string `json:"lang,omitempty"`
U2FRegistrations []*u2f.Registration `json:"u2f_registrations,omitempty"`
}
// Validate the request.
func (r *UpdateUserRequest) Validate(_ context.Context, _ *AccountService) error {
if len(r.U2FRegistrations) > 20 {
return errors.New("too many U2F registrations")
}
// TODO: better validation of the language code!
if len(r.Lang) > 2 {
return errors.New("invalid language code")
}
return nil
}
// UpdateUser 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.
func (s *AccountService) UpdateUser(ctx context.Context, tx TX, req *UpdateUserRequest) (*User, error) {
var resp *User
err := s.handleUserRequest(ctx, tx, req, s.authUser(req.RequestBase), func(ctx context.Context, user *User) error {
if req.Lang != "" {
user.Lang = req.Lang
}
// TODO: check if this allows the caller to use an
// empty list to unset the field completely.
if req.U2FRegistrations != nil {
user.U2FRegistrations = req.U2FRegistrations
}
resp = user
return tx.UpdateUser(ctx, user)
})
return resp, err
}
func randomBase64(n int) string {
b := make([]byte, n*3/4)
_, err := rand.Read(b[:])
......
......@@ -28,6 +28,11 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*User, error)
return b.users[username], nil
}
func (b *fakeBackend) UpdateUser(_ context.Context, user *User) error {
b.users[user.Name] = user
return nil
}
func (b *fakeBackend) CreateUser(_ context.Context, user *User) error {
b.users[user.Name] = user
return nil
......
......@@ -5,6 +5,7 @@ import (
"strings"
ldaputil "git.autistici.org/ai3/go-common/ldap"
"github.com/tstranex/u2f"
"gopkg.in/ldap.v2"
"git.autistici.org/ai3/accountserver"
......@@ -20,6 +21,7 @@ const (
storagePublicKeyLDAPAttr = "storagePublicKey"
storagePrivateKeyLDAPAttr = "storageEncryptedSecretKey"
passwordLDAPAttr = "userPassword"
u2fRegistrationsLDAPAttr = "u2fRegistration"
)
// backend is the interface to an LDAP-backed user database.
......@@ -103,6 +105,7 @@ func newUser(entry *ldap.Entry) (*accountserver.User, error) {
Has2FA: (entry.GetAttributeValue(totpSecretLDAPAttr) != ""),
//HasEncryptionKeys: (len(entry.GetAttributeValues("storageEncryptionKey")) > 0),
//PasswordRecoveryHint: entry.GetAttributeValue("recoverQuestion"),
U2FRegistrations: decodeU2FRegistrations(entry.GetAttributeValues(u2fRegistrationsLDAPAttr)),
}
if user.Lang == "" {
user.Lang = "en"
......@@ -125,10 +128,42 @@ func userToLDAP(user *accountserver.User) (attrs []ldap.PartialAttribute) {
{Type: "shadowWarning", Vals: []string{"7"}},
{Type: "shadowMax", Vals: []string{"99999"}},
{Type: preferredLanguageLDAPAttr, Vals: s2l(user.Lang)},
{Type: u2fRegistrationsLDAPAttr, Vals: encodeU2FRegistrations(user.U2FRegistrations)},
}...)
return
}
func decodeU2FRegistration(enc string) (*u2f.Registration, error) {
var reg u2f.Registration
if err := reg.UnmarshalBinary([]byte(enc)); err != nil {
return nil, err
}
return &reg, nil
}
func encodeU2FRegistration(r *u2f.Registration) string {
b, _ := r.MarshalBinary()
return string(b)
}
func decodeU2FRegistrations(encRegs []string) []*u2f.Registration {
var out []*u2f.Registration
for _, enc := range encRegs {
if r, err := decodeU2FRegistration(enc); err == nil {
out = append(out, r)
}
}
return out
}
func encodeU2FRegistrations(regs []*u2f.Registration) []string {
var out []string
for _, r := range regs {
out = append(out, encodeU2FRegistration(r))
}
return out
}
func (tx *backendTX) getUserDN(user *accountserver.User) string {
return joinDN("uid="+user.Name, "ou=People", tx.backend.baseDN)
}
......@@ -152,6 +187,15 @@ func (tx *backendTX) CreateUser(ctx context.Context, user *accountserver.User) e
return nil
}
// UpdateUser updates values for the user only (not the resources).
func (tx *backendTX) UpdateUser(ctx context.Context, user *accountserver.User) error {
dn := tx.getUserDN(user)
for _, attr := range userToLDAP(user) {
tx.setAttr(dn, attr.Type, attr.Vals...)
}
return nil
}
// GetUser returns a user.
func (tx *backendTX) GetUser(ctx context.Context, username string) (*accountserver.User, error) {
// First of all, find the main user object, and just that one.
......
......@@ -44,6 +44,13 @@ func (s *AccountServer) handleCreateUser(tx as.TX, w http.ResponseWriter, r *htt
})
}
func (s *AccountServer) handleUpdateUser(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.UpdateUserRequest
return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) {
return s.service.UpdateUser(ctx, tx, &req)
})
}
func (s *AccountServer) handleChangeUserPassword(tx as.TX, w http.ResponseWriter, r *http.Request) (interface{}, error) {
var req as.ChangeUserPasswordRequest
return handleJSON(w, r, &req, func(ctx context.Context) (interface{}, error) {
......@@ -171,6 +178,7 @@ func (s *AccountServer) Handler() http.Handler {
h := http.NewServeMux()
h.HandleFunc("/api/user/get", s.withTx(s.handleGetUser))
h.HandleFunc("/api/user/create", s.withTx(s.handleCreateUser))
h.HandleFunc("/api/user/update", s.withTx(s.handleUpdateUser))
h.HandleFunc("/api/user/change_password", s.withTx(s.handleChangeUserPassword))
h.HandleFunc("/api/user/set_password_recovery_hint", s.withTx(s.handleSetPasswordRecoveryHint))
h.HandleFunc("/api/user/enable_otp", s.withTx(s.handleEnableOTP))
......
......@@ -43,6 +43,7 @@ type TX interface {
HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
GetUser(context.Context, string) (*User, error)
UpdateUser(context.Context, *User) error
CreateUser(context.Context, *User) error
SetUserPassword(context.Context, *User, string) error
SetPasswordRecoveryHint(context.Context, *User, string, string) error
......
......@@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"time"
"github.com/tstranex/u2f"
)
// Some considerations about the model design:
......@@ -31,6 +33,8 @@ type User struct {
AppSpecificPasswords []*AppSpecificPasswordInfo `json:"app_specific_passwords,omitempty"`
U2FRegistrations []*u2f.Registration `json:"u2f_registrations"`
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