Filtering Datasets Based on OPA Policies - open-policy-agent

I have an access control model that is composed of two layers: Access Levels and Shared Permissions.
A user's Access Level governs both the maximum set of permissions that you can have in the system and it also grants you some basic permissions to create top level objects in the system (portfolios, programs, projects, etc..). Objects in the system can also be shared with you by someone else in the system (thus granting you one or more permissions specifically on an object). If an object was not created by you or has not been assigned to you, then you should be able to see it unless it's explicitly been shared with you. An example dataset would look something like this:
"access_levels": {
"Worker": ["projects.view"],
"Planner": ["projects.create", "projects.edit", "projects.view"]
},
"users_access_level": {
"bob.jones#example.com": "Planner",
"joe.humphreys#example.com": "Worker"
},
"resource_hierarchy": {
"customer1": ["customer1"],
"project1": ["project1", "customer1"],
"project2": ["project2", "customer1"]
},
"resource_policyIDs": {
"customer1": "1",
"project1": "2",
"project2": "3",
},
"policies": {
"1": {
"permissions": ["projects.create"],
"subjects": ["users:joe.humphreys#example.com"]
},
"2": {
"permissions": ["projects.view"],
"subjects": ["users:joe.humphreys#example.com"]
},
"3": {}
}
and a policy would looks something like this:
package authz
default authorized = false
authorized {
input.method == "POST"
http_path = ["programs", "create"]
input.customerId == token.payload.customerId
permitted_actions(token.payload.sub, input.customerId)[_] == "programs.create"
}
subjects_resource_permissions(sub, resource) = { perm |
resource_ancestors := data.resource_hierarchy[resource]
ancestor := resource_ancestors[_]
id := data.resource_policyIDs[ancestor]
data.policies[id]["subjects"][_] == sprintf("users:%v", [sub])
perm := data.policies[id]["permissions"][_]
}
permitted_actions(sub, resource) = x {
resource_permissions := subjects_resource_permissions(sub, resource)
access_permissions := data.access_levels[data.users_access_level[sub]]
perms := {p | p := access_permissions}
x = perms & resource_permissions
}
http_path := split(trim_left(input.path, "/"), "/")
Let's say I have created a Projects API to manage project resources. The API has a method to list projects, and the method should only return the projects the user has view access to. So in the example above, the user 'joe.humphreys#example.com' shouldn't be able to view Project 2 even though his access level gives him 'projects.view'. This is because it wasn't shared with him.
If I wanted to use OPA, how could I provide a general pattern to accomplish this across multiple APIs? What would the query look like to OPA to accomplish something like this? In other words, if I wanted to enforce this authorization in SQL what would that look like?
I've read this artcile, but I'm having a hard time seeing how it fits here..

I'm assuming your two-layer model ANDs the 'Access Level' with the 'Shared Permission'. E.g., "joe" can see "project1" because "joe" is a Worker therefore he has "projects.view" permission AND "joe" is assigned to "project1" (via policy "2") with permission "projects.view". Since "joe" is not assigned to "project2" via any policy with the "projects.view" permission, "joe" cannot see "project2". I.e., even if "joe" was assigned to "project2" via some policy, that policy must specify the "projects.view" permission otherwise the "joe" cannot see it.
You could write something like this to generate the set of project resources that the subject is allowed to see:
authorized_project[r] {
# for some projects resource 'r', if...
r := data.projects[_]
# subject has 'projects.view' access, and...
level := data.user_access_levels[input.sub]
"projects.view" == data.access_level_permissions[level][_]
# subject assigned to project resource (or any parents)
x := data.resource_hierarchy[r.id][_]
p := data.resource_policies[x]
"projects.view" == data.policies[p].permissions[_]
input.sub == data.policies[p].subjects[_]
}
This begs the question of how data.projects, data.policies, and data.resource_hierarchy get populated (I'm assuming the Access Level data sets are much smaller but there could also be the same question with those.) The blog post (which you linked) discusses answers to that question. Note, passing the data via input instead of data does not really change anything--it still needs to be available on every query.
You could refactor the example above and make it slightly more readable:
authorized_project[r] {
r := data.projects[_]
subject_access_level[[input.sub, "projects.view"]]
subject_shared_permission[[input.sub, "projects.view", r.id]]
}
subject_access_level[[sub, perm]] {
some sub
level := data.user_access_levels[sub]
perm := data.access_level_permissions[level][_]
}
subject_shared_permission[[sub, perm, resource]] {
some resource
x := data.resource_hierarchy[resource][_]
p := data.resource_policies[x]
perm := data.policies[p].permissions[_]
sub := data.policies[p].subjects[_]
}
You could generalize the above as follows:
authorized_resource[[r, kind]] {
r := data.resources[kind][_]
perm := sprintf("%v.view", [kind])
subject_access_level[[input.sub, perm]]
subject_shared_permission[[input.sub, perm, r.id]]
}
subject_access_level[[sub, perm]] {
some sub
level := data.user_access_levels[sub]
perm := data.access_level_permissions[level][_]
}
subject_shared_permission[[sub, perm, resource]] {
some resource
x := data.resource_hierarchy[resource][_]
p := data.resource_policies[x]
perm := data.policies[p].permissions[_]
sub := data.policies[p].subjects[_]
}

Related

Are Rego function calls memoized?

I have defined a function that is not idempotent; it can return different results for the same inputs. Does Rego memoize the result of the function on each query? In other words, given the following policy:
val := myFunc(...) # Returns an object with "a" and "b" fields.
foo {
val.a
}
bar {
val.b
}
Rules foo and bar would operate on the same val which resulted from a single call to myFunc. Does Rego guarantee this?
Except for http.send, I don't think there are any builtins that would allow you to return different data provided the same input. I'd be curious to find out though! :) To answer your question, a rule/function is cached in the scope of a single query, so multiple calls to the same rule/function won't be re-evaluated. The http.send builtin addditionally allows you to cache results across queries, which can be very useful when requesting data which is rarely updated.
Speaking of http.send, it's a pretty useful builtin to test things like this. Just fire up a local webserver, e.g. python3 -m http.server and then have a policy that queries that:
package policy
my_func(x) {
http.send({
"method": "GET",
"url": "http://localhost:8000",
})
}
boo := my_func("x")
foo := my_func("x")
Then evaluate the policy:
opa eval -f pretty -d policy.rego data.policy
{
"boo": true,
"foo": true
}
Checking the logs of the web server, you'll see that only one request was sent despite two rules calling the my_func function:
::1 - - [29/Jun/2022 19:27:01] "GET / HTTP/1.1" 200 -

Is there any way to store values in OPA

I have a usecase to do like, if a variable is already defined then return that value else invoke a rest endpoint to get the variable.
get_value = value {
data.value
value
}else = value {
value := <> #invoke rest
}
I will be running OPA as a server and my expectation is like, in the first invokation it will go to the else block then to the first block for rest of the calls. Could you please help me
You cannot write to the in-memory store from a policy, no. Except for that though, your code looks fine, and I'd say it's a pretty common pattern to first check for the existence of a value in data before fetching it via http.send. One thing to note though is that you may use caching on the http.send call, which would effectively do the same thing you're after:
value := http.send({
"url": "https://example.com",
"method": "get",
# Cached for one hour
"force_cache": true,
"force_cache_duration": 3600
})
If the server responds with caching headers, you won't even need to use force_cache but can simply say cache: true and the http.send client will cache according to what the server suggests.

How to retrieve Slack messages via API identified by permalink?

I'm trying to retrieve a list of Slack reminders, which works fine using Slack API's reminders.list method. However, reminders that are set using SlackBot (i.e. by asking Slackbot to remind me of a message) return the respective permalink of that message as text:
{
"ok": true,
"reminders": [
{
"id": "Rm012C299C1E",
"creator": "UV09YANLX",
"text": "https:\/\/team.slack.com\/archives\/DUNB811AM\/p1583441290000300",
"user": "UV09YANLX",
"recurring": false,
"time": 1586789303,
"complete_ts": 0
},
Instead of showing the permalink, I'd naturally like to show the message I wanted to be reminded of. However, I couldn't find any hints in the Slack API docs on how to retrieve a message identified by a permalink. The link is presumably generated by chat.getPermalink, but there seems to be no obvious chat.getMessageByPermalink or so.
I tried to interpet the path elements as channel and timestamp, but the timestamp (transformed from the example above: 1583441290.000300) doesn't seem to really match. At least I don't end up with the message I expected to retrieve when passing this as latest to conversations.history and limiting to 1.
After fiddling a while longer, here's how I finally managed in JS:
async function downloadSlackMsgByPermalink(permalink) {
const pathElements = permalink.substring(8).split('/');
const channel = pathElements[2];
var url;
if (permalink.includes('thread_ts')) {
// Threaded message, use conversations.replies endpoint
var ts = pathElements[3].substring(0, pathElements[3].indexOf('?'));
ts = ts.substring(0, ts.length-6) + '.' + ts.substring(ts.length-6);
var latest = pathElements[3].substring(pathElements[3].indexOf('thread_ts=')+10);
if (latest.indexOf('&') != -1) latest = latest.substring(0, latest.indexOf('&'));
url = `https://slack.com/api/conversations.replies?token=${encodeURIComponent(slackAccessToken)}&channel=${channel}&ts=${ts}&latest=${latest}&inclusive=true&limit=1`;
} else {
// Non-threaded message, use conversations.history endpoint
var latest = pathElements[3].substring(1);
if (latest.indexOf('?') != -1) latest = latest.substring(0, latest.indexOf('?'));
latest = latest.substring(0, latest.length-6) + '.' + latest.substring(latest.length-6);
url = `https://slack.com/api/conversations.history?token=${encodeURIComponent(slackAccessToken)}&channel=${channel}&latest=${latest}&inclusive=true&limit=1`;
}
const response = await fetch(url);
const result = await response.json();
if (result.ok === true) {
return result.messages[0];
}
}
It's not been tested to the latest extend, but first results look alright:
The trick with the conversations.history endpoint was to include the inclusive=true parameter
Messages might be threaded - the separate endpoint conversations.replies is required to fetch those
As the Slack API docs state: ts and thread_ts look like timestamps, but they aren't. Using them a bit like timestamps (i.e. cutting off some characters at the back and inserting a dot) seems to work, gladly, however.
Naturally, the slackAccessToken variable needs to be set beforehand
I'm aware the way to extract & transform the URL components in the code above might not the most elegant solution, but it proves the concept :-)

How to write a security rule to allow read without using auth variable

I am trying to write Security rules, but I am bit confused on writing it. For my case I am not authenticating the users using Firebase, but I have node in database which has child named by usernames. I am trying to achieve logic like this: for any child of this node if value is true then he can move on further or else not. Here is my sample node
"Customers":{
"John":"true",
"Jack":"false"
}
"Messages":{
"Message1":{
....
},
},
And here is my rules node where I am confused.I have tried using "$" wild card variable but getting error that variable is unknown.
"rules":{
"Messages":{
".read":"root.child('Customers').child($name).val()===true",
".write":"root.child('Customers').child($name).val()===true"
}
}
I think the "$" variable can't be used this way. So how should I do this?
How do you decide which user to check for value? You must have a value for comparison. If you want a logic like users can see their own messages, you should add an Username field under messages node. Like;
"Messages":{
"Message1":{
"John": {
},
....
},
}
And with this field you can do this;
"rules":{
"Messages":{
"$userId" : {
".read":"root.child('Customers').child($userId).val()===true",
".write":"root.child('Customers').child($userId).val()===true"
}
}
}

Symfony 1.4 Permissions and Credentials AND OR not working

Need to know if I'm missing something... I'm using sfGuardPlugin and trying to get a complex credential to work... and it's not even that complex. I just can't get either AND or OR to work.
"user_a" is set up to have permission "A" in both permissions and group "A" which also has permission "A" assigned to it.
I also have a Permission "B" and a group "B" set up in the same fashion as above... however, I did not assign user_a to these permissions. To be clear: user_a only has A permissions.
Now in security I have the following (where the user needs to either have credential A or B):
dashboard:
credentials: [[A, B]]
Now when I try to have user_a access the dashboard, it fails and redirects to the credentials required page. I tried the same thing with an AND statement and set up user_a with both, using:
dashboard:
credentials: [A, B]
...again, it failed.
Now, when I remove the brackets, and just use one credential, it all works perfectly. It's just when I use them in combination, in any form, that I run into problems.
Furthermore, I have checked if the user has a single credential, using:
echo $user->hasCredential('A');
And it responds as expected: True
But if I assign the user to both A and B and then try either:
echo $user->hasCredential(array('A', 'B'), false);
or
echo $user->hasCredential(array('A', 'B'));
It responds with False.
I'm stumped. What am I missing? I MUST have at least the [[OR]] working. Has anyone else experienced this? Is there a work-around?
EDIT: code snippet in myUser.class:
public function hasCredential($permission_name)
{
//this overrides the default action (hasCredential) and instead of checking
//the user's session, it now checks the database directly.
if (!$this->isAuthenticated()) {
return false;
}
$gu = $this->getGuardUser();
$groups = $gu->getGroups();
$permissions = $gu->getPermissions();
$permission_names = array();
foreach($permissions as $permission) {
$permission_names[] = $permission->getName();
}
foreach($groups as $group) {
$group_permissions = $group->getPermissions();
foreach($group_permissions as $group_permission) {
$permission_names = array_merge($permission_names, array($group_permission->getName()));
}
}
$permission_names = array_unique($permission_names);
return (in_array($permission_name, $permission_names)) ? true : false;
}
EDIT:
The above code snippet is indeed the problem. I tested it without the code snippet and it works as expected. So my next question, is how to tweak the snippet to accommodate instances with AND or OR? Suggestions?
I'm going to close this question, because I have found the problem and I will open a new question as a result of the issue I'm having with the code snippet (which becomes a new question).

Resources