Repository: gratipay/gratipay.com

Issue:
KeyError: 'Vary'
https://app.getsentry.com/gittip/gittip/group/17914289/

```
Stacktrace (most recent call last):

  File "site-packages/algorithm.py", line 288, in run
    new_state = function(**deps.as_kwargs)
  File "gittip/utils/cache_static.py", line 107, in outbound
    response.headers.pop('Vary')
  File "aspen/http/mapping.py", line 121, in pop
    return Mapping.pop(self, name.title())
  File "aspen/http/mapping.py", line 49, in pop
    dict.pop(self, name) # KeyError
```

You get this error when visiting http://gittip.com/assets/widgets/0002.js

This was likely caused by #1969, though I'm not sure how.



Discussion:
Don't know if this helps, but you can pass a second argument to pop which is a default value if `'Vary'` isn't found.


Resolving patch:
diff --git a/gittip/security/authentication.py b/gittip/security/authentication.py
--- a/gittip/security/authentication.py
+++ b/gittip/security/authentication.py
@@ -8,27 +8,14 @@
 from gittip.security import csrf
 from gittip.security.user import User
 
-
 BEGINNING_OF_EPOCH = rfc822.formatdate(0)
-TIMEOUT = 60 * 60 * 24 * 7 # one week
-ROLES = ['anonymous', 'authenticated', 'owner', 'admin']
-ROLES_SHOULD_BE = "It should be one of: {}.".format(', '.join(ROLES))
-
-
-class NoMinimumRoleSpecified(Exception):
-    def __str__(self):
-        return "There is no minimum_role specified in the simplate at {}. {}" \
-               .format(self.args[0], ROLES_SHOULD_BE)
-
-class BadMinimumRole(Exception):
-    def __str__(self):
-        return "The minimum_role specific in {} is bad: {}. {}" \
-               .format(self.args[0], self.args[1], ROLES_SHOULD_BE)
-
+TIMEOUT = 60 * 60 * 24 * 7
 
 def inbound(request):
     """Authenticate from a cookie or an API key in basic auth.
     """
+    if request.line.uri.startswith('/assets/'): return
+
     user = None
     if 'Authorization' in request.headers:
         header = request.headers['authorization']
@@ -48,63 +35,26 @@ def inbound(request):
         token = request.headers.cookie['session'].value
         user = User.from_session_token(token)
 
-    if user is None:
-        user = User()
-    request.context['user'] = user
+    request.context['user'] = user or User()
 
+def outbound(request, response):
+    if request.line.uri.startswith('/assets/'): return
 
-def check_role(request):
-    """Given a request object, possibly raise Response(403).
-    """
-
-    # XXX We can't use this yet because we don't have an inbound Aspen hook
-    # that fires after the first page of the simplate is exec'd.
-
-    context = request.context
-    path = request.line.uri.path
-
-    if 'minimum_role' not in context:
-        raise NoMinimumRoleSpecified(request.fs)
-
-    minimum_role = context['minimum_role']
-    if minimum_role not in ROLES:
-        raise BadMinimumRole(request.fs, minimum_role)
-
-    user = context['user']
-    highest_role = user.get_highest_role(path.get('username', None))
-    if ROLES.index(highest_role) < ROLES.index(minimum_role):
-        request.redirect('..')
-
+    response.headers['Expires'] = BEGINNING_OF_EPOCH # don't cache
 
-def outbound(request, response):
-    if 'user' in request.context:
-        user = request.context['user']
-        if not isinstance(user, User):
-            raise Response(400, "If you define 'user' in a simplate it has to "
-                                "be a User instance.")
-    else:
-        user = User()
+    user = request.context.get('user') or User()
+    if not isinstance(user, User):
+        raise Response(500, "If you define 'user' in a simplate it has to "
+                            "be a User instance.")
 
-    if user.ANON: # user is anonymous
-        if 'session' not in request.headers.cookie:
-            # no cookie in the request, don't set one on response
-            return
-        else:
-            # expired cookie in the request, instruct browser to delete it
-            response.headers.cookie['session'] = ''
-            expires = 0
-    else: # user is authenticated
-        response.headers['Expires'] = BEGINNING_OF_EPOCH # don't cache
+    if not user.ANON:
         response.headers.cookie['session'] = user.participant.session_token
         expires = time.time() + TIMEOUT
         user.keep_signed_in_until(expires)
 
-    cookie = response.headers.cookie['session']
-    # I am not setting domain, because it is supposed to default to what we
-    # want: the domain of the object requested.
-    #cookie['domain']
-    cookie['path'] = '/'
-    cookie['expires'] = rfc822.formatdate(expires)
-    cookie['httponly'] = "Yes, please."
-    if gittip.canonical_scheme == 'https':
-        cookie['secure'] = "Yes, please."
+        cookie = response.headers.cookie['session']
+        cookie['path'] = '/'
+        cookie['expires'] = rfc822.formatdate(expires)
+        cookie['httponly'] = 'Yes, please.'
+        if gittip.canonical_scheme == 'https':
+            cookie['secure'] = 'Yes, please.'
diff --git a/gittip/security/csrf.py b/gittip/security/csrf.py
--- a/gittip/security/csrf.py
+++ b/gittip/security/csrf.py
@@ -54,7 +54,7 @@ def same_origin(url1, url2):
 REASON_BAD_TOKEN = "CSRF token missing or incorrect."
 
 TOKEN_LENGTH = 32
-TIMEOUT = 60 * 60 * 24 * 7 * 52
+TIMEOUT = 60 * 60 * 24 * 7
 
 
 def _get_new_csrf_key():
@@ -86,18 +86,16 @@ def _get_host(request):
 def inbound(request):
     """Given a Request object, reject it if it's a forgery.
     """
+    if request.line.uri.startswith('/assets/'): return
 
     try:
         csrf_token = request.headers.cookie.get('csrf_token')
         csrf_token = '' if csrf_token is None else csrf_token.value
         csrf_token = _sanitize_token(csrf_token)
-        # Use same token next time
-        request.context['csrf_token'] = csrf_token
     except KeyError:
-        csrf_token = None
-        # Generate token and store it in the request, so it's
-        # available to the view.
-        request.context['csrf_token'] = _get_new_csrf_key()
+        csrf_token = _get_new_csrf_key()
+
+    request.context['csrf_token'] = csrf_token
 
     # Assume that anything not defined as 'safe' by RC2616 needs protection
     if request.line.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
@@ -122,7 +120,6 @@ def inbound(request):
             if referer is None:
                 raise Response(403, REASON_NO_REFERER)
 
-            # Note that get_host() includes the port.
             good_referer = 'https://%s/' % _get_host(request)
             if not same_origin(referer, good_referer):
                 reason = REASON_BAD_REFERER % (referer, good_referer)
@@ -130,9 +127,6 @@ def inbound(request):
                 raise Response(403, reason)
 
         if csrf_token is None:
-            # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
-            # and in this way we can avoid all CSRF attacks, including login
-            # CSRF.
             raise Response(403, REASON_NO_CSRF_COOKIE)
 
         # Check non-cookie token for match.
@@ -150,28 +144,14 @@ def inbound(request):
 
 
 def outbound(request, response):
-
+    """Store the latest CSRF token as a cookie.
+    """
     csrf_token = request.context.get('csrf_token')
-
-
-    # If csrf_token is unset, then inbound was never called, probaby because
-    # another inbound hook short-circuited.
-
-    if csrf_token is None:
-        return response
-
-
-    # Set the CSRF cookie even if it's already set, so we renew
-    # the expiry timer.
-
-    response.headers.cookie['csrf_token'] = csrf_token
-    cookie = response.headers.cookie['csrf_token']
-    # I am not setting domain, because it is supposed to default to what we
-    # want: the domain of the object requested.
-    #cookie['domain']
-    cookie['path'] = '/'
-    cookie['expires'] = rfc822.formatdate(time.time() + TIMEOUT)
-    #cookie['httponly'] = "Yes, please."  Want js access for this.
-
-    # Content varies with the CSRF cookie, so set the Vary header.
-    patch_vary_headers(response, ('Cookie',))
+    if csrf_token:
+        response.headers.cookie['csrf_token'] = csrf_token
+        cookie = response.headers.cookie['csrf_token']
+        cookie['path'] = '/'
+        cookie['expires'] = rfc822.formatdate(time.time() + TIMEOUT)
+
+        # Content varies with the CSRF cookie, so set the Vary header.
+        patch_vary_headers(response, ('Cookie',))
diff --git a/gittip/utils/cache_static.py b/gittip/utils/cache_static.py
--- a/gittip/utils/cache_static.py
+++ b/gittip/utils/cache_static.py
@@ -103,9 +103,6 @@ def outbound(request, response, website):
     if not uri.startswith('/assets/'):
         return response
 
-    response.headers.cookie.clear()
-    response.headers.pop('Vary')
-
     if response.code != 200:
         return response
 


Test patch:
diff --git a/tests/py/test_hooks.py b/tests/py/test_hooks.py
--- a/tests/py/test_hooks.py
+++ b/tests/py/test_hooks.py
@@ -49,12 +49,12 @@ def test_session_cookie_set_in_auth_response(self):
         assert normal.headers.cookie['session'].value == alice.session_token
 
 
-    def test_session_cookie_is_sent_for_http_as_well(self):
+    def test_session_cookie_isnt_overwritten_by_canonizer(self):
         # https://github.com/gittip/www.gittip.com/issues/940
 
         self.make_participant('alice')
 
-        # Now make a request that canonizer will redirect.
+        # Make a request that canonizer will redirect.
         redirect = self.client.GET( "/"
                                   , auth_as='alice'
                                   , HTTP_X_FORWARDED_PROTO='http'
@@ -62,17 +62,11 @@ def test_session_cookie_is_sent_for_http_as_well(self):
                                   , raise_immediately=False
                                    )
         assert redirect.code == 302
-        assert redirect.headers.cookie['session'].value == ""
+        assert 'session' not in redirect.headers.cookie
 
         # This is bad, because it means that the user will be signed out of
         # https://www.gittip.com/ if they make a request for
-        # http://www.gittip.com/. They might do this themselves accidentally,
-        # but more likely a browser plugin (such as DoNotTrack) will do it for
-        # them. The way we fix this is to set "secure" on the session cookie,
-        # so that the browser won't send the session cookie to the server in
-        # the case of http://www.gittipcom/. Without a session cookie in the
-        # request, gittip.security.authentication.outbound won't set one on the
-        # way out.
+        # http://www.gittip.com/.
 
 
     def test_session_cookie_is_secure_if_it_should_be(self):