README
¶
go-example-rego (github.com/antonio-alexander/go-example-rego)
The purpose of this repository is to show how you can integrate rego into a Go application. Although rego certainly has enough depth to exist on its own, Rego generally must be integrated somewhere to use it. As a result, there will be a strong focus on native Rego (e.g., testing using opa etc.), but there will also be a focus integrating into Go with some practical situations thrown in.
Once you've read through this repository (hopefully only once); you should have an opinion about the following:
- how to integrate Rego into a golang application
- the benefits of two-stage compilation/evaluation at scale
- how to optimize rego by reducing the number of iterations
- how to make rego work in the absence of information (i.e., performing authz with endpoints that have very few inputs)
Although these things are related, I may mention them briefly, but won't attempt to dive to deep into them as they're worthy of their own repositories:
- authentication vs authorization
- oauth2; claims
- scopes vs permissions
- JWT tokens
Bibliography
Here are some links that I used to put together this repository:
- https://www.openpolicyagent.org/docs/v0-compatibility
- https://github.com/open-policy-agent/vscode-opa/blob/main/README.md
- https://www.styra.com/blog/advanced-rego-testing-techniques/
- https://www.styra.com/blog/how-to-express-or-in-rego/
- https://github.com/antonio-alexander/go-bludgeon
- https://play.openpolicyagent.org/
- https://github.com/antonio-alexander/go-blog-benchmarking
Getting Started
The easiest way to get started with rego (even if you're not using VSCode) is to install the opa application from https://github.com/open-policy-agent/opa and the regal application from https://github.com/open-policy-agent/regal. Regal provides a language server (like gopls) that helps you with syntax etc, while opa provides you the ability to debug and evaluate rego files in realtime.
If you're using VSCode, you can also install this extension: https://marketplace.visualstudio.com/items?itemName=tsandall.opa
In the Makefile you can take advantage of the make test-opa and make test-go commands to execute the existing tests. Although they may be a little boring, it's a good starting point to see if you've installed everything correctly.
In addition to the tests for Go and Rego, I also have benchmarks for Go that attempt to demonstrate that with enough scale, it takes longer to compile.
Developing Rego
For the first maybe 10 hours of rego, the syntax may be pretty frustrating if not outright confusing. If you're programming in Go, some of the syntax may be similar, but do very different things. The V1 syntax versus the V0 syntax helps with this a lot; in that you have more alternate syntaxes that feel unique to Rego, but pretty much do the same thing. If you require complex data types, composition can be a pain to figure out initially, but with enough finagling, you can figure it out once and never have to do the work of figuring it out again.
Rego is very much a 'once you've figured it out, you don't have to figure it out again', you can copy+paste most solutions once you have something that works, so know that every solution you find, you only need to find it once (for your entire rego career)
If I were to dumb down the use case of Rego for this repository, it's to take relevant data and make a decision on whether it should be allowed or not. The normal use case is authorization decisions: whether a user can perform a given action (on a specific object).
Keep in mind that the example(s) I'm giving are simple and many business rules that could be present are not; the core idea doesn't change, but getting to your minimum viable product could take far more effort
Another way to view rego is "authorization as code"; you can compare it to file system permissions in Linux, that authorization exists as "code" in the operating system, but it's not exposed like Rego nor is there any ease (or reason) to change it. There are three Linux file system permissions: execute (1), read (2) and write (3); these permissions can be set for a user, a group or others (everyone else); also related, a given file (or directory) must be owned by a user and group.
This is a very static authorization model; the following should make sense:
- if a file is owned by group 'admin' and the group has read permission, a user belonging to group 'admin' can read that file
- if a file is owned by user 'root' and the user has read permission, user 'root' can read the file
- if a file is owned by user 'root' and the group 'admin' and each has read permission; a user who is neither root nor belonging to the group 'admin' cannot read the file
A relevant caveat is that group and user permissions are separate and you can have access taken away (or given) both ways
The above isn't comprehensive, but should give you a general feel for how authorization works: you define an object (e.g. a file); you define an owner (e.g. users, groups etc.) and you define permissions (e.g. read, write, execute). A user is allowed to perform the action associated with the permission if they are an owner and have been assigned that permission. The ownership isn't required, but it'll help tie your logic together; the absence of an owner will eventually cause problems; it's a longer conversation that I won't do justice here (but will in Authorization Model), but ownership helps with auditing and answering the question: "How do I give someone else access to do this thing?"
When developing rego, you'll do the following steps:
- define your objects, actions and business rules surrounding them
- define your authorization model determining how someone is authorized to perform a specific action on a given object
- determine how you'll integrate rego into an application and how it'll interact with external contracts
Business Rules; objects and actions
This isn't something you would do only for Rego, this is something that you'd do for any application, you'd do a subset of this for a database; understanding the lifetime of a given object from the womb to the tomb. Understanding how objects are created, how they are used and how they are disposed of helps you understand how it should (and can) be used.
This projects takes some inspiration from go-bludgeon: there are two objects we've defined, users and timers. A user is an approximation of an identity (authentication) while a timer is an object that can be owned by a user (it does more, but this doesn't mean anything from the perspective of rego).
Authorization Model
'Authorization Model' is a term I use to describe the logic/system behind determining whether a user is authorized to perform a given action on a given object (or objects).
Introspection is a term I use to describe a user being able to understand things about themselves. For example, the timer object I use in this repo can be owned by a user; if a timer is associated with a user, it doesn't make sense for that user to need to request access to read (or in some cases modify) that object. Introspection, like permission hierarchy, can reduce the amount of permissions you NEED to assign and can reduce the proliferation of assigned permissions.
One of the annoying things about anything permission/role related is that at scale (or over time); you have permissions proliferation and all of them matter. It becomes harder to know which permissions to remove or if a user needs less permission than required. One of the ways I've resolved this issue, is by creating a hierarchy of permissions such that permissions that are meant to do "more" have logic that implies (or infers) that they can do other operations that are less important; for example, if I have the ability to create a specific object, it makes sense (under most use cases) that I should also be able to read that object and that if I can create an object, I should also be able to update and delete it. Why should you have to explicitly give permission to read, update and delete, when a user can already create? Permissions hierarchy fixes that (although it creates some problems of its own, i.e., implicit vs explicit permissions and auditing). This is a basic hierarchy around the permissions, create, read, update and delete:
- create: implicitly read, update and delete
- read: no additional permissions are implied
- update: implicitly read
- delete: implicitly read (implicitly update if you support soft deletes)
I use explicit and implicit permission to determine what access a user has been given vs what a user has access to. The two methods I've mentioned so far to reduce permission proliferation (permissions hierarchy and introspection); involve implicit access. Even though they haven't been "granted" the access, they can still do the operations. Explicit permissions are easy to audit, while implicit permissions require you understand the business rules around introspection, permission hierarchy and anything else you add.
Finally, one of the things that's easy to overlook is that at runtime, you may not have enough information to make policy decisions. The information available during create, read, update and delete can be VERY different. For example, if you're reading (or searching) an object, you may only have the object id available or the name of the id; this information may not be enough information on its own to make a policy decision. If you want to know if a user can read a timer, they can read it if they have permission to read it (requires no information), but they can also read it via introspection (the timer is associated with them); there's no way to perform the policy evaluation regarding introspection unless you can look up the timer by its id and determine if the user making the request is the same user in the timer. Lookups add a layer of complexity (and scale) to your compiled rego, but are almost ALWAYS required if you take into account which endpoints you'll be evaluating and the information available.
Integration
One of the more confusing things (I think) about integrating rego is understanding the limitations of what's available for a given endpoint; for example, if you're reading a timer (in the context of this repository), the only two pieces of information you have are: (1) the timer id and (2) the user making the request. Although it may be confusing at first, these two pieces of information aren't enough to make a complete policy decision.
It's my opinion that the "data" for input, must be shared across all of your endpoint execution for consistency, but also that it should be static; it shouldn't be something that you constantly query from its source of truth (e.g., sql or redis). And this is not because its impossible, but because it adds to the bottom line of your round-trip-time; input should be something that you update safely (i.e., using a mutex) and asynchronously (such that you don't get inconsistent answers).
With only the timer id and the user making the request, you can only determine if the user has explicit or implicit access determined at compile time. It's not possible to determine if the user can interact with the timer because the user is referenced in the timer. In order to make those policy decisions, you need a way to lookup the user id associated with a timer using its timer id.
Permissions hierarchy for all the benefits it brings, also brings some slight complications (for better or worse). Although it's not super practical with a timer given that the associated user id can't change post-creation, one of the more interesting things is saying that a user can read, update or delete because they can create. In the case of an associated user id, you must ALSO be able to answer the question: "Is this a timer that the user could have created? If so, they should be able to read, update or delete it". Similar to the above example, there may not be enough information available at read, update or delete to make that policy decision: create would have the associated user id available, but subsequent reads, updates and deletes wouldn't so lookup would be necessary.
Optimizing Rego
When you first get your rego linted and working, you're probably amazed at how fast it is; you're probably not even worried about optimization. Its impossible to optimize rego without approaching scale: if you don't have enough data (enough of the right data); there's no way to know what to optimize. This isn't to say that you can't do it right the first time, just that it's dangerous to go blind.
All of the ways to optimize rego will generally stem from reducing the number of total iterations, you want to arrange your rules by filtering the items with the least number of items as early as possible; this reduces the total number of iterations.
One way to conceptualize rego is that a rule will filter over and over again until it gets one of something; filter all roles by ones with specific permissions and then filter by all the users in those roles. Generally, fewer iterations means that it'll finish faster. You can think of this as an analog for short-circuit evaluation. Sometimes this requires more intimate knowledge of the data you're filtering like the order of magnitude in comparison to each other
{
"roles": {
"timers_create": {
"permissions": [
"timer_create"
],
"userIds": [
"user_timer_create"
]
},
"timers_delete": {
"permissions": [
"timer_delete"
],
"userIds": [
"user_timer_delete"
]
},
"timers_read": {
"permissions": [
"timer_read"
],
"userIds": [
"user_timer_read"
]
},
"timers_update": {
"permissions": [
"timer_update"
],
"userIds": [
"user_timer_update"
]
},
"users_create": {
"permissions": [
"user_create"
],
"userIds": [
"user_user_create"
]
},
"users_delete": {
"permissions": [
"user_delete"
],
"userIds": [
"user_user_delete"
]
},
"users_read": {
"permissions": [
"user_read"
],
"userIds": [
"user_user_read"
]
},
"users_update": {
"permissions": [
"user_update"
],
"userIds": [
"user_user_update"
]
}
},
"tokenUserId": "user_user_create"
}
package policy.compilation_evaluation
default can_create_user := false
can_create_user if {
input.tokenUserId != ""
some role in input.roles
print(".")
"user_create" in role.permissions
print(".")
input.tokenUserId in role.userIds
print(".")
}
For example, if you execute this rego with the above input (it's a simplification of compilation_evaluation.rego) you'll see that it iterates 7 times (one less than the number of roles). This rego could be optimized in two ways:
- filtering by users before tokens
- filtering by permissions before tokens
- (not checking to see if the token is empty)
Each of the above solutions won't actually change the number of iterations unless the shape of the input data changes; for example if there are fewer roles, or roles with more users...or roles with more permissions. If through process (or circumstance); you can identify a specific shape of the data, you can optimize your rego to account for that: if there are fewer roles with fewer permissions, then filtering by permissions first should trend toward fewer iterations.
One of the strategies you'll see implemented is a separate compilation and evaluation versus a single (i.e. combined) rego; both of these strategies work and have varying degrees of benefit. This follows similar rules/circumstances of caches, if there's a subset of your data that doesn't change often, then why should you evaluate the entirety of the rule each time someone makes a request? The idea behind compilation + evaluation is that you can perform a subset of the evaluation in one place, the subset that doesn't change often, and then the subset that does change often (i.e., as a function of the request being made); you can make all of those decisions at run time.
Depending on how complicated your rules are, you may find that "pre-compiling" some of the rules, optimizes the amount of extra time that gets added to each request you handle in a significant way. For example, if compilation took you 10s, that would get added to your total request time (what's experienced by your users); by pre-compiling, or compiling asynchronously, they may only experience the time it takes to perform the evaluation.
github.com/open-policy-agent/opa supports prepared evaluations where you can create a prepared query/evaluation (like in a database) and even though the data namespace is fixed, you can change the input namespace without having to re-create the prepared query/evaluation. At scale, this has statistically significant improvement (see benchmarks).
You may also wonder if the way you organize data makes a big difference; and the answer is yes, but maybe not how you think. Data should be organized such that you don't have to iterate over multiple levels; so within a rule you should avoid having to unnecessarily iterate over nested objects (e.g., maps of arrays or arrays of maps of objects etc.). Rego DOES iterate through maps and arrays differently in terms of performance, iterating over an array is faster than iterating over a map, but if you know the key for a map, you can avoid iterating at all. So...depending on the structure of your data, you can take advantage of certain benefits.
{
"numbers_array": [
"1",
"2",
"3",
"4",
"5"
],
"numbers_map": {
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5"
}
}
package play
# 66.567us
test_rule_array if {
print("!")
some number in input.numbers_array
print(".")
number == "5"
}
# 40.00us
test_rule_map if {
print("?")
input.numbers_map["5"]
print(".")
}
#80us
test_rule_map_iterate if {
print("$")
some number in input.numbers_map
print(number)
print(".")
number == "5"
}
This example can be run in the playground to show three ways to iterate through a similar data set and how each is performant in comparison to each other. Here are some things we can take away from this:
- a map is most useful when performing lookups where the key is known (e.g., a timer id or a user id)
- an array is most useful when performing non-exact comparisons (e.g. time) or where the thing you're looking for in a dataset can't be reasonably represented with its key (e.g. permissions in a given role)
- a map is NOT useful if you're ever forced to iterate over it
You could use a combination of the two (e.g. keys in an array and a map with the data); but I think it harms readability more than it optimizes
Example Code
The example code is meant to describe what would be a reasonable implementation of Rego in the Go programming language. It's not complete in the sense that it doesn't include (important) things like authentication, oauth2, jwt and a webserver, but it's enough to understand the authorization portion of the integration.
I think it goes without saying, but this isn't production ready code (although I do think my code is pretty good); while you can probably copy+paste the ideas (and even the code) safely, please do your due diligence to ensure you don't create more problems for yourself.
You can look at the Getting Started section to understand how to interact with the Makefile and test/benchmark the code.
Objects
There are a handful of objects that exist within this repository; each has a specific function to allow us to perform authz for a given user (and function); they are as follows:
- Permission: this defines an action that can be taken on an object
- User: an identity of an entity making a request
- Role: this provides a way to associate permissions with users
- Timer: this is the timer from go-bludgeon; but truncated for the purposes of this repo along with additional functionality (i.e., time user id)
Although users, permissions and roles are vastly simplified (e.g., they don't take into account any multi-tenancy or co-existing applications); the timer is closest to how you'd represent an actual object.
The timer (in the context of go-bludgeon) is meant to be used to...track time with a focus on tracking work (e.g., if I wanted to track how long it took me to accomplish a task). A timer is started, then it may be paused and resumed a number of times and then finally stopped and completed. It's expected that a timer would be owned by the person doing the work, but...because this may be a business, timers would need to be administered by specific people who didn't create the timer; so its a perfect object to show how authz could work and put the ideas of introspection, permission hierarchy and lookups.
Web Integration
Although it's very out of date; this is an example of what the endpoints would look like for timers: https://antonio-alexander.github.io/go-bludgeon/?urls.primaryName=Timers; you can ignore time slices for the time being. There are four endpoints worthy of note:
- (POST) /timers: to create a timer
- (GET) /timers/search: to search for zero or more timers
- (GET) /timers/{timer_id}: to read a specific timer using its id
- (DELETE) /timers/{timer_id}: to delete a specific timer user its id
Keep in mind that the user id field is specific to this repo and not go-bludgeon
When you POST to create a timer, you'd have the ability to use a JSON body to include all of the information about the timer; you may provide (or omit) the userId, but in this case it's possible that you would provide it, so it could be used to make policy decisions.
Contrast with the GET or DELETE endpoints, which would most likely ONLY provide the timer_id; search is a little strange, but we'll talk about that last. It's unlikely to impossible that someone would provide the user_id associated with the timer when attempting to read the timer by id (or even search for it); additionally the application couldn't trust that input. This is the core reason for requiring a lookup to make policy decisions: that you need to perform the lookup to know what user id is associated with a given timer AND you need to trust it.
Searching through objects is a bit more complicated; in order to properly secure an endpoint that allows you to search through all of the objects, you can use either of the following solutions:
- if possible, determine if the user has explicit access to read all objects of a given type (this allows you to short circuit your policy decision)
- otherwise, you can iterate through all of the timers available in your lookup and determine if the user has access to read them
This isn't always the case, but we could optimize the second solution by filtering all timers by user ids equal to the token user id
If option 1 returns false (not allowed) and option 2 returns no timers, from the perspective of rego, that user has no access to read timers. It’s possible that eventually the lookups could be updated with new timers or they could be added to a role that gives them that access, but if both options return false or empty, they have no access.
One of the confusing things about this is that the database isn't necessarily aware of what the user has access to, so you have to communicate that to the database layer, one way is to inject the list of timers that they have access to (option 2) or if they have explicit access to read all timers, you can pass it through as-is.
An alternative to lookups, is to perform the database lookups in the request and handle the authz on the backend; while this isn't a bad solution, it means you hit the database no matter what, so when someone doesn't have access and makes many requests, you may have more load on the database than just evaluation in rego (also failed requests may take longer to return)
Benchmarks
I've added a few benchmarks (see: policy_test.go and fixture.go) to try to show how fast (or slow) rego is at scale. You should take away the following:
- without scale, there's not much difference between prepared evaluations vs non-prepared
- with scale, prepared evaluations are noticeably faster (and probably doesn't engage garbage collection..ever)
- when using the compilation + evaluation method, compilation takes far more time than evaluation
I've taken some effort to ensure the data set makes some sense; it's not perfect nor is it completely heterogenous, but it varies enough that the rego is forced to iterate in semi-random way.
If you don't know much about benchmarking, a good place to start is: https://github.com/antonio-alexander/go-blog-benchmarking
Compilation + Evaluation vs Compilation && Evaluation
A co-worker came up with this idea a long time ago, its possible that I may have figured it out on my own but I wouldn't dare take credit for it. There are three regos included in this repo:
Each of these is a different implementation that provides varying levels of flexibility; all influenced by optimizing-rego. Policy decisions are made with a given data set; it’s expected that the data set will change over time. First and foremost, we want our policy decisions to be correct and second, we want them to be as fast as possible. Correct is relatively straight-forward, if the rego is valid, and the data is valid, then the policy decisions are correct. Fast can be weird and very subjective, fast is going to depend on your data and just as many other variables.
Making policy decisions fast isn't just the time it takes to perform an evaluation, but also the following:
- how long does it take to perform an evaluation?
- how long does it take to perform a compilation?
- how long does it take for a user to realize their access after they've been granted it?
- how long does it take for a user to realize the absence of their access after its been taken away?
- how long does it take for your code to realize that data has changed (e.g., new roles or timers were added)?
Although the benchmarks answer the first two items, the remainder of the items aren't as easy to answer, because they have everything to do with the architecture around how you store, query and sense changes in the input data (whether its specifically roles and timers or the output of the compilation rego).
Separation of evaluation and compilation doesn't necessarily make your evaluations faster (but it can help), but it provides a flexibility that you otherwise may not have. The input needed for compilation isn't necessarily the same input needed for evaluation and vice versa; the data may not change at the same rate (or you may not want to poll it at the same rate), the flexibility you get in conditionally updating the data or re-compiling can affect the bottom three policy decisions.
If at some point, you reach the scale where "preparing" the data for evaluation is very time consuming, separating compilation vs evaluation will significantly impact the time it takes to make policy decisions at runtime
Frequently Asked Questions
- I'm integrating Rego into a Go application, should I perform the tests in Rego or in Go?
The short answer is that you should probably test in Go, yes you can create tests in Rego, but because you're not using Rego in and of itself, if there's an integration issue in Go, you may lack the appropriate code coverage to sense that error when developing
- Do I need to model the rego data types in Go? Can't I just use anonymous structs?
This is definitely NOT the Go way (using anonymous structs) and you'll find that they are a pain to interact with, but if your rego is simple enough, it's unlikely you'll see immediate benefit from the models, but once you go beyond true/false rego for evaluation, like lists or aggregate access, you'll find that modeling the data makes it infinitely easier to interact with
- Can't I just perform evaluation with the raw data? Why should I do two stages of evaluation, isn't that slower?
Yes and yes. I've described this in detail above, but I want to re-iterate that this is a conversation of scale and will vary depending on your rego; if you need to perform more iterations to arrive at an evaluation result, you'll see this knee sooner rather than later. Because this evaluation HAS to be done on a per endpoint basis, it'll be directly attached to your bottom line (what your user's experience) and at scale, this could be a big deal; especially when a subset of what you iterate over will be static more often than not
- Is there some point where I should be worried about memory usage?
Sure, normal rules apply here, you should observe memory usage and see if Go's garbage collector requires some assistance. I think you'd need to involve a MASSIVE amount of data to make this something to worry about; 10MB of JSON data is an insane amount of data. This is definitely a problem to be concerned about (eventually), but you're far more likely to encounter issues with evaluation taking a longer time before you're worried about memory usage
- How should you handle time based rules in the context of evaluation and compilation regos?
time based rules should always be done at runtime/evaluation and NOT within the compile.rego; if nothing changes, you shouldn't have to re-compile, by placing it in the evaluation rego, you can ensure this behavior...time should have NO meaning in your compile.rego
- How do you do logical OR with rego?
Generally, you solve this with multiple rules with the same name; DON'T try to do logical OR within a rule; it's not easy and it generally indicates you did something wrong somewhere else
- Is there a difference between an empty array and a null array?
Yes, there are very few situations where you need to be worried about this, but a null array is not present, while an empty array is present, but empty. An example situation is a function that takes an array as an input, you may want it to return false rather than undefined, so you need to "solve" that logic
- How to resolve the issue if I get an error that says, "complete rules must not produce multiple outputs"?
This can be infuriating, but it generally means that you have open-ended logic in your rule so that it can give you two different outputs; sometimes you can solve this by adding logic to indicate what inputs are expected to be empty or null, so only one rule evaluates as true
- When I do rego, I have a lot of linting errors/suggestions, how should I know which to ignore?
Focus on the linting errors/suggestions that affect readability or performance; and then if you can increase performance by ignoring a suggestion, do so
- Sometimes my rules return undefined instead of true or false, how can I resolve this?
You should start by ensuring you have a default rule, and then you should look at your rules to see if there's some inconsistencies, is it possible for certain inputs to be null/empty?
- Why do you separate the different access rules rather than defining only one rule?
Rules must evaluate to true, false or undefined; if one of the rules evaluates to undefined, it can make the entire structure undefined unexpectedly. Think of if you only gave one user explicit access to create timers, but no other permissions, if read, update or delete were undefined, it would also make create undefined
- Why do you define both the token user id AND the user id?
Because they're not the same, they come from two different sources and if you code in that assumption you could unknowingly create a bug; this has a lot to do with trust. Inherently you can trust the user from the token (since it's signed); but everything else is user supplied
- Why when I evaluate test rules, do they evaluate to undefined instead of false?
This is a bit of semantics, but it's not the rule that you're testing giving undefined as much as it is the test rule giving undefined because one of the steps (all of which are expected to evaluate to true) has evaluated to false
- How does having evaluation and compilation rego help with understanding how a user has access to perform a given action?
Although it's semantics, understanding if a user has access explicitly or implicitly, lets you know how to take it away (while being able to provide the least amount of permissions). If a user is given their access explicitly, you know to simply remove the permission while if a user has their access implicitly, you can modify the permissions they have or understand that it makes sense that if a user can update an object, they should ALSO be able to read it (unless being able to update without reading makes sense for your business rules)
- What happens if policy data changes, could evaluations be wrong?
Kinda, it's not true to say they're wrong, but better to say that without all the information you need, your policy data may not evaluate as expected; for example, you could have a valid role available to give a user explicit access to interact with a timer, but because the timer itself isn't available in the lookup yet (roles will most likely be updated separately from timers); you won't be able to make some implicit policy decisions. This should be minimal (1-2x the time it takes to evaluate the compile.rego)
- Do I need to store the lookup in rego, can't I store it somewhere else like redis?
Yes, you can use something like this: https://github.com/tibotix/opa-redis-plugin; there are probably other options as well; the only down-side is that this significantly complicates your solution, creates another point of failure and will reduce the performance of your evaluations
- Why use Rego when you could just use a database, wouldn't that be faster?
Yes and no. Ignoring the complexity of the query (or QUERIES) you'd need to implement permission hierarchy, introspection and implicit permissions, a [properly indexed] database should offer a consistent/flat time on all policy decisions; you're as fast as the query and its ALWAYS added to each request. Compile + Evaluation skews such that compilation may take some time, but evaluation should always be VERY fast and 100% in-memory (~10ms) meaning that the user experience isn't adversely affected by the evaluation (or query). While a 10ms database query isn't impossible, I think rego is the more elegant (and less time consuming) solution
- Why do you use the input namespace rather than the data namespace?
the data namespace is what holds the actual rego rules, so whenever you use this space you have to effectively re-create it. This is true for prepared evaluations which won't let you modify the data namespace dynamically; prepared evaluations are read-only for all intents and purposes, but if you use the input namespace, you can use the same rego, but...change its input
- What's the benefit of using a prepared evaluation?
I don't feel confident that I know enough to say this empirically, but there seems to be SIGNIFICANT performance enhancements (an order of magnitude) of using a prepared evaluation vs just an evaluation at scale
- Why do you separate the tokenUserId from the timerUserId in the evaluation input?
This is the basic problem when it comes to integration, Rego doesn't know what you don't tell it nor can it differentiate between the input from your application vs the input from the client making a call to your application. For situations where you have two of the same thing, it can be pretty dangerous, so for Rego, you must differentiate between the user id that comes from the [signed] token versus the userId from the timer itself; if we don't process them separately, you could create an api based vulnerability