Skip to content
Snippets Groups Projects
Select Git revision
  • test-deb
  • master default protected
  • renovate/github.com-tklauser-go-sysconf-0.x
  • renovate/github.com-prometheus-client_golang-1.x
  • v2
5 results

docs

Blame
  • actions_user.go 13.85 KiB
    package accountserver
    
    import (
    	"errors"
    	"strings"
    
    	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
    	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
    	}
    
    	// 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 := &ct.AppSpecificPassword{
    		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. We
    	// don't really expect a bad value coming from the generator, so the
    	// length check is just for internal consistency.
    	if r.TOTPSecret != "" && len(r.TOTPSecret) < 10 {
    		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. Fields are
    // associated with a "set_field" attribute to allow for selective updates.
    type UpdateUserRequest struct {
    	UserRequestBase
    
    	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 r.SetU2FRegistrations && len(r.U2FRegistrations) > maxU2FRegistrations {
    		return newValidationError(nil, "u2f_registrations", "too many U2F registrations")
    	}
    
    	// TODO: better validation of the language code!
    	if r.SetLang && (r.Lang == "" || 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.SetLang {
    		rctx.User.Lang = strings.ToLower(r.Lang)
    	}
    
    	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 privileged 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)
    }