Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

caiorcferreira
Copy link

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

  • Create types.EntityReference (inspired by Rust's implementation) to allow policies to be parsed while deferring variable slot resolution.
  • Add slot field to JSON representation, following how cedar-policy-cli serializes Policy Templates. Note that JSON's variable slot is currently wrong in the official documentation
  • Implement LinkTemplateToPolicy, which creates a LinkedPolicy from a Template 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.

  • Few unit tests and no integration test.
  • I left multiple todo comments in the code with decisions that I delayed or that I would like the team input.
  • At this stage, variable slot resolution only works for == 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!

@patjakdev
Copy link
Collaborator

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
Copy link
Collaborator

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!

Copy link
Author

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

Caio Cavalcante added 8 commits October 29, 2024 14:52
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]>
Comment on lines +28 to +31
case s.Slot != nil:
ref = types.VariableSlot{Name: types.String(*s.Slot)}
case s.Entity != nil:
ref = types.EntityUID(*s.Entity)
Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Author

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: entity or slot

Comment on lines +28 to +30
case s.Slot != nil:
ref = types.VariableSlot{Name: types.String(*s.Slot)}
case s.Entity != nil:
Copy link
Collaborator

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")
Copy link
Collaborator

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:

  1. ast.LinkTemplateToPolicy(ast.Template, map[string]string) *ast.Policy - this basically just clones the template body and calls the linkScope() functions you've outlined, returning a *ast.Policy.
  2. func (*PolicySet) StoreLinkedPolicy(TemplateID, PolicyID, map[string]types.EntityUID) - this looks up the template in the PolicySet by TemplateID, calls ast.LinkTemplateToPolicy() to get an ast.Policy() and then calls PolicySet.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 {
Copy link
Collaborator

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?
Copy link
Collaborator

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.

Comment on lines +75 to +82
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))
}
Copy link
Collaborator

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")
Copy link
Collaborator

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?
Copy link
Collaborator

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.

@caiorcferreira
Copy link
Author

Thanks for the thorough review! I'm working on applying your suggestions and extending the design to other operators beyond =.

@philhassey
Copy link
Collaborator

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.

@caiorcferreira
Copy link
Author

Using __cedar::Slot would reduce some work, but I don't think it would be much. Although template variables are now restricted to the policy scope, I can see they being extended to when/unless conditions in the near future.
Even more now, with the introduction of tags, producing a policy by replacing a tag in the when clause is an everyday use case.

Hence, I feel that laying the groundwork for variables with their own type is useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants