-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: implement policy templates #48
base: main
Are you sure you want to change the base?
Conversation
Thank you very much for the contribution! We'll begin reviewing it today; hopefully we should have some feedback for you soon. |
.editorconfig
Outdated
@@ -2,6 +2,6 @@ | |||
root = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This spacing change broke your PR, it now seems to change everything. Can you update this? Also what editor does this config file work for? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used this configuration because, in a previous PR, my Goland used 4 spaces and changed multiple lines I didn't touch.
I removed the file, but I can't see the linting job. Let me know if the error persists.
About which editor supports it, the most popular ones do without any plugin: VS Code, IntelliJ, Vim, etc. It's an open-source standard
3e2a17a
to
193b04c
Compare
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
Signed-off-by: Caio Cavalcante <[email protected]>
0d94494
to
198ce1b
Compare
case s.Slot != nil: | ||
ref = types.VariableSlot{Name: types.String(*s.Slot)} | ||
case s.Entity != nil: | ||
ref = types.EntityUID(*s.Entity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me a feel a bit icky that slots could end up being parsed into static policies and not just templates.
I wonder if we want a type Template struct
which calls the same underlying code to do the JSON unmarshaling, but passes a flag to this function indicating that slots are allowed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may have to imitate Rust implementation and have slots in the Policy
type.
The following policy is valid:
@id("AccessVacation")
permit(
principal,
action == Action::"view",
resource
);
@id("Template")
permit(
principal == ?principal,
action == Action::"view",
resource
);
It contains both a static policy and a template. Because of that, in Rust, a policy is defined as:
pub struct Policy {
/// Reference to the template
template: Arc<Template>,
/// Id of this link
/// None in the case that this is an instance of a Static Policy
link: Option<PolicyID>,
// INVARIANT (values total map)
// All of the slots in `template` MUST be bound by `values`
/// values the slots are bound to.
/// The constructor `new` is only visible in this module,
/// so it is the responsibility of callers to maintain
values: HashMap<SlotId, EntityUID>,
}
I started splitting the serialization so that slots could only be parsed by a type Template struct
. But because the above policy is valid, we couldn't know ahead of time whether to use a Policy
or Template
.
Unless we create a cedarDTO
that could be either policy or template and decide which type to use based on whether there are slots, but I wouldn't say I like this idea.
However, instead of doing exactly like Rust, I think we could compromise to avoid a massive change in the codebase by changing ast.Policy
to:
type templateContext struct {
linkID types.String
slots map[types.SlotID]types.EntityUID
}
type Policy struct {
Effect Effect
Annotations []AnnotationType // duplicate keys are prevented via the builders
Principal IsPrincipalScopeNode
Action IsActionScopeNode
Resource IsResourceScopeNode
Conditions []ConditionType
Position Position
templateContext templateContext
}
P.S. I'm still trying to think of a better name than templateContext
. The case is to encapsulate template-related information in a new field instead of encapsulating the expression fields in a Template
field, which would demand changes in all the codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @patjakdev, do you have any insights on this?
case s.Entity != nil: | ||
ref = types.EntityUID(*s.Entity) | ||
default: | ||
return nil, fmt.Errorf("missing entity and slot") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: entity or slot
case s.Slot != nil: | ||
ref = types.VariableSlot{Name: types.String(*s.Slot)} | ||
case s.Entity != nil: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should error if both Slot
and Entity
are set.
// todo: add invariant len(slotEnv) == len(template.Slots) | ||
func LinkTemplateToPolicy(template ast.Template, linkID string, slotEnv map[string]string) ast.LinkedPolicy { | ||
p := template.Body | ||
templateID, _ := findAnnotation(p, "id") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IDs as annotations are not part of the Cedar spec and are merely a nicety supported by the Cedar CLI. So, we can't be mucking around with annotations at the AST level. Instead, the mapping of IDs to policies or templates should all be done at the PolicySet
level.
So, I think we end up with two functions:
ast.LinkTemplateToPolicy(ast.Template, map[string]string) *ast.Policy
- this basically just clones the template body and calls thelinkScope()
functions you've outlined, returning a*ast.Policy
.func (*PolicySet) StoreLinkedPolicy(TemplateID, PolicyID, map[string]types.EntityUID)
- this looks up the template in thePolicySet
byTemplateID
, callsast.LinkTemplateToPolicy()
to get anast.Policy()
and then callsPolicySet.Store()
to store the policy with the given ID.
Unfortunately, in order to implement the JSON serialization spec for PolicySet, we'll also have to keep around the information about the template links that were created and whether stored policies were added statically or via a template link.
|
||
// todo: how to clone policy body to avoid modifying the original template? | ||
// todo: add invariant len(slotEnv) == len(template.Slots) | ||
func LinkTemplateToPolicy(template ast.Template, linkID string, slotEnv map[string]string) ast.LinkedPolicy { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want slotEnv
to be a map[string]types.EntityUID
. Or, even better, a map[types.SlotID]types.EntityUID
.
return linkedScope.(T) | ||
} | ||
|
||
// todo: should we panic on this? or just trust that the interface is correct? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any harm in panicking.
func parseEntityUID(euid string) types.EntityUID { | ||
typeIDseparator := strings.LastIndex(euid, "::") | ||
|
||
etype := euid[:typeIDseparator] | ||
eID := strings.Trim(euid[typeIDseparator+2:], `"`) | ||
|
||
return types.NewEntityUID(types.EntityType(etype), types.String(eID)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let's just have the value type of the slot env be a types.EntityUID
. Then it's up to the caller how it constructs the EntityUID (NewEntityUID(), UnmarshalJSON(), etc).
@@ -6,6 +6,8 @@ import ( | |||
"strings" | |||
) | |||
|
|||
const CedarVariable = EntityType("__cedar::variable") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want this leaking out. It was just used for some internal placeholder values during partial evaluation.
@@ -84,3 +84,15 @@ func scopeToNode(varNode ast.NodeTypeVariable, in ast.IsScopeNode) ast.Node { | |||
panic(fmt.Sprintf("unknown scope type %T", t)) | |||
} | |||
} | |||
|
|||
// todo: should we panic on this? or just trust that the interface is correct? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
panicking seems fine to me
Hopefully the type system prevents us from ever evaling an ast.Template
.
Thanks for the thorough review! I'm working on applying your suggestions and extending the design to other operators beyond |
I don't know if @patjakdev mentioned this or not, but I think having the Slots be just a EntityUID with the Type being "__cedar::Slot" might be a nice way to avoid changing a lot of interfaces. |
Using Hence, I feel that laying the groundwork for variables with their own type is useful. |
Description of changes:
One of Cedar's main features is Policy Templates, which allows for managing large sets of policies while keeping the authorization rules centralized.
This PR is an attempt to implement Templates in
cedar-go
.Features
types.EntityReference
(inspired by Rust's implementation) to allow policies to be parsed while deferring variable slot resolution.slot
field to JSON representation, following howcedar-policy-cli
serializes Policy Templates. Note that JSON's variable slot is currently wrong in the official documentationLinkTemplateToPolicy
, which creates aLinkedPolicy
from aTemplate
and variable slot environment.Limitations
I wanted to gather feedback on the implementation strategy before covering all the bases, so this PR has lots of shortcuts that will be addressed if you believe I'm heading in the right direction.
todo
comments in the code with decisions that I delayed or that I would like the team input.==
expressions in principal and resource scope conditions.Conclusion
Policy templates will soon become a critical feature for my Cedar use case. Hence, I want to try to move this feature forward, and I appreciate any help!