PropelAuth Python v4 Release

PropelAuth Python v4 Release

Today, we are excited to release a new version of our base Python library, as well as releases of our framework-specific libraries for FastAPI, Flask, and Django Rest Framework.

Let’s jump in to some of the larger changes!

Better Typing Support (breaking change)

If you’ve used our Python libraries before, the type hinting left a lot to be desired. In our latest release, we now have type hints for all requests as well as datatypes for all responses.

NOTE: This will break specifically if you were previously unpacking (using the ** operator) on responses. Responses were previously dicts and are now explicit datatypes.

We have implemented commonly used functions like a key lookup (response["user_id"] will still work, but response.user_id is now preferred). We typically try to avoid breaking changes (this is our second in 3 years), but this felt like a pretty narrow problem.

User class improvements

For simpler permissions checking, you can now call functions directly on the User object like:

  • user.has_permission_in_org(orgId, 'can_export_reports')
  • user.is_role(orgId, 'Admin')
  • user.get_active_org().has_permission('api_key::write')

These allow you to pass around the User object instead of needing to refer back to the Auth object, and it also allows for easier mocking/testing.

New APIs

This isn’t specific to our Python library, but we’ve released a lot of new APIs like:

See the full list in our reference docs here.

Example - Easy feature gating by pricing plan

At PropelAuth, we’ve been fortunate to have a front-row seat to seeing many B2B SaaS companies grow. Auth providers are most important at critical moments in a company's history (initial launch, onboarding your first customer, closing your first enterprise customer, etc.). The most important thing we can do as you grow is to get out of the way.

That’s why we’re really happy with this FastAPI route:

@app.post("/api/expensive-action")
async def do_expensive_action(user: User = Depends(auth.require_user)):
    org = user.get_active_org()
    
    if org == None or \
       not org.user_has_permission("can_do_expensive_action"):
        raise HTTPException(status_code=403, detail="Forbidden")
        
    return do_expensive_action_inner(user, org)

At first glance, this seems like a pretty simple route, but it has a few important pieces:

  • The dependency injected User works with any type of authenticated user - whether it’s password, SSO, SAML, etc.
  • With role mappings, we can make it so an Admin of an org on our Free plan can not do the expensive feature, but an Admin of an org on our Paid plans can do the expensive action.
  • We can enforce that programmatically by handling a webhook from our payment provider and setting their role mapping, like so:
@app.post("/stripe/webhook")
def stripe_webhook(request):
    event = parse_and_validate_stripe_webhook(request)
    org_id = event["org_id"]
    
    if event["type"] == Events.CUSTOMER_SUBSCRIBED:
        # This can give the users in this organization access to increased
        # permissions, additional roles, and access to more features
        auth.subscribe_org_to_role_mapping(org_id, "Paid Plan")
        
    elif event["type"] == Events.CUSTOMER_UNSUBSCRIBED:
        auth.subscribe_org_to_role_mapping(org_id, "Free Plan")
    
    # ...
  • We can even give them self-serve access to advanced features like SAML and SCIM by programmatically generating a SAML connection URL, which will guide your user through setting up those features - with specific instructions for each identity provider (Okta, Azure AD, ADFS, etc.).

And the best part? That same code snippet above will continue to work. Even as our customer’s requirements get more complicated, your code won’t.

Questions? Feedback?

We're always looking to improve our libraries and services based on your feedback. If you have any questions about this release or suggestions for future improvements, please don't hesitate to reach out.