The SALAD Principles of Policy-as-Code Development
Introduction
Having written a fair amount of production-focused Policy-as-Code implementations over the last few years, we’ve developed a set of principles for what the implementations should look like. Someone recently asked for the equivalent of the SOLID principles for PaC development, and this is our attempt to come up with an acronym to represent these principles.
Why not just use SOLID?
Policy-as-Code languages are not Object-Oriented. They do not provide inheritance, subclassing or interface contracts that are some of the key features of an object-oriented language. So most the SOLID principles don’t apply.
Policy-as-Code: The SALAD Principles
Single Responsibility
The reusable functions you write should be responsible for solving one specific problem. For example, these should each be separate functions wherever possible:
Compare two strings in a case-insensitive way
Create a string from a specified location in an input document
Accept an array of booleans, and return false if any of the boolean values are false
This is more-or-less a general principle of good programming, regardless of the context or language. It makes code easier to follow, easier to maintain, easier to test, and easier to reuse.
Note: If you are responsible for creating the policies themselves, they should also be single-topic, in the same fashion. However, in many cases, you are not in control of the policy itself, and the policy may have broad enforcement responsibilities. This is out of your control.
API Consistency
Your policies make decisions based on input documents. The schema of that input document is the API contract to which your clients (the Policy Execution Points) must adhere. To the best of your ability, you should make your API contract consistent across your policy implementations. For example:
If your strategy is to deny by default, always deny by default
If your strategy is to deny immediately when any part of a policy is violated, always deny immediately. Or if your strategy is to collect all violations in a particular policy, and return the entire set of violations to the caller, always do that.
If your strategy is to version your policies (to allow for graceful deprecation), you should version all of your policies
By having consistent APIs, your make it easier for your clients/consumers (typically application developers) to build out a set of templates & code to make interacting with the Policy Decision Points easier. In addition, you make it easier for code review.
Note: If your clients are automated tools, they may have contracts to which you must adhere, in which case this principle does not apply.
Legibility
Other than certain types of IaC enforcement policy, your policies are usually written by lawyers, business people and security personnel. To the best of your ability, you should produce Policy-as-Code logic that is easy for those non-developer types to read and understand. If they can’t understand the PaC implementation, they will not be able to verify that the implementation matches the human-language description.
This is especially important when the policy is a legal or regulatory mandate. Improper implementation of policy in these domains is potentially catastrophic.
Note: policy languages have some “exotic” syntax which is very difficult for a layperson to understand (Comprehensions in Rego, for example). In those situations, use a combination of well-named functions and thoughtful comments to reduce confusion.
Autarky (self-sufficiency)
To the best of your ability, your policy decision point should be self-sufficient. It should not have to call out to an LDAP server or a database or a RESTFul endpoint. When you add external dependencies, the policy implementation will be
slower
much more difficult to test in isolation
more susceptible to network failures
require a more complex network security architecture
Note: sometimes external calls are unavoidable, either because the policy system can’t contain what it needs, or because the data it needs changes too frequently for effective caching.
Disciplined Implementation
In addition to API consistency, the policy and functional logic should be implemented in a consistent way as well. For example:
Your function arguments should follow a consistent pattern
Your function and policy comments should follow a metadata format
Your logging/debug messages should be implemented in a consistent way
In some of the most formal implementations, all of the arguments to a function are bundled into a structure, and all returns from functions are structures as well (for example: including the success/failure of the call, the response value(s) and any error messages).
Note: Often, while improving robustness via formal argument & return structures, this makes it harder for a layperson to follow (i.e. the code is less legible). There’s no perfect answer here. Where possible, err on the side of legibility, but this is a judgement call.
Where These Principles do not Apply
The principles described in this article are primarily focused on more complex policy implementations that require development by hand. Not all policy needs that level of attention:
If you’re using GUI-based tools to develop your policies, the principles discussed here may be difficult or impossible to follow.
Some frameworks/platforms are built around the need to reach out to third-party systems to make decisions.
Some frameworks/platforms that are focused on Access Control do not support functions or any sort of reusability.
Some frameworks/platforms implement their own API contracts, in which case you must adhere to their rules.
Conclusion
We believe that following these principles will lead to policy implementations that are easier to implement, easier to use, easier to troubleshoot, and more likely to properly enforce the human-language policy description. What do you think? Contact us at: info@paclabs.io