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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2017 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_test
    21  
    22  import (
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"time"
    28  
    29  	. "gopkg.in/check.v1"
    30  	"gopkg.in/macaroon.v1"
    31  	"gopkg.in/retry.v1"
    32  
    33  	"github.com/snapcore/snapd/store"
    34  	"github.com/snapcore/snapd/testutil"
    35  )
    36  
    37  type authTestSuite struct {
    38  	testutil.BaseTest
    39  }
    40  
    41  var _ = Suite(&authTestSuite{})
    42  
    43  const mockStoreInvalidLoginCode = 401
    44  const mockStoreInvalidLogin = `
    45  {
    46      "message": "Provided email/password is not correct.", 
    47      "code": "INVALID_CREDENTIALS", 
    48      "extra": {}
    49  }
    50  `
    51  
    52  const mockStoreNeeds2faHTTPCode = 401
    53  const mockStoreNeeds2fa = `
    54  {
    55      "message": "2-factor authentication required.", 
    56      "code": "TWOFACTOR_REQUIRED", 
    57      "extra": {}
    58  }
    59  `
    60  
    61  const mockStore2faFailedHTTPCode = 403
    62  const mockStore2faFailedResponse = `
    63  {
    64      "message": "The provided 2-factor key is not recognised.", 
    65      "code": "TWOFACTOR_FAILURE", 
    66      "extra": {}
    67  }
    68  `
    69  
    70  const mockStoreReturnMacaroon = `{"macaroon": "the-root-macaroon-serialized-data"}`
    71  
    72  const mockStoreReturnDischarge = `{"discharge_macaroon": "the-discharge-macaroon-serialized-data"}`
    73  
    74  const mockStoreReturnNoMacaroon = `{}`
    75  
    76  const mockStoreReturnNonce = `{"nonce": "the-nonce"}`
    77  
    78  const mockStoreReturnNoNonce = `{}`
    79  
    80  func (s *authTestSuite) SetUpTest(c *C) {
    81  	store.MockDefaultRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second,
    82  		retry.Exponential{
    83  			Initial: 1 * time.Millisecond,
    84  			Factor:  1.1,
    85  		},
    86  	)))
    87  }
    88  
    89  func (s *authTestSuite) TestRequestStoreMacaroon(c *C) {
    90  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    91  		io.WriteString(w, mockStoreReturnMacaroon)
    92  	}))
    93  	defer mockServer.Close()
    94  	store.MacaroonACLAPI = mockServer.URL + "/acl/"
    95  
    96  	macaroon, err := store.RequestStoreMacaroon(&http.Client{})
    97  	c.Assert(err, IsNil)
    98  	c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
    99  }
   100  
   101  func (s *authTestSuite) TestRequestStoreMacaroonMissingData(c *C) {
   102  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   103  		io.WriteString(w, mockStoreReturnNoMacaroon)
   104  	}))
   105  	defer mockServer.Close()
   106  	store.MacaroonACLAPI = mockServer.URL + "/acl/"
   107  
   108  	macaroon, err := store.RequestStoreMacaroon(&http.Client{})
   109  	c.Assert(err, ErrorMatches, "cannot get snap access permission from store: empty macaroon returned")
   110  	c.Assert(macaroon, Equals, "")
   111  }
   112  
   113  func (s *authTestSuite) TestRequestStoreMacaroonError(c *C) {
   114  	n := 0
   115  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   116  		w.WriteHeader(500)
   117  		n++
   118  	}))
   119  	defer mockServer.Close()
   120  	store.MacaroonACLAPI = mockServer.URL + "/acl/"
   121  
   122  	macaroon, err := store.RequestStoreMacaroon(&http.Client{})
   123  	c.Assert(err, ErrorMatches, "cannot get snap access permission from store: store server returned status 500")
   124  	c.Assert(n, Equals, 5)
   125  	c.Assert(macaroon, Equals, "")
   126  }
   127  
   128  func (s *authTestSuite) TestDischargeAuthCaveat(c *C) {
   129  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   130  		io.WriteString(w, mockStoreReturnDischarge)
   131  	}))
   132  	defer mockServer.Close()
   133  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   134  
   135  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "guy@example.com", "passwd", "")
   136  	c.Assert(err, IsNil)
   137  	c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data")
   138  }
   139  
   140  func (s *authTestSuite) TestDischargeAuthCaveatNeeds2fa(c *C) {
   141  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   142  		w.WriteHeader(mockStoreNeeds2faHTTPCode)
   143  		io.WriteString(w, mockStoreNeeds2fa)
   144  	}))
   145  	defer mockServer.Close()
   146  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   147  
   148  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "")
   149  	c.Assert(err, Equals, store.ErrAuthenticationNeeds2fa)
   150  	c.Assert(discharge, Equals, "")
   151  }
   152  
   153  func (s *authTestSuite) TestDischargeAuthCaveatFails2fa(c *C) {
   154  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   155  		w.WriteHeader(mockStore2faFailedHTTPCode)
   156  		io.WriteString(w, mockStore2faFailedResponse)
   157  	}))
   158  	defer mockServer.Close()
   159  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   160  
   161  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "")
   162  	c.Assert(err, Equals, store.Err2faFailed)
   163  	c.Assert(discharge, Equals, "")
   164  }
   165  
   166  func (s *authTestSuite) TestDischargeAuthCaveatInvalidLogin(c *C) {
   167  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   168  		w.WriteHeader(mockStoreInvalidLoginCode)
   169  		io.WriteString(w, mockStoreInvalidLogin)
   170  	}))
   171  	defer mockServer.Close()
   172  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   173  
   174  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "")
   175  	c.Assert(err, Equals, store.ErrInvalidCredentials)
   176  	c.Assert(discharge, Equals, "")
   177  }
   178  
   179  func (s *authTestSuite) TestDischargeAuthCaveatMissingData(c *C) {
   180  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   181  		io.WriteString(w, mockStoreReturnNoMacaroon)
   182  	}))
   183  	defer mockServer.Close()
   184  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   185  
   186  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "")
   187  	c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned")
   188  	c.Assert(discharge, Equals, "")
   189  }
   190  
   191  func (s *authTestSuite) TestDischargeAuthCaveatError(c *C) {
   192  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   193  		w.WriteHeader(500)
   194  	}))
   195  	defer mockServer.Close()
   196  	store.UbuntuoneDischargeAPI = mockServer.URL + "/tokens/discharge"
   197  
   198  	discharge, err := store.DischargeAuthCaveat(&http.Client{}, "third-party-caveat", "foo@example.com", "passwd", "")
   199  	c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500")
   200  	c.Assert(discharge, Equals, "")
   201  }
   202  
   203  func (s *authTestSuite) TestRefreshDischargeMacaroon(c *C) {
   204  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   205  		io.WriteString(w, mockStoreReturnDischarge)
   206  	}))
   207  	defer mockServer.Close()
   208  	store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
   209  
   210  	discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon")
   211  	c.Assert(err, IsNil)
   212  	c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data")
   213  }
   214  
   215  func (s *authTestSuite) TestRefreshDischargeMacaroonInvalidLogin(c *C) {
   216  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   217  		w.WriteHeader(mockStoreInvalidLoginCode)
   218  		io.WriteString(w, mockStoreInvalidLogin)
   219  	}))
   220  	defer mockServer.Close()
   221  	store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
   222  
   223  	discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon")
   224  	c.Assert(err, Equals, store.ErrInvalidCredentials)
   225  	c.Assert(discharge, Equals, "")
   226  }
   227  
   228  func (s *authTestSuite) TestRefreshDischargeMacaroonMissingData(c *C) {
   229  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   230  		io.WriteString(w, mockStoreReturnNoMacaroon)
   231  	}))
   232  	defer mockServer.Close()
   233  	store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
   234  
   235  	discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon")
   236  	c.Assert(err, ErrorMatches, "cannot authenticate to snap store: empty macaroon returned")
   237  	c.Assert(discharge, Equals, "")
   238  }
   239  
   240  func (s *authTestSuite) TestRefreshDischargeMacaroonError(c *C) {
   241  	n := 0
   242  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   243  		data, err := ioutil.ReadAll(r.Body)
   244  		c.Assert(err, IsNil)
   245  		c.Assert(data, NotNil)
   246  		c.Assert(string(data), Equals, `{"discharge_macaroon":"soft-expired-serialized-discharge-macaroon"}`)
   247  		w.WriteHeader(500)
   248  		n++
   249  	}))
   250  	defer mockServer.Close()
   251  	store.UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh"
   252  
   253  	discharge, err := store.RefreshDischargeMacaroon(&http.Client{}, "soft-expired-serialized-discharge-macaroon")
   254  	c.Assert(err, ErrorMatches, "cannot authenticate to snap store: server returned status 500")
   255  	c.Assert(n, Equals, 5)
   256  	c.Assert(discharge, Equals, "")
   257  }
   258  
   259  func (s *authTestSuite) TestLoginCaveatIDReturnCaveatID(c *C) {
   260  	m, err := macaroon.New([]byte("secret"), "some-id", "location")
   261  	c.Check(err, IsNil)
   262  	err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", store.UbuntuoneLocation)
   263  	c.Check(err, IsNil)
   264  
   265  	caveat, err := store.LoginCaveatID(m)
   266  	c.Check(err, IsNil)
   267  	c.Check(caveat, Equals, "third-party-caveat")
   268  }
   269  
   270  func (s *authTestSuite) TestLoginCaveatIDMacaroonMissingCaveat(c *C) {
   271  	m, err := macaroon.New([]byte("secret"), "some-id", "location")
   272  	c.Check(err, IsNil)
   273  	err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", "other-location")
   274  	c.Check(err, IsNil)
   275  
   276  	caveat, err := store.LoginCaveatID(m)
   277  	c.Check(err, NotNil)
   278  	c.Check(caveat, Equals, "")
   279  }
   280  
   281  func (s *authTestSuite) TestRequestStoreDeviceNonce(c *C) {
   282  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   283  		io.WriteString(w, mockStoreReturnNonce)
   284  	}))
   285  	defer mockServer.Close()
   286  
   287  	deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces"
   288  	nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   289  	c.Assert(err, IsNil)
   290  	c.Assert(nonce, Equals, "the-nonce")
   291  }
   292  
   293  func (s *authTestSuite) TestRequestStoreDeviceNonceRetry500(c *C) {
   294  	n := 0
   295  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   296  		n++
   297  		if n < 4 {
   298  			w.WriteHeader(500)
   299  		} else {
   300  			io.WriteString(w, mockStoreReturnNonce)
   301  		}
   302  	}))
   303  	defer mockServer.Close()
   304  
   305  	deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces"
   306  	nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   307  	c.Assert(err, IsNil)
   308  	c.Assert(nonce, Equals, "the-nonce")
   309  	c.Assert(n, Equals, 4)
   310  }
   311  
   312  func (s *authTestSuite) TestRequestStoreDeviceNonce500(c *C) {
   313  	n := 0
   314  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   315  		n++
   316  		w.WriteHeader(500)
   317  	}))
   318  	defer mockServer.Close()
   319  
   320  	deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces"
   321  	_, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   322  	c.Assert(err, NotNil)
   323  	c.Assert(err, ErrorMatches, `cannot get nonce from store: store server returned status 500`)
   324  	c.Assert(n, Equals, 5)
   325  }
   326  
   327  func (s *authTestSuite) TestRequestStoreDeviceNonceFailureOnDNS(c *C) {
   328  	deviceNonceAPI := "http://nonexistingserver121321.com/api/v1/snaps/auth/nonces"
   329  	_, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   330  	c.Assert(err, NotNil)
   331  	c.Assert(err, ErrorMatches, `cannot get nonce from store.*`)
   332  }
   333  
   334  func (s *authTestSuite) TestRequestStoreDeviceNonceEmptyResponse(c *C) {
   335  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   336  		io.WriteString(w, mockStoreReturnNoNonce)
   337  	}))
   338  	defer mockServer.Close()
   339  
   340  	deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces"
   341  	nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   342  	c.Assert(err, ErrorMatches, "cannot get nonce from store: empty nonce returned")
   343  	c.Assert(nonce, Equals, "")
   344  }
   345  
   346  func (s *authTestSuite) TestRequestStoreDeviceNonceError(c *C) {
   347  	n := 0
   348  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   349  		w.WriteHeader(500)
   350  		n++
   351  	}))
   352  	defer mockServer.Close()
   353  
   354  	deviceNonceAPI := mockServer.URL + "/api/v1/snaps/auth/nonces"
   355  	nonce, err := store.RequestStoreDeviceNonce(&http.Client{}, deviceNonceAPI)
   356  	c.Assert(err, ErrorMatches, "cannot get nonce from store: store server returned status 500")
   357  	c.Assert(n, Equals, 5)
   358  	c.Assert(nonce, Equals, "")
   359  }
   360  
   361  type testDeviceSessionRequestParamsEncoder struct{}
   362  
   363  func (pe *testDeviceSessionRequestParamsEncoder) EncodedRequest() string {
   364  	return "session-request"
   365  }
   366  
   367  func (pe *testDeviceSessionRequestParamsEncoder) EncodedSerial() string {
   368  	return "serial-assertion"
   369  }
   370  
   371  func (pe *testDeviceSessionRequestParamsEncoder) EncodedModel() string {
   372  	return "model-assertion"
   373  }
   374  
   375  func (s *authTestSuite) TestRequestDeviceSession(c *C) {
   376  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   377  		jsonReq, err := ioutil.ReadAll(r.Body)
   378  		c.Assert(err, IsNil)
   379  		c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","model-assertion":"model-assertion","serial-assertion":"serial-assertion"}`)
   380  		c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
   381  
   382  		io.WriteString(w, mockStoreReturnMacaroon)
   383  	}))
   384  	defer mockServer.Close()
   385  
   386  	deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions"
   387  	macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "")
   388  	c.Assert(err, IsNil)
   389  	c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
   390  }
   391  
   392  func (s *authTestSuite) TestRequestDeviceSessionWithPreviousSession(c *C) {
   393  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   394  		jsonReq, err := ioutil.ReadAll(r.Body)
   395  		c.Assert(err, IsNil)
   396  		c.Check(string(jsonReq), Equals, `{"device-session-request":"session-request","model-assertion":"model-assertion","serial-assertion":"serial-assertion"}`)
   397  		c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="previous-session"`)
   398  
   399  		io.WriteString(w, mockStoreReturnMacaroon)
   400  	}))
   401  	defer mockServer.Close()
   402  
   403  	deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions"
   404  	macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "previous-session")
   405  	c.Assert(err, IsNil)
   406  	c.Assert(macaroon, Equals, "the-root-macaroon-serialized-data")
   407  }
   408  
   409  func (s *authTestSuite) TestRequestDeviceSessionMissingData(c *C) {
   410  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   411  		io.WriteString(w, mockStoreReturnNoMacaroon)
   412  	}))
   413  	defer mockServer.Close()
   414  
   415  	deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions"
   416  	macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "")
   417  	c.Assert(err, ErrorMatches, "cannot get device session from store: empty session returned")
   418  	c.Assert(macaroon, Equals, "")
   419  }
   420  
   421  func (s *authTestSuite) TestRequestDeviceSessionError(c *C) {
   422  	n := 0
   423  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   424  		w.WriteHeader(500)
   425  		w.Write([]byte("error body"))
   426  		n++
   427  	}))
   428  	defer mockServer.Close()
   429  
   430  	deviceSessionAPI := mockServer.URL + "/api/v1/snaps/auth/sessions"
   431  	macaroon, err := store.RequestDeviceSession(&http.Client{}, deviceSessionAPI, &testDeviceSessionRequestParamsEncoder{}, "")
   432  	c.Assert(err, ErrorMatches, `cannot get device session from store: store server returned status 500 and body "error body"`)
   433  	c.Assert(n, Equals, 5)
   434  	c.Assert(macaroon, Equals, "")
   435  }