diff --git a/README.md b/README.md index 0ba3894..6ab2ce3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This repository contains examples demonstrating the use of [Cedar](https://githu |---------------------------------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`tinytodo`][] | Rust, Python | A simple application for managing task lists that uses Cedar for authorization demonstrating the usage of the [Cedar Rust APIs][] | | [`tinytodo-go`][] | Go, Python | A simple application for managing task lists that uses Cedar for authorization demonstrating the usage of the [Cedar Go APIs][] | | +| [`cedar-go-hello-world`][] | Go | A simple application demonstrating the usage of the [Cedar Go APIs][] | | [`cedar-java-hello-world`][] | Java | A simple application demonstrating the usage of the [Cedar Java APIs][] | | [`cedar-rust-hello-world`][] | Rust | A simple application demonstrating the usage of the [Cedar Rust APIs][] | | [`cedar-wasm-example`][] | TypeScript | A simple application demonstrating the usage of the [Cedar Wasm APIs][] | @@ -26,6 +27,7 @@ This project is licensed under the Apache-2.0 License. [Cedar Java APIs]: https://github.com/cedar-policy/cedar-java [Cedar Wasm APIs]: https://github.com/cedar-policy/cedar/tree/main/cedar-wasm [`cedar-example-use-cases`]: ./cedar-example-use-cases +[`cedar-go-hello-world`]: ./cedar-go-hello-world [`cedar-java-hello-world`]: ./cedar-java-hello-world [`cedar-rust-hello-world`]: ./cedar-rust-hello-world [`cedar-wasm-example`]: ./cedar-wasm-example diff --git a/cedar-go-hello-world/README.md b/cedar-go-hello-world/README.md new file mode 100644 index 0000000..528280b --- /dev/null +++ b/cedar-go-hello-world/README.md @@ -0,0 +1,15 @@ +# Cedar Go Hello World +This repository contains a simple hello world program demonstrating typical usage of the [Cedar Go APIs](https://github.com/cedar-policy/cedar-go). +It is designed for demonstration purposes and does not reflect a production-ready application. + +The file `main.go` provides example code, showcasing the primary steps an application takes to use Cedar. +- Parsing policies from JSON and Cedar +- Creating policies programmatically with the AST package +- Creating entity sets +- Creating authorization requests +- Outputting and interpreting the decision from the request + +## Setup and run +The example is built on Go 1.25.6 and uses cedar-go 1.6.0 - you will need to have a compatible version of Go installed. +To run the program, navigate to the `cedar-go-hello-world` directory and run the following command: +`go run .` \ No newline at end of file diff --git a/cedar-go-hello-world/go.mod b/cedar-go-hello-world/go.mod new file mode 100644 index 0000000..928f644 --- /dev/null +++ b/cedar-go-hello-world/go.mod @@ -0,0 +1,7 @@ +module cedar-go-hello-world + +go 1.25.6 + +require github.com/cedar-policy/cedar-go v1.6.0 + +require golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e // indirect diff --git a/cedar-go-hello-world/go.sum b/cedar-go-hello-world/go.sum new file mode 100644 index 0000000..984ee36 --- /dev/null +++ b/cedar-go-hello-world/go.sum @@ -0,0 +1,6 @@ +github.com/cedar-policy/cedar-go v1.6.0 h1:5dYWkrQjza+GzdJxnzmus7Ag/2pHv4bYWe460/kDlAM= +github.com/cedar-policy/cedar-go v1.6.0/go.mod h1:h5+3CVW1oI5LXVskJG+my9TFCYI5yjh/+Ul3EJie6MI= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e h1:Ctm9yurWsg7aWwIpH9Bnap/IdSVxixymIb3MhiMEQQA= +golang.org/x/exp v0.0.0-20220921023135-46d9e7742f1e/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= diff --git a/cedar-go-hello-world/main.go b/cedar-go-hello-world/main.go new file mode 100644 index 0000000..ed76087 --- /dev/null +++ b/cedar-go-hello-world/main.go @@ -0,0 +1,214 @@ +package main + +import ( + "fmt" + + "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/ast" +) + +func main() { + // For an authorization evaluation, we need three inputs: + // 1. A cedar.PolicySet to evaluate against the request. + // 2. A cedar.EntityMap that contains the entities referenced in the policies and request. + // 3. A cedar.Request to evaluate. + // In this example, we define the policies, entities, and requests inline, + // but they could also be loaded from external sources. + policySet := buildPolicySet() + entityMap := buildEntityMap() + // We build a slice of requests to showcase the different outcomes of the cedar.Authorize method. + evalRequests := buildRequests() + + // The first request is for Alice to view JanesVacation.jpg, which should be allowed by policy0. + fmt.Println("Evaluating if Alice can view JanesVacation.jpg...") + ok, diag := cedar.Authorize(policySet, entityMap, evalRequests[0]) + outputDecision(ok, diag) + + // The second request is for Alice to view JanesVacationPrivate.jpg, which should be denied by policy2. + fmt.Println("Evaluating if Alice can view JanesVacationPrivate.jpg...") + ok, diag = cedar.Authorize(policySet, entityMap, evalRequests[1]) + outputDecision(ok, diag) + + // The third request is for Alice to view John's vacation album, which should be denied and error + // as the resource Album::"john_vacation" does not exist in the entity map. + fmt.Println("Evaluating if Alice can view John's vacation album...") + ok, diag = cedar.Authorize(policySet, entityMap, evalRequests[2]) + outputDecision(ok, diag) + + // The fourth request is for Bob to view JanesVacation.jpg, which should be denied as no policy gives permission. + fmt.Println("Evaluating if Bob can view JanesVacation.jpg...") + ok, diag = cedar.Authorize(policySet, entityMap, evalRequests[3]) + outputDecision(ok, diag) +} + +// buildPolicySet constructs a PolicySet with three policies: +// 1. A standard permit policy defined via JSON. +// 2. A permit policy with context defined via Cedar syntax. +// 3. A forbid policy constructed via AST. +// It returns the constructed PolicySet for use in authorization evaluations. +func buildPolicySet() *cedar.PolicySet { + // policy0 allows Alice to view the photo JanesVacation.jpg + policy0 := buildPolicyFromJson() + // policy1 allows Alice to view a photo if it's tagged with work + policy1 := buildPolicyFromCedar() + // policy2 forbids access to resources tagged as private unless the principal is the owner of the resource. + policy2 := buildPolicyFromAST() + + policySet := cedar.NewPolicySet() + policySet.Add("policy0", &policy0) + policySet.Add("policy1", &policy1) + policySet.Add("policy2", policy2) + + return policySet +} + +// buildPolicyFromJson constructs a simple permit policy from a JSON representation. +// The policy allows the user "alice" to perform the "view" action on the Photo resource "JanesVacation.jpg". +func buildPolicyFromJson() cedar.Policy { + var jsonPolicy = []byte(`{ + "effect": "permit", + "principal": {"op": "==", "entity": {"type": "User", "id": "alice"}}, + "action": {"op": "==", "entity": {"type": "Action", "id": "view"}}, + "resource": {"op": "==", "entity": {"type": "Photo", "id": "JanesVacation.jpg"}} + }`) + + var policy0 cedar.Policy + if err := policy0.UnmarshalJSON(jsonPolicy); err != nil { + fmt.Println("Unmarshal error for JSON policy:", err) + return cedar.Policy{} + } + + return policy0 +} + +// buildPolicyFromCedar constructs a permit policy from a Cedar syntax representation. +// The policy allows the user "john" to perform the "view" action on the "jane_vacation" album resource +// when the resource is tagged with "work". +func buildPolicyFromCedar() cedar.Policy { + var cedarPolicy = []byte(`permit ( + principal == User::"john", + action == Action::"view", + resource in Album::"jane_vacation" + ) + when{ resource.tags.contains("work") };`) + + var policy1 cedar.Policy + if err := policy1.UnmarshalCedar(cedarPolicy); err != nil { + fmt.Println("Unmarshal error for Cedar policy:", err) + return cedar.Policy{} + } + return policy1 +} + +// buildPolicyFromAST constructs a forbid policy programmatically using the AST package. +// The policy forbids actions when the resource is tagged as private, +// unless the principal is the owner of the resource. +func buildPolicyFromAST() *cedar.Policy { + + astPolicy := ast.Forbid(). + When(ast.Resource().Access("tags").Contains(ast.String("private"))). + Unless(ast.Resource().Access("owner").Equal(ast.Principal())) + + policy2 := cedar.NewPolicyFromAST(astPolicy) + return policy2 +} + +// buildEntityMap constructs an EntityMap containing the entities referenced in the policies and requests. +func buildEntityMap() cedar.EntityMap { + var jsonEntities = []byte(`[ + { + "uid": { "type": "User", "id": "alice" }, + "attrs": {}, + "parents": [] + }, + { + "uid": { "type": "User", "id": "john" }, + "attrs": {}, + "parents": [] + }, + { + "uid": { "type": "User", "id": "jane" }, + "attrs": {}, + "parents": [] + }, + { + "uid": { "type": "Photo", "id": "JanesVacation.jpg" }, + "attrs": { "tags": [], "owner": "User::jane" }, + "parents": [{ "type": "Album", "id": "jane_vacation" }] + }, + { + "uid": { "type": "Photo", "id": "JanesVacationPrivate.jpg" }, + "attrs": { "tags": ["private"], "owner": "User::jane" }, + "parents": [{ "type": "Album", "id": "jane_vacation" }] + }, + { + "uid": { "type": "Album", "id": "jane_vacation" }, + "attrs": { "tags": [], "owner": "User::jane" }, + "parents": [] + } + ]`) + + var entityMap cedar.EntityMap + if err := entityMap.UnmarshalJSON(jsonEntities); err != nil { + fmt.Println("Entity map unmarshal error:", err) + return cedar.EntityMap{} + } + return entityMap +} + +// buildRequests constructs a slice of Requests to pass to the Authorize method for evaluation. Each request is designed to test different policies and outcomes: +// 1. Alice viewing JanesVacation.jpg (should be allowed by policy0). +// 2. Alice viewing JanesVacationPrivate.jpg (should be denied by policy2). +// 3. Alice viewing John's vacation album (should be denied due to an error of the resource missing from the entity map and therefore unable to fulfil policy2) +// 4. John viewing JanesVacation.jpg (should be denied because no policy applies). +func buildRequests() []cedar.Request { + + makeRequest := func(principalType, actionType, resourceType cedar.EntityType, principalID, actionID, resourceID cedar.String) cedar.Request { + return cedar.Request{ + Principal: cedar.NewEntityUID(principalType, principalID), + Action: cedar.NewEntityUID(actionType, actionID), + Resource: cedar.NewEntityUID(resourceType, resourceID), + } + } + + return []cedar.Request{ + makeRequest("User", "Action", "Photo", "alice", "view", "JanesVacation.jpg"), + makeRequest("User", "Action", "Photo", "alice", "view", "JanesVacationPrivate.jpg"), + makeRequest("User", "Action", "Album", "alice", "view", "john_vacation"), + makeRequest("User", "Action", "Photo", "john", "view", "JanesVacation.jpg"), + } +} + +// outputDecision is a helper function that takes an authorization decision and +// its corresponding diagnostic information, and prints the decision along with +// the reasons and any errors encountered during evaluation. +func outputDecision(decision cedar.Decision, diag cedar.Diagnostic) { + fmt.Println("Decision:", decision.String()) + + // If there are errors in the diagnostic, print them and return. + if len(diag.Errors) > 0 { + fmt.Println("Errors encountered during evaluation:") + for _, err := range diag.Errors { + fmt.Println(" *", err) + fmt.Println() + } + } + + // If there are no reasons, then no policies applied to the request. + if len(diag.Reasons) == 0 { + fmt.Println("No applicable policies found.") + fmt.Println() + return + } + + if decision == cedar.Allow { + fmt.Println("Permitted by:") + } else { + fmt.Println("Denied by:") + } + // The reasons will contain the policies that contributed to the final decision. + for _, d := range diag.Reasons { + fmt.Println(" *", d.PolicyID) + } + fmt.Println() +}