github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/errors.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package store
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"net/url"
    26  	"sort"
    27  	"strings"
    28  
    29  	"github.com/snapcore/snapd/snap/channel"
    30  	"github.com/snapcore/snapd/strutil"
    31  )
    32  
    33  var (
    34  	// ErrBadQuery is returned from Find when the query has special characters in strange places.
    35  	ErrBadQuery = errors.New("bad query")
    36  
    37  	// ErrInvalidScope is returned from Find when an invalid scope is requested.
    38  	ErrInvalidScope = errors.New("invalid scope")
    39  
    40  	// ErrSnapNotFound is returned when a snap can not be found
    41  	ErrSnapNotFound = errors.New("snap not found")
    42  
    43  	// ErrUnauthenticated is returned when authentication is needed to complete the query
    44  	ErrUnauthenticated = errors.New("you need to log in first")
    45  
    46  	// ErrAuthenticationNeeds2fa is returned if the authentication needs 2factor
    47  	ErrAuthenticationNeeds2fa = errors.New("two factor authentication required")
    48  
    49  	// Err2faFailed is returned when 2fa failed (e.g., a bad token was given)
    50  	Err2faFailed = errors.New("two factor authentication failed")
    51  
    52  	// ErrInvalidCredentials is returned on login error
    53  	// It can also be returned when refreshing the discharge
    54  	// macaroon if the user has changed their password.
    55  	ErrInvalidCredentials = errors.New("invalid credentials")
    56  
    57  	// ErrTOSNotAccepted is returned when the user has not accepted the store's terms of service.
    58  	ErrTOSNotAccepted = errors.New("terms of service not accepted")
    59  
    60  	// ErrNoPaymentMethods is returned when the user has no valid payment methods associated with their account.
    61  	ErrNoPaymentMethods = errors.New("no payment methods")
    62  
    63  	// ErrPaymentDeclined is returned when the user's payment method was declined by the upstream payment provider.
    64  	ErrPaymentDeclined = errors.New("payment declined")
    65  
    66  	// ErrLocalSnap is returned when an operation that only applies to snaps that come from a store was attempted on a local snap.
    67  	ErrLocalSnap = errors.New("cannot perform operation on local snap")
    68  
    69  	// ErrNoUpdateAvailable is returned when an update is attempetd for a snap that has no update available.
    70  	ErrNoUpdateAvailable = errors.New("snap has no updates available")
    71  )
    72  
    73  // RevisionNotAvailableError is returned when an install is attempted for a snap but the/a revision is not available (given install constraints).
    74  type RevisionNotAvailableError struct {
    75  	Action   string
    76  	Channel  string
    77  	Releases []channel.Channel
    78  }
    79  
    80  func (e *RevisionNotAvailableError) Error() string {
    81  	return "no snap revision available as specified"
    82  }
    83  
    84  // DownloadError represents a download error
    85  type DownloadError struct {
    86  	Code int
    87  	URL  *url.URL
    88  }
    89  
    90  func (e *DownloadError) Error() string {
    91  	return fmt.Sprintf("received an unexpected http response code (%v) when trying to download %s", e.Code, e.URL)
    92  }
    93  
    94  // PasswordPolicyError is returned in a few corner cases, most notably
    95  // when the password has been force-reset.
    96  type PasswordPolicyError map[string]stringList
    97  
    98  func (e PasswordPolicyError) Error() string {
    99  	var msg string
   100  
   101  	if reason, ok := e["reason"]; ok && len(reason) == 1 {
   102  		msg = reason[0]
   103  		if location, ok := e["location"]; ok && len(location) == 1 {
   104  			msg += "\nTo address this, go to: " + location[0] + "\n"
   105  		}
   106  	} else {
   107  		for k, vs := range e {
   108  			msg += fmt.Sprintf("%s: %s\n", k, strings.Join(vs, "  "))
   109  		}
   110  	}
   111  
   112  	return msg
   113  }
   114  
   115  // InvalidAuthDataError signals that the authentication data didn't pass validation.
   116  type InvalidAuthDataError map[string]stringList
   117  
   118  func (e InvalidAuthDataError) Error() string {
   119  	var es []string
   120  	for _, v := range e {
   121  		es = append(es, v...)
   122  	}
   123  	// XXX: confirm with server people that extra args are all
   124  	//      full sentences (with periods and capitalization)
   125  	//      (empirically this checks out)
   126  	return strings.Join(es, "  ")
   127  }
   128  
   129  // SnapActionError conveys errors that were reported on otherwise overall successful snap action (install/refresh) request.
   130  type SnapActionError struct {
   131  	// NoResults is set if there were no results in the response
   132  	NoResults bool
   133  	// Refresh errors by snap name.
   134  	Refresh map[string]error
   135  	// Install errors by snap name.
   136  	Install map[string]error
   137  	// Download errors by snap name.
   138  	Download map[string]error
   139  	// Other errors.
   140  	Other []error
   141  }
   142  
   143  // SingleOpError returns the single operation, snap name, and error if
   144  // e represents a single error of a single operation on a single snap
   145  // (i.e. if e.Other is empty, and e.Refresh, e.Install and e.Download
   146  // have a single error in total).
   147  // In any other case, the error returned will be nil.
   148  func (e SnapActionError) SingleOpError() (op, name string, err error) {
   149  	if len(e.Other) > 0 {
   150  		return "", "", nil
   151  	}
   152  
   153  	nRefresh := len(e.Refresh)
   154  	nInstall := len(e.Install)
   155  	nDownload := len(e.Download)
   156  	if nRefresh+nInstall+nDownload != 1 {
   157  		return "", "", nil
   158  	}
   159  
   160  	var errs map[string]error
   161  	switch {
   162  	case nRefresh > 0:
   163  		op = "refresh"
   164  		errs = e.Refresh
   165  	case nInstall > 0:
   166  		op = "install"
   167  		errs = e.Install
   168  	case nDownload > 0:
   169  		op = "download"
   170  		errs = e.Download
   171  	}
   172  	for name, err = range errs {
   173  		return op, name, err
   174  	}
   175  	// can't happen
   176  	return "", "", nil
   177  }
   178  
   179  func (e SnapActionError) Error() string {
   180  	nRefresh := len(e.Refresh)
   181  	nInstall := len(e.Install)
   182  	nDownload := len(e.Download)
   183  	nOther := len(e.Other)
   184  
   185  	// single error
   186  	switch nRefresh + nInstall + nDownload + nOther {
   187  	case 0:
   188  		if e.NoResults {
   189  			// this is an atypical result
   190  			return "no install/refresh information results from the store"
   191  		}
   192  	case 1:
   193  		if nOther == 0 {
   194  			op, name, err := e.SingleOpError()
   195  			return fmt.Sprintf("cannot %s snap %q: %v", op, name, err)
   196  		} else {
   197  			return fmt.Sprintf("cannot refresh, install, or download: %v", e.Other[0])
   198  		}
   199  	}
   200  
   201  	header := "cannot refresh, install, or download:"
   202  	if nOther == 0 {
   203  		// at least one of nDownload, nInstall, or nRefresh is > 0
   204  		switch {
   205  		case nDownload == 0 && nRefresh == 0:
   206  			header = "cannot install:"
   207  		case nDownload == 0 && nInstall == 0:
   208  			header = "cannot refresh:"
   209  		case nRefresh == 0 && nInstall == 0:
   210  			header = "cannot download:"
   211  		case nDownload == 0:
   212  			header = "cannot refresh or install:"
   213  		case nInstall == 0:
   214  			header = "cannot refresh or download:"
   215  		case nRefresh == 0:
   216  			header = "cannot install or download:"
   217  		}
   218  	}
   219  
   220  	// reverse the "snap->error" map to "error->snap", as the
   221  	// common case is that all snaps fail with the same error
   222  	// (e.g. "no refresh available")
   223  	errToSnaps := map[string][]string{}
   224  	errKeys := []string{} // poorman's ordered map
   225  
   226  	for _, m := range []map[string]error{e.Refresh, e.Install, e.Download} {
   227  		for snapName, err := range m {
   228  			k := err.Error()
   229  			v, ok := errToSnaps[k]
   230  			if !ok {
   231  				errKeys = append(errKeys, k)
   232  			}
   233  			errToSnaps[k] = append(v, snapName)
   234  		}
   235  	}
   236  
   237  	es := make([]string, 1, 1+len(errToSnaps)+nOther)
   238  	es[0] = header
   239  	for _, k := range errKeys {
   240  		sort.Strings(errToSnaps[k])
   241  		es = append(es, fmt.Sprintf("%s: %s", k, strutil.Quoted(errToSnaps[k])))
   242  	}
   243  
   244  	for _, e := range e.Other {
   245  		es = append(es, e.Error())
   246  	}
   247  
   248  	if len(es) == 2 {
   249  		// header + 1 reason
   250  		return strings.Join(es, " ")
   251  	}
   252  
   253  	return strings.Join(es, "\n")
   254  }
   255  
   256  // Authorization soft-expiry errors that get handled automatically.
   257  var (
   258  	errUserAuthorizationNeedsRefresh   = errors.New("soft-expired user authorization needs refresh")
   259  	errDeviceAuthorizationNeedsRefresh = errors.New("soft-expired device authorization needs refresh")
   260  )
   261  
   262  func translateSnapActionError(action, snapChannel, code, message string, releases []snapRelease) error {
   263  	switch code {
   264  	case "revision-not-found":
   265  		e := &RevisionNotAvailableError{
   266  			Action:  action,
   267  			Channel: snapChannel,
   268  		}
   269  		if len(releases) != 0 {
   270  			parsedReleases := make([]channel.Channel, len(releases))
   271  			for i := 0; i < len(releases); i++ {
   272  				var err error
   273  				parsedReleases[i], err = channel.Parse(releases[i].Channel, releases[i].Architecture)
   274  				if err != nil {
   275  					// shouldn't happen, return error without Releases
   276  					return e
   277  				}
   278  			}
   279  			e.Releases = parsedReleases
   280  		}
   281  		return e
   282  	case "id-not-found", "name-not-found":
   283  		return ErrSnapNotFound
   284  	case "user-authorization-needs-refresh":
   285  		return errUserAuthorizationNeedsRefresh
   286  	case "device-authorization-needs-refresh":
   287  		return errDeviceAuthorizationNeedsRefresh
   288  	default:
   289  		return fmt.Errorf("%v", message)
   290  	}
   291  }