1

I am creating a policy to validate access to a collection of Records. These records are passed as input and have a collection of permissions attached to them. I validate them against permissions data stored in the OPA.

For instance, I can return the collection of Records that are accessible by doing something like this

isAllowed[id] {
   permissionSet := {x | x := permissions.groups[_].name}
   id := input.records[i].id
   input.operation == "update"
   input.records[i].acls.owners[j]==permissionSet[k]
}{
   id := input.records[i].id
   input.operation == "create" }

Which would return something like

"isAllowed": ["123"]

when the input is like the following and the 'permissions' data included "service.legal.user"

  "input": { 
        "operation": "update", 
        "records": [
            { "id": "123", "acls": { "owners": ["service.legal.user"] }},
            { "id": "456", "acls": { "owners": ["service.storage.viewer"] }}
        ]
   }

However I want to return something like the following where I list all input records and assign error messages to ones that have failed with all the reasons it failed

"records":[
   {"id": "123", "errors": ""}, 
   {"id": "456", "errors": "You must have owner permission to update a record"}
]

I have tried an incremental rule but I get the error message from OPA 'complete rules must not produce multiple outputs'

isAllowed = response {
    #owner permission checked for update operation on all records
    some i
    response := {
      "id" : input.records[i].id,
      "errors" : CheckErrors
    }
}
CheckErrors[reason] {
    reason := "Must be an owner to update a record"
    input.operation == "update"
    permissionSet := {x | x := permissions.groups[_].name}
    input.records[i].acls.owners[j]==permissionSet[k]
}
CheckErrors[reason]{
    #no permission checked for create operation on all records
    reason := "Anyone can create"
    input.operation == "create"
}

Any help would be welcome.

1 Answers1

0

Not sure I followed entirely, and you didn't provide the permissions object, but assuming groups is just a list of objects like {"name": "service.legal.user"} something like the below would produce your desired output.

records[response] {    
    id := input.records[_].id
    errors := [r | e := check_errors[_]
                   e.id == id
                   r := e.reason]
    
    response := {
        "id" : id,
        "errors" : errors
    }
}

check_errors[{"id": id, "reason": "Must be an owner to update a record"}] {
    input.operation == "update"
    id := input.records[x].id
    permissionSet := {x | x := permissions.groups[_].name}
    owners := {o | o := input.records[x].acls.owners[_]}
    
    count(owners & permissionSet) == 0
}

Full example here.

Devoops
  • 2,018
  • 8
  • 21
  • Awesome, that gets the data structure of the response correct. Thanks so much. Side question is there a way to remove the 'check_errors' from the result so that it only has 'records' – Ashley Kelham Dec 14 '21 at 22:06
  • Yeah, simply just query the `records` rule only, and you'll only see that in the result. If you're running OPA as a server you'd query the endpoint `/v1/data/package_name/records` or `data.package_name.records` if you're using `opa eval` ... the Rego Playground always evaluates the whole document, but if you select the records rule name the "Evaluate" button will be changed to "Evaluate Selection" and only that rule will be evaluated. – Devoops Dec 15 '21 at 09:16
  • 1
    Perfect, thanks a lot for your help – Ashley Kelham Dec 20 '21 at 21:21