Apistar-contrib: Extra batteries for API Star 0.4+ (Session and CSRF)


#1

Hey guys,

I have just published a package called apistar-contrib that has components like Sessions and CSRF tokens.

Let me know what you guys think, or if you have any suggestions/comments.

Thanks.


#2

Long story short:

  • Sessions: I would like to see a pure stateless cookie-based session than backend ones. More tornadoished first than djangoished/flasked . With the minimalist approach of apistar statelessness would be preferred IMHO. See here how Tornado Web has lived and holds a long life without server backed sessions.

  • CSRF: Again a little bit overengineered, why {% if show_csrf %} {{ csrf_token() }} {% endif %} delegating in every rendering to template engine if it’s crsf or not? Why not just {{ csrf_token() }} alone? CSRF protection in all frameworks usually is a global setting, meaning if it’s enabled, it’s for all unsecure methods called. Then just provide a decorator csrf_exempt to the views you want unprotected.

def csrf_exempt(fn):
     fn.csrf_exempt = True
     return fn

class EnforceCsrfHook(
         route: Route, 
         """
         [... the rest of params ...]
         """
):
         if getattr(route.handler, 'csrf_exempt', False):
                return  # bye bye secure CSRF
        #[... the rest of the stuff ...]
        request._csrf_hook = self
        # This will fail in the template when added to an unprotected form as won't be included, 
        # maybe it should just do nothing or raise template error, up to you
        utils.update_global_template_context(app, csrf_token=self.csrf_token_template_hook)  
        [...]     

@csrf_exempt
def handler_007():
      return "My name is Exempt, CSRF Exempt"
  • Last but no least, compat module is a little rudimentary with import - except clauses, maybe a better setup.py extras configuration would more proper.

  • In any case if you wanna honor ASyncApp for App.interface=="asgi" use aredis not redis, which is blocking threads in the loop.

If you accept PR I’d be happy to contribute.


#3

@danigosa Sure, I would love to accept PRs.

As for your notes:

  • Cookie based sessions are a good idea. I’ll add that to the TODO list.
  • CSRF tags: the {% if show_csrf %} is only for the demo so show an error when {{ csrf_token() }} is not included.
  • I was going to have the logic able to be called from both a hook or a decorator, but am currently waiting for a way to inject annotations into views. (Please use __annotations__ instead of inspect.signature). I think the @csrf_exempt decorator should work. I’ll add this to the list.
  • I think that aredis should have its own session store AsyncRedisSessionStore

Thanks for the feedback.


#4

Cookie based sessions seems to run smoothly with APIStar.

If you really need to require additional components, the only way I found was parsing the handler function with the ast module, adding parameters to it and then recompiling into a new function. There is some working related code here.


#5

Is there a Cookie component that has already processed the headers for them? I could not find one, but doing it with stdlib cookies is ridiculous.


#6
  • Cookie based is the oldest session protocol and the first session store you gotta think of, before installing servers for caching, give it priority, it’s going to be used more than the ones requiring a cache server IMHO, be thorough with security (http-only, secure, encrypt the cookie, path, domain, etc.)
  • {{ csrf_token() }} in then rest of frameworks they render nothing if nothing is to render, better approach
  • Simple decorators returning just the function or the more advanced solution proposed by @pslacerda , here an example of a very simple decorator with parameter that we use for an internal cache process:
def cache(ttl=None):
    def decorator(fn):
        fn.cache = ttl

        return fn

    return decorator

@cache(ttl=100)
def cached_handler():
      return "This is gonna be cached a 100 seconds"

#7

Nice explanation about cookie based sessions and example for cache.


#8

Just a tip on the cache decorator sample, if you decorating directly to a bound method as handler, remember it’s not like a function, you need to:

class A:
     def bound_handler():
          pass
a = A()
if cache_A:
    bound_route = Route("/", method="GET", handler=cache(ttl=60)(a.__class__.bound_handler)
else:
     bound_route = Route("/", method="GET", handler=a.bound_handler)

This is useful if you are building some kind of class based view with external logic (like external settings), alternatively the logical way works directly as expected:

class A:
     @cache(ttl=60)
     def bound_handler():
          pass
a = A()
bound_route = Route("/", method="GET", handler=a.bound_handler)