Talks

The Boring Bits Bite Back

The Boring Bits Bite Back

by Katie Miller

In her presentation titled 'The Boring Bits Bite Back' at Helvetic Ruby 2024, Katie Miller, a senior staff engineer at NFI, emphasizes the importance of laying a solid foundation for authorization in application development. The focus is on the often-overlooked aspects of 'boring' features like users, accounts, and billing, which, if neglected, can lead to tech debt and security vulnerabilities as applications scale. Miller clarifies the distinction between authentication and authorization in a system, describing authentication as 'the keys to the castle' while authorization determines who can access various areas within that castle.

Key points discussed in the presentation include:
- Understanding Authorization: Authorization can become convoluted as applications grow; developing a simple, predictable framework from the beginning is essential.
- Initial Setup Challenges: As the example app 'ptrax' evolves, the need for role-based access control becomes evident as different users interact with the app, such as pet owners and caretakers who require varying access levels.
- Policy Authorization Pattern: Introducing this pattern helps address complex permission requirements by focusing on whether a user can perform an action on a resource, making the system easier to manage.
- Growing Complexity: As more roles are added and requirements evolve, ensuring clarity and simplicity becomes challenging. The importance of refactoring to streamline authorization processes is highlighted.
- Avoiding Overcomplication: While policy patterns offer a structured approach, they can complicate customization. Using relational data structures and established libraries like 'cancan' can enhance flexibility in managing roles and permissions.
- CRUD Principles: Maintaining CRUD structures is essential to prevent a bloated codebase, ensuring that authorization mechanisms are straightforward and easy to follow.

Miller concludes by advising developers to prioritize simplicity, flexibility, and clear documentation in authorization systems. The overarching message is that while it’s tempting to innovate in authorization, focusing on its basic principles will mitigate future complications. This proactive approach enables teams to dedicate their creative energy to enhancing product features that genuinely delight users.

00:00:04.319 My name is Katie Miller. I'm a senior staff engineer at NFI, and I live in Berlin. I work remotely; it's a globally remote company. I'm here to talk to you about "The Boring Bits Bite Back."
00:00:20.760 Authorization is important. Often, it's referred to as 'auth,' which can be a little confusing because it actually involves two systems: authentication and authorization. These systems live under the same umbrella. Authentication is essentially the keys to the castle; it determines who can enter. On the other hand, authorization is about who can access the VIP areas or different parts within the castle.
00:01:05.239 Throughout my career, I've found myself drawn to fixing messy authorization systems. This can be tricky and isn't something most people expect to spend their careers working on; yet, it seems to be where I shine. I understand why: Who wants to discuss access to specific API endpoints? While this might excite security people, it's not exactly the flashy part of any SaaS product. In fact, I would argue that the subject is fairly boring, and it should be boring, predictable, and simple for everyone to use without overthinking it as they build features.
00:01:43.000 Unfortunately, authorization often isn't a feature that's marketed, so very few people give it the attention it deserves. Over time, as it gets ignored, it tends to become more complicated and less predictable. Even worse, neglected limitations can hinder company growth or, in the worst-case scenario, lead to security breaches. It's normal for the complexity of an app to grow over time. We've all experienced the shift from a nice, fresh greenfield app to working in legacy systems. Unfortunately, this complexity, especially in authorization, can create a real mess and make things very difficult to manage.
00:02:05.439 I want to emphasize that it doesn't have to be this way. If you think about authorization early on in your app development, there will be less pain later. So, let's build an app together as an example of how these issues can arise over time—not necessarily because anyone intends for them to happen, but because things naturally evolve as you move quickly and aim to get to market.
00:02:35.240 Our app will be called 'ptrax,' a place where people can manage and track their pet's care. Users will be able to share care instructions with other caretakers, avoiding the hassle of tracking everything on paper. We'll set up a new Rails app, which is my favorite way to create fast applications. Let's assume this crowd is also on board, and we'll fast-forward a bit.
00:03:06.080 We receive a feature request to allow multiple people to care for a pet because typically, it's not just one person responsible. Using Rails systems, we'll create an account, add users, and ensure that those users belong to an account. However, there's a complication: some caretakers, like a boarding facility or a dog walker, may not actually own the pet. We still want them to participate in caring for pets without being the official owners. This introduces an authorization feature: should all caretakers have the same abilities?
00:03:44.360 Some users might not want another caretaker to adjust their pet's medications or even change their pet’s name in the app. We need to ensure that we check roles throughout the application. We'll add an owner ID, so if the owner ID matches the user ID, they qualify as the owner. We’ll store other user IDs in an array and check against it to verify caretaker status. In our controllers, we can check if a user is the owner, allowing them to perform certain actions; otherwise, we'll render an unauthorized response. Quick and done—that was fast! Now, let's move forward again.
00:04:37.720 As we progress, we're going to add support for our admin users. We'll do this as simply as possible by checking the user's email. Since we control access to Google Suite, we know that only our users have their respective emails. We'll let admins see what users can access, but we don't want to create a new admin interface; instead, we'll allow them to make the same requests as any other user, ensuring they're either part of the account or designated as admins.
00:05:15.560 Continuing onward, we've grown quite a bit and are adding larger features, including vet office integration. This allows vet staff to manage pet health records, which necessitates new roles. We'll introduce a veterinarian role; if a user is an owner, they're the veterinarian, while veterinary assistants take on another specific role. As we continue to update our controllers, we need to ensure that we're verifying ownership or veterinarian status in our logic. Otherwise, access will be denied.
00:06:18.280 As we recap this journey, you will notice an increasing list of roles as we move forward. At this point, part of the feature requirements is that we should update pet medications at appropriate times. If an account is canceled, we shouldn't allow updates, although the account owner may still view certain information. We'll implement checks to determine whether an account is active before allowing updates. It’s important to note that our use of if-else statements is growing tedious and complex.
00:06:49.920 As growth continues, we see the charts thrive, but complexity increases significantly. Fortunately, we’ve gained some breathing room to tackle our tech debt. I’d like to introduce the concept of the policy authorization pattern. You may be familiar with this pattern, but essentially, we want to answer the question of whether a user can perform a specific action on a resource.
00:07:17.040 In code, this can appear quite simple to read. For example, can they update a pet’s medication? The reason we favor the policy pattern is due to its straightforward implementation without needing to install any additional gems. There are plenty of examples available to guide you, enabling us to take checks from our controllers and transfer them into easily manageable policies.
00:08:12.480 Here's a general example of a pet medication policy where we will place our role checks in designated methods. Additionally, I think we should clean up our code as we implement this change. For instance, we can check the account cancellation state here, issuing an unauthorized response when necessary. This approach will significantly streamline our controllers, leading to better organization.
00:08:49.920 As our application evolves, we notice that many vet offices fall under corporate management. Corporate entities want to oversee these operations, which means we need new roles. For instance, we’ll need roles for financial management and data analysis, including sales and potentially multi-tiered support systems. This will introduce different levels of access, but fortunately, we’ve refactored our earlier policies, allowing us to simply add these new roles to our policy action methods.
00:09:48.200 One day, you might receive a message from a product manager informing you about a significant deal in the works. They might mention that the head of security has specific suggestions for how to adjust current roles and permissions. You might think, 'No big deal; we already have RBAC—Role-Based Access Control.' However, when you receive a detailed list of changes, you realize that our roles are expanding exponentially.
00:10:35.960 They're also requesting modifications to existing roles, raising questions about whether our current capabilities align with customer needs. Users may want to define their own roles and set unique permissions based on their specific environments, leading to a demand for greater flexibility in our role management, as well as importantly, the ability to display these structures in an easily understandable format.
00:11:14.040 At this point, you'll notice that many of our existing roles might have convoluted ways of granting access, whether it’s through checking email or arrays of IDs. As our policies expand, they might also scrutinize the state of other resources, complicating the straightforwardness of our operations. Additionally, many new features added by team members come with their own unique policy methods, creating overlapping responsibilities and confusion.
00:12:15.960 Eventually, you might hear from the PM that it’s time for a rewrite, even after just having refactored the system. They might ask how long this would take, and while you might optimistically guess two to four months, you know the reality is that a major overhaul can extend over six months to a year—or longer.
00:12:56.480 This scenario is common among startups, where configurable authorization is often not requested until significant amounts of code have been written. While aiming for a minimal viable product, teams often overlook how the enterprise phase will demand flexible systems that adapt to changing requirements.
00:13:32.680 How could we have navigated this progression differently? To start with, we should have incorporated relational data structures to clarify how roles are assigned, avoiding redundant string checks that cause confusion. By implementing join tables for roles and permissions, we can centralize our access controls, enabling easy updates and retirements.
00:14:20.720 Let's talk about the policy authorization pattern again. While it seemed innovative at first, it's crucial to recognize that it complicates the customization process. Combining various concerns into our policy action methods dilutes their focus and makes it hard to locate permissions across the codebase.
00:14:53.640 Instead, we should consolidate our permissions per role and resource. I recommend exploring existing libraries and gems, looking for those that streamline the separation of roles and permissions. An example would be the 'cancan' gem, which allows you to define permissions more clearly, making it easier to manage authorization as your application escalates in complexity.
00:15:28.840 As a final proposal, everyone should stick to CRUD (Create, Read, Update, Delete) structures. By adhering to RESTful API design principles, we can prevent uncontrolled growth in the size of controllers and maintain clarity in resource hierarchy.
00:16:03.080 If you're managing permissions, have clear deliberations on how these structures will look. Moreover, consider designating ownership over certain elements to ensure a streamlined process. Clear documentation concerning new permissions is crucial to ensuring all team members are aligned.
00:16:52.880 In conclusion, the key points are: leverage relational data; avoid the pitfalls of overly complex policy authorization systems; maintain simplicity around CRUD principles; and focus on flexibility. Authorizing actions based on roles should answer, 'Can a user perform this action on a resource?' It's vital to minimize creativity in this area; focus your innovation on other domains of your application.
00:18:09.200 Thank you for your attention!