package accountserver import ( "context" "crypto/rand" "encoding/base64" "errors" "fmt" "log" "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. type RequestBase struct { Username string `json:"username"` SSO string `json:"sso"` // Optional comment, will end up in audit logs. Comment string `json:"comment,omitempty"` } type userCtxKeyType int var userCtxKey userCtxKeyType func userFromContext(ctx context.Context) string { s, ok := ctx.Value(userCtxKey).(string) if ok { return s } return "" } type commentCtxKeyType int var commentCtxKey commentCtxKeyType func commentFromContext(ctx context.Context) string { s, ok := ctx.Value(commentCtxKey).(string) if ok { return s } return "" } // NewContext returns a new Context with some request-related values set. func (r RequestBase) NewContext(ctx context.Context) context.Context { ctx = context.WithValue(ctx, userCtxKey, r.Username) if r.Comment != "" { ctx = context.WithValue(ctx, commentCtxKey, r.Comment) } return ctx } // PrivilegedRequestBase extends RequestBase with the user password, // for privileged endpoints. type PrivilegedRequestBase struct { RequestBase CurPassword string `json:"cur_password"` } // ResourceRequestBase is the base type for resource-level requests. type ResourceRequestBase struct { ResourceID ResourceID `json:"resource_id"` SSO string `json:"sso"` // Optional comment, will end up in audit logs. Comment string `json:"comment,omitempty"` } // NewContext returns a new Context with some request-related values set. func (r ResourceRequestBase) NewContext(ctx context.Context) context.Context { if u := r.ResourceID.User(); u != "" { ctx = context.WithValue(ctx, userCtxKey, u) } if r.Comment != "" { ctx = context.WithValue(ctx, commentCtxKey, r.Comment) } return ctx } // GetUserRequest is the request type for AccountService.GetUser(). type GetUserRequest struct { RequestBase } // GetUser returns public information about a user. func (s *AccountService) GetUser(ctx context.Context, tx TX, req *GetUserRequest) (resp *User, err error) { err = s.handleUserRequest(ctx, tx, req, s.authUser(req.RequestBase), func(ctx context.Context, user *User) error { resp = user return nil }) return } // setResourceStatus sets the status of a single resource (shared // logic between enable / disable resource methods). func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, r *Resource, status string) error { r.Status = status if err := tx.UpdateResource(ctx, r); err != nil { return newBackendError(err) } s.audit.Log(ctx, r.ID, fmt.Sprintf("status set to %s", status)) return nil } // DisableResourceRequest is the request type for AccountService.DisableResource(). type DisableResourceRequest struct { ResourceRequestBase } // DisableResource disables a resource belonging to the user. func (s *AccountService) DisableResource(ctx context.Context, tx TX, req *DisableResourceRequest) error { return s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { return s.setResourceStatus(ctx, tx, r, ResourceStatusInactive) }) } // EnableResourceRequest is the request type for AccountService.EnableResource(). type EnableResourceRequest struct { ResourceRequestBase } // EnableResource enables a resource belonging to the user. func (s *AccountService) EnableResource(ctx context.Context, tx TX, req *EnableResourceRequest) error { return s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { return s.setResourceStatus(ctx, tx, r, ResourceStatusActive) }) } // ChangeUserPasswordRequest is the request type for AccountService.ChangeUserPassword(). type ChangeUserPasswordRequest struct { PrivilegedRequestBase Password string `json:"password"` } // Validate the request. func (r *ChangeUserPasswordRequest) Validate(ctx context.Context, s *AccountService) error { return s.fieldValidators.password(ctx, r.Password) } // ChangeUserPassword updates a user's password. It will also take // care of re-encrypting the user encryption key, if present. func (s *AccountService) ChangeUserPassword(ctx context.Context, tx TX, req *ChangeUserPasswordRequest) error { return s.handleUserRequest(ctx, tx, req, s.authUserWithPassword(req.PrivilegedRequestBase), func(ctx context.Context, user *User) error { return s.changeUserPasswordAndUpdateEncryptionKeys(ctx, tx, user, req.CurPassword, req.Password) }) } // PasswordRecoveryRequest is the request type for AccountService.RecoverPassword(). // It is not authenticated with SSO. type PasswordRecoveryRequest struct { Username string `json:"username"` RecoveryPassword string `json:"recovery_password"` Password string `json:"password"` } // NewContext adds logging data to the current request context. func (r *PasswordRecoveryRequest) NewContext(ctx context.Context) context.Context { return context.WithValue(ctx, userCtxKey, r.Username) } // Validate the request. func (r *PasswordRecoveryRequest) Validate(ctx context.Context, s *AccountService) error { return s.fieldValidators.password(ctx, r.Password) } // RecoverPassword lets users reset their password by providing // secondary credentials, which we authenticate ourselves. // // TODO: call out to auth-server for secondary authentication? func (s *AccountService) RecoverPassword(ctx context.Context, tx TX, req *PasswordRecoveryRequest) error { user, err := getUserOrDie(ctx, tx, req.Username) if err != nil { return err } // Authenticate the secret recovery password. if !pwhash.ComparePassword(tx.GetUserRecoveryEncryptedPassword(ctx, user), req.RecoveryPassword) { return ErrUnauthorized } ctx = context.WithValue(ctx, authUserCtxKey, req.Username) return s.withRequest(ctx, tx, req, user, func(ctx context.Context) error { s.audit.Log(ctx, ResourceID{}, "password reset via account recovery") // Change the user password (the recovery password does not change). if err := s.changeUserPasswordAndUpdateEncryptionKeys(ctx, tx, user, req.RecoveryPassword, req.Password); err != nil { return err } // Disable 2FA. return s.disable2FA(ctx, tx, user) }) } // ResetPasswordRequest is the request type for AccountService.ResetPassword(). type ResetPasswordRequest struct { RequestBase Password string `json:"password"` } // Validate the request. func (r *ResetPasswordRequest) Validate(ctx context.Context, s *AccountService) error { return s.fieldValidators.password(ctx, r.Password) } // ResetPassword is an admin operation to forcefully reset the // password for an account. The user will lose access to all stored // email (because the encryption keys will be reset) and to 2FA. func (s *AccountService) ResetPassword(ctx context.Context, tx TX, req *ResetPasswordRequest) error { return s.handleUserRequest(ctx, tx, req, s.authAdmin(req.RequestBase), func(ctx context.Context, user *User) error { // Disable 2FA. if err := s.disable2FA(ctx, tx, user); err != nil { return err } // Reset encryption keys and set the new password. return s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, user, req.Password) }) } // SetPasswordRecoveryHintRequest is the request type for // AccountService.SetPasswordRecoveryHint(). type SetPasswordRecoveryHintRequest struct { PrivilegedRequestBase Hint string `json:"recovery_hint"` Response string `json:"recovery_response"` } // Validate the request. func (r *SetPasswordRecoveryHintRequest) Validate(ctx context.Context, s *AccountService) error { return s.fieldValidators.password(ctx, r.Response) } // SetPasswordRecoveryHint lets users set the password recovery hint // and expected response (secondary password). func (s *AccountService) SetPasswordRecoveryHint(ctx context.Context, tx TX, req *SetPasswordRecoveryHintRequest) error { return s.handleUserRequest(ctx, tx, req, s.authUserWithPassword(req.PrivilegedRequestBase), func(ctx context.Context, user *User) error { // If the encryption keys are not set up yet, use the // CurPassword to initialize them. keys, decrypted, err := s.readOrInitializeEncryptionKeys(ctx, tx, user, req.CurPassword, req.CurPassword) if err != nil { return err } keys, err = updateEncryptionKey(keys, decrypted, UserEncryptionKeyRecoveryID, req.Response) if err != nil { return err } if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil { return newBackendError(err) } encResponse := pwhash.Encrypt(req.Response) return tx.SetPasswordRecoveryHint(ctx, user, req.Hint, encResponse) }) } // Change the user password and update encryption keys, provided we // have a password that we can use to decrypt them first. func (s *AccountService) changeUserPasswordAndUpdateEncryptionKeys(ctx context.Context, tx TX, user *User, oldPassword, newPassword string) error { // If the user does not yet have an encryption key, generate one now. var err error keys, decrypted, err := s.readOrInitializeEncryptionKeys(ctx, tx, user, oldPassword, newPassword) if err != nil { return err } keys, err = updateEncryptionKey(keys, decrypted, UserEncryptionKeyMainID, newPassword) if err != nil { return err } if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil { return newBackendError(err) } // Set the encrypted password attribute on the user (will set it on emails too). encPass := pwhash.Encrypt(newPassword) if err := tx.SetUserPassword(ctx, user, encPass); err != nil { return newBackendError(err) } s.audit.Log(ctx, ResourceID{}, "password changed") return nil } // Change the user password and reset all encryption keys. Existing email // won't be readable anymore. Existing 2FA credentials will be deleted. func (s *AccountService) changeUserPasswordAndResetEncryptionKeys(ctx context.Context, tx TX, user *User, newPassword string) error { // Calling initialize will wipe the current keys and replace // them with a new one. keys, _, err := s.initializeEncryptionKeys(ctx, tx, user, newPassword) if err != nil { return err } if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil { return newBackendError(err) } // Set the encrypted password attribute on the user (will set it on emails too). encPass := pwhash.Encrypt(newPassword) if err := tx.SetUserPassword(ctx, user, encPass); err != nil { return newBackendError(err) } s.audit.Log(ctx, ResourceID{}, "password reset") return nil } // Disable 2FA for a user account. func (s *AccountService) disable2FA(ctx context.Context, tx TX, user *User) error { // Disable OTP. if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil { return newBackendError(err) } // Disable U2F. if len(user.U2FRegistrations) > 0 { user.U2FRegistrations = nil if err := tx.UpdateUser(ctx, user); err != nil { return newBackendError(err) } } // Wipe all app-specific passwords. for _, asp := range user.AppSpecificPasswords { if err := tx.DeleteApplicationSpecificPassword(ctx, user, asp.ID); err != nil { return newBackendError(err) } } return nil } // CreateApplicationSpecificPasswordRequest is the request type for // AccountService.CreateApplicationSpecificPassword(). type CreateApplicationSpecificPasswordRequest struct { PrivilegedRequestBase Service string `json:"service"` Notes string `json:"notes"` } // Validate the request. func (r *CreateApplicationSpecificPasswordRequest) Validate(_ context.Context, _ *AccountService) error { if r.Service == "" { return errors.New("empty 'service' attribute") } return nil } // CreateApplicationSpecificPasswordResponse is the response type for // AccountService.CreateApplicationSpecificPassword(). type CreateApplicationSpecificPasswordResponse struct { Password string `json:"password"` } // CreateApplicationSpecificPassword will generate a new // application-specific password for the given service. func (s *AccountService) CreateApplicationSpecificPassword(ctx context.Context, tx TX, req *CreateApplicationSpecificPasswordRequest) (*CreateApplicationSpecificPasswordResponse, error) { var resp CreateApplicationSpecificPasswordResponse err := s.handleUserRequest(ctx, tx, req, s.authUserWithPassword(req.PrivilegedRequestBase), func(ctx context.Context, user *User) error { // No application-specific passwords unless 2FA is enabled. if !user.Has2FA { return newRequestError(errors.New("2FA is not enabled for this user")) } // Create a new application-specific password and set it in // the database. We don't need to update the User object as // we're not reusing it. asp := &AppSpecificPasswordInfo{ ID: randomAppSpecificPasswordID(), Service: req.Service, Comment: req.Notes, } password := randomAppSpecificPassword() encPass := pwhash.Encrypt(password) if err := tx.SetApplicationSpecificPassword(ctx, user, asp, encPass); err != nil { return newBackendError(err) } // Create or update the user encryption key associated with // this password. The user encryption key IDs for ASPs all // have an 'asp_' prefix, followed by the ASP ID. if user.HasEncryptionKeys { keys, decrypted, err := s.readOrInitializeEncryptionKeys(ctx, tx, user, req.CurPassword, req.CurPassword) if err != nil { return err } keyID := "asp_" + asp.ID keys, err = updateEncryptionKey(keys, decrypted, keyID, password) if err != nil { return err } if err := tx.SetUserEncryptionKeys(ctx, user, keys); err != nil { return newBackendError(err) } } resp.Password = password return nil }) return &resp, err } // DeleteApplicationSpecificPasswordRequest is the request type for // AccountService.DeleteApplicationSpecificPassword(). type DeleteApplicationSpecificPasswordRequest struct { RequestBase AspID string `json:"asp_id"` } // DeleteApplicationSpecificPassword destroys an application-specific // password, identified by its unique ID. func (s *AccountService) DeleteApplicationSpecificPassword(ctx context.Context, tx TX, req *DeleteApplicationSpecificPasswordRequest) error { return s.handleUserRequest(ctx, tx, req, s.authUser(req.RequestBase), func(ctx context.Context, user *User) error { if err := tx.DeleteApplicationSpecificPassword(ctx, user, req.AspID); err != nil { return err } // Delete the user encryption key associated with this // password (we're going to find it via its ID). keys, err := tx.GetUserEncryptionKeys(ctx, user) if err != nil { return newBackendError(err) } if len(keys) == 0 { return nil } aspKeyID := "asp_" + req.AspID var newKeys []*UserEncryptionKey for _, k := range keys { if k.ID != aspKeyID { newKeys = append(newKeys, k) } } return tx.SetUserEncryptionKeys(ctx, user, newKeys) }) } // ResetResourcePasswordRequest is the request type for AccountService.ResetResourcePassword(). type ResetResourcePasswordRequest struct { ResourceRequestBase } func resourceHasPassword(r *Resource) bool { switch r.ID.Type() { case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: return true default: return false } } // Validate the request. func (r *ResetResourcePasswordRequest) Validate(ctx context.Context, s *AccountService) error { switch r.ResourceID.Type() { case ResourceTypeDAV, ResourceTypeDatabase, ResourceTypeMailingList: case ResourceTypeEmail: return errors.New("can't reset email passwords independently") default: return errors.New("can't reset password on this resource type") } return nil } // ResetResourcePasswordResponse is the response type for AccountService.ResetResourcePassword(). type ResetResourcePasswordResponse struct { Password string `json:"password"` } // ResetResourcePassword can reset the password associated with a // resource (if the resource type supports it). It will generate a // random password and return it to the caller. func (s *AccountService) ResetResourcePassword(ctx context.Context, tx TX, req *ResetResourcePasswordRequest) (*ResetResourcePasswordResponse, error) { var resp ResetResourcePasswordResponse err := s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { newPassword, err := s.doResetResourcePassword(ctx, tx, r) if err != nil { return err } // Return the password to the caller, in cleartext. resp.Password = newPassword // TODO: This is where we may want to call out to // other backends in order to reset credentials for // certain resources that have their own secondary // authentication databases (lists, mysql). return nil }) return &resp, err } func (s *AccountService) doResetResourcePassword(ctx context.Context, tx TX, r *Resource) (string, error) { newPassword := randomPassword() encPassword := pwhash.Encrypt(newPassword) // TODO: this needs a resource type-switch. if err := tx.SetResourcePassword(ctx, r, encPassword); err != nil { return "", err } return newPassword, nil } // MoveResourceRequest is the request type for AccountService.MoveResource(). type MoveResourceRequest struct { RequestBase ResourceID ResourceID `json:"resource_id"` Shard string `json:"shard"` } // MoveResourceResponse is the response type for AccountService.MoveResource(). type MoveResourceResponse struct { MovedIDs []string `json:"moved_ids"` } // MoveResource is an administrative operation to move resources // between shards. Resources that are part of a group are moved all at // once regardless of which individual ResourceID is provided as long // as it belongs to the group. func (s *AccountService) MoveResource(ctx context.Context, tx TX, req *MoveResourceRequest) (*MoveResourceResponse, error) { var resp MoveResourceResponse err := s.handleUserRequest(ctx, tx, req, s.authAdmin(req.RequestBase), func(ctx context.Context, user *User) error { // Collect all related resources, as they should all be moved at once. r, err := tx.GetResource(ctx, req.ResourceID) if err != nil { return newBackendError(err) } var resources []*Resource if r.Group != "" { resources = append(resources, user.GetResourcesByGroup(r.Group)...) } else { resources = []*Resource{r} } for _, r := range resources { r.Shard = req.Shard if err := tx.UpdateResource(ctx, r); err != nil { return newBackendError(err) } resp.MovedIDs = append(resp.MovedIDs, r.ID.String()) } return nil }) return &resp, err } // EnableOTPRequest is the request type for AccountService.EnableOTP(). type EnableOTPRequest struct { RequestBase TOTPSecret string `json:"totp_secret"` } // Validate the request. func (r *EnableOTPRequest) Validate(_ context.Context, _ *AccountService) error { // TODO: the length here is bogus, replace with real value. if r.TOTPSecret != "" && len(r.TOTPSecret) != 32 { return errors.New("bad totp_secret value") } return nil } // EnableOTPResponse is the response type for AccountService.EnableOTP(). type EnableOTPResponse struct { TOTPSecret string `json:"totp_secret"` } // EnableOTP enables OTP-based two-factor authentication for a // user. The caller can generate the TOTP secret itself if needed // (useful for UX that confirms that the user is able to login first), // or it can let the server generate a new secret by passing an empty // totp_secret. func (s *AccountService) EnableOTP(ctx context.Context, tx TX, req *EnableOTPRequest) (*EnableOTPResponse, error) { var resp EnableOTPResponse err := s.handleUserRequest(ctx, tx, req, s.authUser(req.RequestBase), func(ctx context.Context, user *User) (err error) { // Replace or initialize the TOTP secret. if req.TOTPSecret == "" { req.TOTPSecret, err = generateTOTPSecret() if err != nil { return err } } if err := tx.SetUserTOTPSecret(ctx, user, req.TOTPSecret); err != nil { return newBackendError(err) } resp.TOTPSecret = req.TOTPSecret s.audit.Log(ctx, ResourceID{}, "totp enabled") return nil }) return &resp, err } // DisableOTPRequest is the request type for AccountService.DisableOTP(). type DisableOTPRequest struct { RequestBase } // DisableOTP disables two-factor authentication for a user. func (s *AccountService) DisableOTP(ctx context.Context, tx TX, req *DisableOTPRequest) error { return s.handleUserRequest(ctx, tx, req, s.authUser(req.RequestBase), func(ctx context.Context, user *User) error { // Delete the TOTP secret (if present). if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil { return newBackendError(err) } s.audit.Log(ctx, ResourceID{}, "totp disabled") return nil }) } // AddEmailAliasRequest is the request type for AccountService.AddEmailAlias(). type AddEmailAliasRequest struct { ResourceRequestBase Addr string `json:"addr"` } // Validate the request. func (r *AddEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error { if r.ResourceID.Type() != ResourceTypeEmail { return errors.New("this operation only works on email resources") } return s.fieldValidators.email(ctx, r.Addr) } const maxEmailAliases = 5 // AddEmailAlias adds an alias (additional address) to an email resource. func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmailAliasRequest) error { return s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { // Allow at most 5 aliases. if len(r.Email.Aliases) >= maxEmailAliases { return errors.New("too many aliases") } r.Email.Aliases = append(r.Email.Aliases, req.Addr) if err := tx.UpdateResource(ctx, r); err != nil { return err } s.audit.Log(ctx, r.ID, fmt.Sprintf("added alias %s", req.Addr)) return nil }) } // DeleteEmailAliasRequest is the request type for AccountService.DeleteEmailAlias(). type DeleteEmailAliasRequest struct { ResourceRequestBase Addr string `json:"addr"` } // Validate the request. func (r *DeleteEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error { if r.ResourceID.Type() != ResourceTypeEmail { return errors.New("this operation only works on email resources") } return nil } // DeleteEmailAlias removes an alias from an email resource. func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *DeleteEmailAliasRequest) error { return s.handleResourceRequest(ctx, tx, req, s.authResource(req.ResourceRequestBase), func(ctx context.Context, r *Resource) error { var aliases []string for _, a := range r.Email.Aliases { if a != req.Addr { aliases = append(aliases, a) } } r.Email.Aliases = aliases if err := tx.UpdateResource(ctx, r); err != nil { return newBackendError(err) } s.audit.Log(ctx, r.ID, fmt.Sprintf("removed alias %s", req.Addr)) return nil }) } // CreateResourcesRequest is the request type for AccountService.CreateResources(). type CreateResourcesRequest struct { SSO string `json:"sso"` Resources []*Resource `json:"resources"` } // CreateResourcesResponse is the response type for AccountService.CreateResources(). type CreateResourcesResponse struct { // Resources to create. All must either be global resources // (no user ownership), or belong to the same user. Resources []*Resource `json:"resources"` } // ApplyTemplate fills in default values for the resources in the request. func (req *CreateResourcesRequest) ApplyTemplate(ctx context.Context, _ TX, s *AccountService, user *User) error { for _, r := range req.Resources { s.resourceTemplates.applyTemplate(ctx, r, user) } return nil } // ValidationUser returns the user to be used for validation purposes. func (req *CreateResourcesRequest) ValidationUser(ctx context.Context, tx TX) *User { // Fetch the user associated with the first resource (if // any). Since resource validation might reference other // resources, we need to provide it with a view of what the // future resources will be. So we merge the resources from // the database with those from the request, using a local // copy of the User object. if len(req.Resources) > 0 { if username := req.Resources[0].ID.User(); username != "" { u, err := getUserOrDie(ctx, tx, username) if err != nil { return nil } user := *u user.Resources = mergeResources(u.Resources, req.Resources) return &user } } return nil } // Validate the request. func (req *CreateResourcesRequest) Validate(ctx context.Context, s *AccountService, user *User) error { var owner string if user != nil { owner = user.Name } for _, r := range req.Resources { // Check same-user ownership. if r.ID.User() != owner { return errors.New("resources owned by different users") } // Validate the resource. if err := s.resourceValidator.validateResource(ctx, r, user); err != nil { log.Printf("validation error while creating resource %+v: %v", r, err) return err } } return nil } // CreateResources can create one or more resources. func (s *AccountService) CreateResources(ctx context.Context, tx TX, req *CreateResourcesRequest) (*CreateResourcesResponse, error) { var resp CreateResourcesResponse err := s.handleAdminRequest(ctx, tx, req, req.SSO, func(ctx context.Context) error { for _, r := range req.Resources { if err := tx.CreateResource(ctx, r); err != nil { return newBackendError(err) } s.audit.Log(ctx, r.ID, "resource created") resp.Resources = append(resp.Resources, r) } return nil }) return &resp, err } // Merge two resource lists by ID (the second one wins), return a new list. func mergeResources(a, b []*Resource) []*Resource { tmp := make(map[string]*Resource) for _, l := range [][]*Resource{a, b} { for _, r := range l { tmp[r.ID.String()] = r } } out := make([]*Resource, 0, len(tmp)) for _, r := range tmp { out = append(out, r) } return out } // CreateUserRequest is the request type for AccountService.CreateUser(). type CreateUserRequest struct { SSO string `json:"sso"` User *User `json:"user"` } // NewContext returns a new Context providing log context data. func (req *CreateUserRequest) NewContext(ctx context.Context) context.Context { return context.WithValue(ctx, userCtxKey, req.User.Name) } // ApplyTemplate fills in default values for the resources in the request. func (req *CreateUserRequest) ApplyTemplate(ctx context.Context, tx TX, s *AccountService, _ *User) error { // Some fields should be unset because there are specific // methods to modify those attributes. req.User.Has2FA = false req.User.HasEncryptionKeys = false req.User.PasswordRecoveryHint = "" req.User.AppSpecificPasswords = nil if req.User.Lang == "" { req.User.Lang = "en" } // Allocate a new user ID. uid, err := tx.NextUID(ctx) if err != nil { return err } req.User.UID = uid // Apply templates to all resources in the request. for _, r := range req.User.Resources { s.resourceTemplates.applyTemplate(ctx, r, req.User) } return nil } // Validate the request. func (req *CreateUserRequest) Validate(ctx context.Context, s *AccountService, _ *User) error { // Validate the user *and* all resources. if err := s.userValidator(ctx, req.User); err != nil { log.Printf("validation error while creating user %+v: %v", req.User, err) return err } for _, r := range req.User.Resources { if err := s.resourceValidator.validateResource(ctx, r, req.User); err != nil { log.Printf("validation error while creating resource %+v: %v", r, err) return err } } return nil } // CreateUserResponse is the response type for AccountService.CreateUser(). type CreateUserResponse struct { User *User `json:"user,omitempty"` Password string `json:"password"` } // CreateUser creates a new user along with the associated resources. func (s *AccountService) CreateUser(ctx context.Context, tx TX, req *CreateUserRequest) (*CreateUserResponse, error) { var resp CreateUserResponse err := s.handleAdminRequest(ctx, tx, req, req.SSO, func(ctx context.Context) (err error) { // Create the user first, along with all the resources. if err := tx.CreateUser(ctx, req.User); err != nil { return newBackendError(err) } resp.User = req.User // Now set a password for the user and return it, and // set random passwords for all the resources // (currently, we don't care about those, the user // will reset them later). newPassword := randomPassword() if err := s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, req.User, newPassword); err != nil { return err } resp.Password = newPassword s.audit.Log(ctx, ResourceID{}, "user created") for _, r := range req.User.Resources { s.audit.Log(ctx, r.ID, "resource created") if resourceHasPassword(r) { if _, err := s.doResetResourcePassword(ctx, tx, r); err != nil { // Just log, don't fail. log.Printf("can't set random password for resource %s: %v", r.ID, err) } } } return nil }) 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[:]) if err != nil { panic(err) } return base64.StdEncoding.EncodeToString(b[:])[:n] } func randomPassword() string { // Create a 16-character password with 4 digits and 2 symbols. return password.MustGenerate(16, 4, 2, false, false) } func randomAppSpecificPassword() string { // Create a 64-character password with 10 digits and 10 symbols. return password.MustGenerate(64, 10, 10, false, false) } const appSpecificPasswordIDLen = 4 func randomAppSpecificPasswordID() string { return randomBase64(appSpecificPasswordIDLen) } func generateTOTPSecret() (string, error) { key, err := totp.Generate(totp.GenerateOpts{}) if err != nil { return "", err } return key.Secret(), nil }