Cloud-native / microservice-based products are complex, and so is building access-control and managing permissions for these products - it’s only getting worse by the pull request. Most developers end up building authorization or access-control for their products multiple times - forced to refactor with new customer, product, or security demands coming in. To make our lives a little easier, let's go over the unique challenges that building cloud-native permissions poses before us, and cover the five best-practices for building them that can save you a lot of hassle.
Things have changed
We used to build authorization by using monolithic frameworks like Django or Spring that came with authorization or access-control baked-in, but these are no longer applicable when we create applications in the cloud-native space. There are a few reasons for that - Firstly, applications themselves are no longer monoliths - they're based on microservices and are becoming highly distributed. This becomes even more critical when you need to incorporate devices or instances that are deployed at the edge, which often need access-control too. Second, cloud-native applications tend to require the integration of third-party services (Such as billing, authentication, databases, analytics, etc.) and the ability to control access to them in addition to your own application's microservices. Third, more dynamic and distributed applications require us to use a bunch of different authorization models (e.g. RBAC, ReBAC, ABAC), that are based on multiple data sources, and increasingly complex rules 📈. Lastly, security, privacy, and compliance demands are also rising (in the face of increasingly complex cyber threats ☠️) and becoming really complex. We find ourselves not only managing who should access the data but also how it is propagated between different services.
The reality of modern authorization
All of these new needs require us to adopt a different mindset when thinking about authorization:
- Authorization can no longer come as an afterthought - we have to plan it in advance.
- Authorization is an ongoing endeavor - not something you solve once. It’s got to keep evolving along with your product.
- Authorization is key in a customer’s experience - as it affects how users connect and invite others to the product. They won’t like it if it’s bad.
- Authorization is connected to the bigger space of IAM
The 5 best practices
To handle all of these changes, we want to share five of the best practices that will aid you in building cloud-native permissions - and leave you time to actually develop features, rather instead of just running around permissions all day 👍
Decoupling policy and code:
One of the most important and recently popular practices in building cloud-native permissions is decoupling of policy and code. Having the code of the authorization layer mixed in with the application code itself can be very problematic. Most importantly, it creates a situation where we struggle to upgrade, add capabilities and monitor the code overall as it is replicated between different microservices. Each change would require us to refactor large areas of code that only drift further from one another as these microservices develop. This can be avoided by decoupling our policy from our code by (ideally) creating a separate microservice for authorization, that will be used by the other services in order to fulfill their authorization needs. Open-source policy/permissions engines such as OpenPolicyAgent (OPA) or SpiceDB for example allow us to manage authorization in a separate service.
We want the application that we are building to be dynamic. Applications often utilize abilities such as user invites, role assignment, or the usage of 3rd party data sources - all of which require to be managed in a real-time fashion. Without it, our ability to make authorization decisions will be significantly reduced. This requires us to design our authorization layer to be event-driven. We want to create a reality where every time an event that affects authorization happens, it is immediately put through the system to make sure the authorization layer learns about it and remains in sync with the application and any relevant 3rd party data service. Ideally, to achieve this we’d decouple the authorization data from the application data (as not all the data that is relevant for the application is relevant for authorization and vice versa), creating a lean model in our authorization layer and then keeping it in sync with our application and additional sources through real-time events. OPAL (Open Policy Administration Layer), for example, is an open-source project that enables making OPA event-driven. This allows you to respond to policy and data changes, push live updates to your agents, and bring open policy up to the speed needed by live applications.
Backoffice for stakeholders:
The authorization layer is part of the product itself, and in product-focused companies, there are various stakeholders that need to be able to connect to the access-control experience. These include (alongside developers) dev-ops, product managers, security, compliance, sales, marketing, etc. While building the authorization layer we want to provide controls and interfaces to these various stakeholders through a back-office. This requires us to consider what different stakeholders would need from the access-control interface to our product from day one - that should keep everyone happy.
Interfaces for customers:
In a similar fashion to the stakeholder’s requirements, we also need to think of our end-users/customers. Authorization is not only relevant in managing the product, but also for the product's end-user. If, for example, users require access to their own audit logs (Something almost every B2B application user would ask) they should be able to see what they have done within the product with ease. Recognizing this need in advance calls for building the authorization layer in a way that enables it to latch on to different interfaces that cater to the needs of the end-users. The list of possible interfaces here is extremely long (and you can probably name a dozen you've seen in multiple products). Permit.io, which empowers developers to bake in permissions and access control into any product, provides many of these interfaces.
So we've created a separate microservice to manage permissions, and we are able to deliver updates to it in an event-driven fashion. Now, how do we get around to managing those changes, applying versions, applying various checks and balances, and making sure that the code and data for the microservices comply with our demands and requirements? The answer is GitOps. Using GitOps allows us to create a pull request for every version change. Then, as our developers are updating the product and its access control, they can push a new commit with new code, have those go through the necessary tests and checks, and apply them to the authorization layer.
The future of permissions
With complexity on the rise and a constant stream of customer and security demands, building your product’s access control in a way that will be ready for the future, and won’t require heavy refactors or rewrites is critical. Creating a separate microservice for authorization, designing it to be event-driven, providing controls and interfaces to various stakeholders and customers, and using GitOps allows us to create a product that is as ‘Future proof’ as possible for authorization, and prevents us from having to rebuild our authorization layer over and over (and over) again - no matter the requirements.
Want to learn more? Check out this video with Permit.io’s CEO, Or Weis, talking about building access control and permissions for cloud-native products at the CloudNative London Meetup.