package accountserver import ( "errors" umdb "git.autistici.org/id/usermetadb" ) // GetUserRequest retrieves a specific User. type GetUserRequest struct { UserRequestBase // Whether to return an inactive user. IncludeInactive bool `json:"include_inactive"` } // Serve the request. func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) { if !r.IncludeInactive && rctx.User.Status != UserStatusActive { return nil, ErrUserNotFound } // Return the public User object contained within the RawUser. return &rctx.User.User, nil } // SearchUserRequest searches the database for users with names // matching a given pattern. The actual pattern semantics are // backend-specific (for LDAP, this is a prefix string search). type SearchUserRequest struct { AdminRequestBase Pattern string `json:"pattern"` Limit int `json:"limit"` } // Validate the request. func (r *SearchUserRequest) Validate(rctx *RequestContext) error { if r.Pattern == "" { return newValidationError(nil, "pattern", "empty pattern") } return nil } // SearchUserResponse is the response type for SearchUserRequest. type SearchUserResponse struct { Usernames []string `json:"usernames"` } // Serve the request. func (r *SearchUserRequest) Serve(rctx *RequestContext) (interface{}, error) { usernames, err := rctx.TX.SearchUser(rctx.Context, r.Pattern, r.Limit) if err != nil { return nil, err } return &SearchUserResponse{Usernames: usernames}, nil } // ChangeUserPasswordRequest updates a user's password. It will also take // care of re-encrypting the user encryption key, if present. type ChangeUserPasswordRequest struct { PrivilegedRequestBase Password string `json:"password"` } // Sanitize the request. func (r *ChangeUserPasswordRequest) Sanitize() { r.PrivilegedRequestBase.Sanitize() if r.Password != "" { r.Password = sanitizedValue } } // Validate the request. func (r *ChangeUserPasswordRequest) Validate(rctx *RequestContext) error { if err := rctx.fieldValidators.password(rctx, r.Password); err != nil { return newValidationError(nil, "password", err.Error()) } if r.Password == r.CurPassword { return newValidationError(nil, "password", "The new password can't be the same as the old one") } return r.PrivilegedRequestBase.Validate(rctx) } // Serve the request. func (r *ChangeUserPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) { err := rctx.User.setPrimaryPassword(rctx.Context, rctx.TX, r.CurPassword, r.Password, rctx.enableOpportunisticEncryption) if err != nil { return nil, err } rctx.audit.Log(rctx, nil, "password changed (user)") rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordChange, "password changed (user)") return nil, err } // AccountRecoveryRequest lets users reset their password by providing // secondary credentials, which we authenticate ourselves. It is not // authenticated with SSO. // // Two-factor authentication is disabled on successful recovery. type AccountRecoveryRequest struct { Username string `json:"username"` RecoveryPassword string `json:"recovery_password"` Password string `json:"password"` RemoteAddr string `json:"remote_addr"` // TODO: add full DeviceInfo? } // AccountRecoveryResponse is the response type for AccountRecoveryRequest. type AccountRecoveryResponse struct { Hint string `json:"hint,omitempty"` } // Sanitize the request. func (r *AccountRecoveryRequest) Sanitize() { if r.RecoveryPassword != "" { r.RecoveryPassword = sanitizedValue } if r.Password != "" { r.Password = sanitizedValue } } // Validate the request. func (r *AccountRecoveryRequest) Validate(rctx *RequestContext) error { // Only validate the password if attempting recovery. if r.RecoveryPassword != "" { if err := rctx.fieldValidators.password(rctx, r.Password); err != nil { return newValidationError(nil, "password", err.Error()) } } return nil } // PopulateContext extracts information from the request and stores it // into the RequestContext. func (r *AccountRecoveryRequest) PopulateContext(rctx *RequestContext) error { user, err := getUserOrDie(rctx.Context, rctx.TX, r.Username) if err != nil { return err } rctx.User = user return nil } // Authorize the request. func (r *AccountRecoveryRequest) Authorize(rctx *RequestContext) error { // The user must be in the 'active' state. if rctx.User.Status != UserStatusActive { return errors.New("user is not active") } // Anyone can request the hint (rate-limit above this layer). if r.RecoveryPassword == "" { 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 } return nil } // Serve the request. func (r *AccountRecoveryRequest) Serve(rctx *RequestContext) (interface{}, error) { resp := AccountRecoveryResponse{ Hint: rctx.User.AccountRecoveryHint, } // Only attempt to authenticate if the recovery password is // set in the request, otherwise just return the hint. if r.RecoveryPassword == "" { return &resp, nil } if err := rctx.User.setPrimaryPassword(rctx.Context, rctx.TX, r.RecoveryPassword, r.Password, rctx.enableOpportunisticEncryption); err != nil { return nil, err } if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil { return nil, err } rctx.audit.Log(rctx, nil, "password changed (account recovery)") rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password changed (account recovery)") return nil, nil } // ResetPasswordRequest is an admin operation to forcefully reset the // password for an account. A new password will be randomly generated // by the accountserver. The user will lose access to all stored email // (because the encryption keys will be reset) and to 2FA. type ResetPasswordRequest struct { AdminUserRequestBase } // ResetPasswordResponse is the response type for ResetPasswordRequest. type ResetPasswordResponse struct { Password string `json:"password"` } // Sanitize the response. func (r *ResetPasswordResponse) Sanitize() { if r.Password != "" { r.Password = sanitizedValue } } // Serve the request. func (r *ResetPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) { password := randomPassword() if err := rctx.User.resetPassword(rctx.Context, rctx.TX, password); err != nil { return nil, err } if err := rctx.User.disable2FA(rctx.Context, rctx.TX); err != nil { return nil, err } rctx.audit.Log(rctx, nil, "password reset (admin)") rctx.logUserAction(&rctx.User.User, umdb.LogTypePasswordReset, "password reset (admin)") return &ResetPasswordResponse{ Password: password, }, nil } // SetAccountRecoveryHintRequest lets users set the password recovery hint // and expected response (secondary password). type SetAccountRecoveryHintRequest struct { PrivilegedRequestBase Hint string `json:"recovery_hint"` Response string `json:"recovery_response"` } // Sanitize the request. func (r *SetAccountRecoveryHintRequest) Sanitize() { r.PrivilegedRequestBase.Sanitize() if r.Response != "" { r.Response = sanitizedValue } } // Validate the request. func (r *SetAccountRecoveryHintRequest) Validate(rctx *RequestContext) error { var err *ValidationError if r.Hint == "" { err = newValidationError(err, "recovery_hint", "mandatory field") } if verr := rctx.fieldValidators.password(rctx, r.Response); verr != nil { err = newValidationError(err, "recovery_response", verr.Error()) } return err.orNil() } // Serve the request. func (r *SetAccountRecoveryHintRequest) Serve(rctx *RequestContext) (interface{}, error) { return nil, rctx.User.setAccountRecoveryHint(rctx.Context, rctx.TX, r.CurPassword, r.Hint, r.Response) } // CreateApplicationSpecificPasswordRequest lets users create their own ASPs. type CreateApplicationSpecificPasswordRequest struct { PrivilegedRequestBase Service string `json:"service"` Notes string `json:"notes"` } // CreateApplicationSpecificPasswordResponse is the response type for // CreateApplicationSpecificPasswordRequest. type CreateApplicationSpecificPasswordResponse struct { Password string `json:"password"` } // Sanitize the response. func (r *CreateApplicationSpecificPasswordResponse) Sanitize() { if r.Password != "" { r.Password = sanitizedValue } } // Validate the request. func (r *CreateApplicationSpecificPasswordRequest) Validate(_ *RequestContext) error { var err *ValidationError if r.Service == "" { err = newValidationError(err, "service", "mandatory field") } return err.orNil() } // Serve the request. func (r *CreateApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) { if !rctx.User.Has2FA { return nil, errors.New("2FA is not enabled for this user") } // Create a new application-specific password metadata. asp := &AppSpecificPasswordInfo{ ID: randomAppSpecificPasswordID(), Service: r.Service, Comment: r.Notes, } pw := randomAppSpecificPassword() if err := rctx.User.addApplicationSpecificPassword(rctx.Context, rctx.TX, r.CurPassword, pw, asp); err != nil { return nil, err } return &CreateApplicationSpecificPasswordResponse{ Password: pw, }, nil } // DeleteApplicationSpecificPasswordRequest deletes an application-specific // password, identified by its unique ID. type DeleteApplicationSpecificPasswordRequest struct { UserRequestBase AspID string `json:"asp_id"` } // Serve the request. func (r *DeleteApplicationSpecificPasswordRequest) Serve(rctx *RequestContext) (interface{}, error) { return nil, rctx.User.deleteApplicationSpecificPassword(rctx.Context, rctx.TX, r.AspID) } // EnableOTPRequest 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. type EnableOTPRequest struct { UserRequestBase TOTPSecret string `json:"totp_secret"` } // Sanitize the request. func (r *EnableOTPRequest) Sanitize() { r.UserRequestBase.Sanitize() if r.TOTPSecret != "" { r.TOTPSecret = sanitizedValue } } // Validate the request. func (r *EnableOTPRequest) Validate(_ *RequestContext) error { var err *ValidationError // Only check if the client-side secret is set, skip otherwise. if r.TOTPSecret == "" && len(r.TOTPSecret) != 16 { err = newValidationError(err, "totp_secret", "bad value") } return err.orNil() } // EnableOTPResponse is the response type for AccountService.EnableOTP(). type EnableOTPResponse struct { TOTPSecret string `json:"totp_secret"` } // Sanitize the response. func (r *EnableOTPResponse) Sanitize() { if r.TOTPSecret != "" { r.TOTPSecret = sanitizedValue } } // Serve the request. func (r *EnableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) { // Replace or initialize the TOTP secret. secret := r.TOTPSecret if secret == "" { var err error secret, err = generateTOTPSecret() if err != nil { return nil, err } } if err := rctx.User.setTOTPSecret(rctx.Context, rctx.TX, secret); err != nil { return nil, err } rctx.audit.Log(rctx, nil, "totp enabled") return &EnableOTPResponse{ TOTPSecret: secret, }, nil } // DisableOTPRequest disables two-factor authentication for a user. type DisableOTPRequest struct { UserRequestBase } // Serve the request. func (r *DisableOTPRequest) Serve(rctx *RequestContext) (interface{}, error) { if err := rctx.User.disableOTP(rctx.Context, rctx.TX); err != nil { return nil, err } rctx.audit.Log(rctx, nil, "totp disabled") return nil, nil } // 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. type UpdateUserRequest struct { UserRequestBase Lang string `json:"lang,omitempty"` U2FRegistrations []*U2FRegistration `json:"u2f_registrations,omitempty"` } const maxU2FRegistrations = 20 // Validate the request. func (r *UpdateUserRequest) Validate(rctx *RequestContext) error { if 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 { return newValidationError(nil, "lang", "invalid language code") } return nil } // Serve the request. func (r *UpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) { if r.Lang != "" { rctx.User.Lang = 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) } // AdminUpdateUserRequest is the privileged version of UpdateUser and // allows to update many more attributes. It is a catch-all function // for very simple changes that don't justify their own specialized // method. type AdminUpdateUserRequest struct { AdminUserRequestBase Lang string `json:"lang,omitempty"` Status string `json:"status"` } // Validate the request. func (r *AdminUpdateUserRequest) Validate(rctx *RequestContext) error { switch r.Status { case "", ResourceStatusActive, ResourceStatusInactive: default: return newValidationError(nil, "status", "invalid or unknown status") } return nil } // Serve the request. func (r *AdminUpdateUserRequest) Serve(rctx *RequestContext) (interface{}, error) { if r.Status != "" { rctx.User.Status = r.Status } return nil, rctx.TX.UpdateUser(rctx.Context, &rctx.User.User) }