From 430111a8dc778f7c9e4b6e25c96a03befdff210f Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 19 Jul 2024 17:53:15 +0800 Subject: [PATCH 1/4] add saturn module to implement cedar-policy --- Cargo.toml | 2 + saturn/Cargo.toml | 15 ++++++ saturn/mega.cedarschema | 103 +++++++++++++++++++++++++++++++++++++ saturn/mega_policies.cedar | 66 ++++++++++++++++++++++++ saturn/src/context.rs | 72 ++++++++++++++++++++++++++ saturn/src/main.rs | 63 +++++++++++++++++++++++ 6 files changed, 321 insertions(+) create mode 100644 saturn/Cargo.toml create mode 100644 saturn/mega.cedarschema create mode 100644 saturn/mega_policies.cedar create mode 100644 saturn/src/context.rs create mode 100644 saturn/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index f5820bbe8..d7fcd6c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "libra", "vault", "neptune", + "saturn", ] exclude = ["craft"] resolver = "1" @@ -25,6 +26,7 @@ callisto = { path = "jupiter/callisto" } gemini = { path = "gemini" } vault = { path = "vault" } neptune = { path = "neptune" } +saturn = { path = "saturn" } anyhow = "1.0.86" serde = "1.0.203" diff --git a/saturn/Cargo.toml b/saturn/Cargo.toml new file mode 100644 index 000000000..ae3453866 --- /dev/null +++ b/saturn/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "saturn" +version = "0.1.0" +edition = "2021" + +[dependencies] +common = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } +serde_json = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +thiserror = { workspace = true } + +itertools = "0.10.5" +cedar-policy = "3.2.1" diff --git a/saturn/mega.cedarschema b/saturn/mega.cedarschema new file mode 100644 index 000000000..551dd386c --- /dev/null +++ b/saturn/mega.cedarschema @@ -0,0 +1,103 @@ +namespace Mega { + entity User = { + "username": String, + "role": String, + }; + + entity Team = { + "name": String, + "members": Set, + }; + + entity Repository in Team = { + "name": String, + "private": Bool, + "owner": User, + "collaborators": Set, + }; + + entity PullRequest in Repository = { + "title": String, + "owner": User, + "source": String, + "target": String, + }; + + entity Issue in Repository = { + "title": String, + "owner": User, + "status": String, // E.g., "open", "closed" + }; + + action "createRepo" appliesTo { + principal: User, + resource: Repository, + context: { + "authenticated": Bool, + }, + }; + + action "deleteRepo" appliesTo { + principal: User, + resource: Repository, + context: { + "authenticated": Bool, + }, + }; + + action "viewRepo" appliesTo { + principal: User, + resource: Repository, + context: { + "authenticated": Bool, + }, + }; + + action "createPullRequest" appliesTo { + principal: User, + resource: Repository, + context: { + "authenticated": Bool, + }, + }; + + action "closePullRequest" appliesTo { + principal: User, + resource: Repository, + context: { + "authenticated": Bool, + }, + }; + + action "mergePullRequest" appliesTo { + principal: User, + resource: PullRequest, + context: { + "authenticated": Bool, + }, + }; + + action "createIssue" appliesTo { + principal: User, + resource: Issue, + context: { + "authenticated": Bool, + }, + }; + + action "closeIssue" appliesTo { + principal: User, + resource: Issue, + context: { + "authenticated": Bool, + }, + }; + + action "viewIssue" appliesTo { + principal: User, + resource: Issue, + context: { + "authenticated": Bool, + }, + }; +} \ No newline at end of file diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar new file mode 100644 index 000000000..91256b40e --- /dev/null +++ b/saturn/mega_policies.cedar @@ -0,0 +1,66 @@ +// policy "anyoneCanViewPublicRepo" even if not login? +permit ( + principal, + action in [ + Mega::Action::"viewRepo" + ], + resource +) +when { resource.private == false }; + +// policy A User can perform any action on a Repo they own +permit ( + principal, + action, + resource is Mega::Repository +) +when { context.authenticated == true && resource.owner == principal }; + + +// policy "adminCanManageRepos" +permit ( + principal, + action in + [Mega::Action::"createRepo", + Mega::Action::"deleteRepo", + Mega::Action::"viewRepo"], + resource +) +when { context.authenticated == true && principal.role == "admin" }; + + +// policy "collaboratorCanCreatePR" +permit ( + principal, + action in [Mega::Action::"createPullRequest"], + resource +) +when { context.authenticated == true && principal in resource.collaborators }; + +// policy "ownerCanMergePR" +permit ( + principal, + action in [Mega::Action::"mergePullRequest"], + resource +) +when { context.authenticated == true && principal == resource.owner }; + + +// policy "userCanManageIssues" +permit ( + principal, + action in [ + Mega::Action::"createIssue", + Mega::Action::"closeIssue" + ], + resource +) +when { context.authenticated == true && principal == resource.owner }; + + +//policy "anyoneCanViewIssues" +permit ( + principal, + action in [Mega::Action::"viewIssue"], + resource +); \ No newline at end of file diff --git a/saturn/src/context.rs b/saturn/src/context.rs new file mode 100644 index 000000000..68c94954c --- /dev/null +++ b/saturn/src/context.rs @@ -0,0 +1,72 @@ +use cedar_policy::{ + Authorizer, HumanSchemaError, ParseErrors, PolicySet, PolicySetError, Schema, SchemaError, + ValidationMode, Validator, +}; +use itertools::Itertools; +use std::path::PathBuf; +use thiserror::Error; + +#[allow(dead_code)] +pub struct AppContext { + // entities: EntityStore, + authorizer: Authorizer, + policies: PolicySet, + schema: Schema, +} + +#[derive(Debug, Error)] +pub enum ContextError { + #[error("{0}")] + IO(#[from] std::io::Error), + #[error("Error Parsing Json Schema: {0}")] + JsonSchema(#[from] SchemaError), + #[error("Error Parsing Human-readable Schema: {0}")] + CedarSchema(#[from] HumanSchemaError), + #[error("Error Parsing PolicySet: {0}")] + Policy(#[from] ParseErrors), + #[error("Error Processing PolicySet: {0}")] + PolicySet(#[from] PolicySetError), + #[error("Validation Failed: {0}")] + Validation(String), + #[error("Error Deserializing Json: {0}")] + Json(#[from] serde_json::Error), +} + +impl AppContext { + pub fn new( + _: impl Into, + schema_path: impl Into, + policies_path: impl Into, + ) -> Result { + let schema_path = schema_path.into(); + let policies_path = policies_path.into(); + + let schema_file = std::fs::File::open(schema_path)?; + let (schema, _) = Schema::from_file_natural(schema_file).unwrap(); + // let entities_file = std::fs::File::open(entities_path.into())?; + // let entities = serde_json::from_reader(entities_file)?; + let policy_src = std::fs::read_to_string(policies_path)?; + let policies = policy_src.parse()?; + let validator = Validator::new(schema.clone()); + let output = validator.validate(&policies, ValidationMode::default()); + + if output.validation_passed() { + tracing::info!("Validation passed!"); + let authorizer = Authorizer::new(); + let c = Self { + // entities, + authorizer, + policies, + schema, + }; + + Ok(c) + } else { + let error_string = output + .validation_errors() + .map(|err| format!("{err}")) + .join("\n"); + Err(ContextError::Validation(error_string)) + } + } +} diff --git a/saturn/src/main.rs b/saturn/src/main.rs new file mode 100644 index 000000000..90e1477c4 --- /dev/null +++ b/saturn/src/main.rs @@ -0,0 +1,63 @@ +use context::AppContext; + +mod context; + +fn main() { + tracing_subscriber::fmt().pretty().init(); + let current_dir = env!("CARGO_MANIFEST_DIR"); + let (schema_path, policies_path) = ( + format!("{}/{}", current_dir, "mega.cedarschema"), + format!("{}/{}", current_dir, "mega_policies.cedar"), + ); + + match AppContext::new("./entities.json", schema_path, policies_path) { + Ok(app) => app, + Err(e) => { + tracing::error!("Failed to load entities, policies, or schema: {e}"); + std::process::exit(1); + } + }; +} + +#[cfg(test)] +mod test { + use cedar_policy::*; + + #[test] + fn test_cedar() { + const POLICY_SRC: &str = r#" + permit(principal == User::"alice", action == Action::"view", resource == File::"93"); + "#; + let policy: PolicySet = POLICY_SRC.parse().unwrap(); + + let action = r#"Action::"view""#.parse().unwrap(); + let alice = r#"User::"alice""#.parse().unwrap(); + let file = r#"File::"93""#.parse().unwrap(); + let request = Request::new( + Some(alice), + Some(action), + Some(file), + Context::empty(), + None, + ) + .unwrap(); + + let entities = Entities::empty(); + let authorizer = Authorizer::new(); + let answer = authorizer.is_authorized(&request, &policy, &entities); + + // Should output `Allow` + println!("{:?}", answer.decision()); + + let action = r#"Action::"view""#.parse().unwrap(); + let bob = r#"User::"bob""#.parse().unwrap(); + let file = r#"File::"93""#.parse().unwrap(); + let request = + Request::new(Some(bob), Some(action), Some(file), Context::empty(), None).unwrap(); + + let answer = authorizer.is_authorized(&request, &policy, &entities); + + // Should output `Deny` + println!("{:?}", answer.decision()); + } +} From cdf70770754cbedc570fc1098035f7535469f0cf Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Sun, 21 Jul 2024 17:54:02 +0800 Subject: [PATCH 2/4] update policy --- saturn/mega.cedarschema | 50 ++++++++++++++++++++++++++++++++++---- saturn/mega_policies.cedar | 29 ++++++++++++++++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/saturn/mega.cedarschema b/saturn/mega.cedarschema index 551dd386c..2188fb8aa 100644 --- a/saturn/mega.cedarschema +++ b/saturn/mega.cedarschema @@ -16,7 +16,7 @@ namespace Mega { "collaborators": Set, }; - entity PullRequest in Repository = { + entity MergeRequest in Repository = { "title": String, "owner": User, "source": String, @@ -53,7 +53,7 @@ namespace Mega { }, }; - action "createPullRequest" appliesTo { + action "createMergeRequest" appliesTo { principal: User, resource: Repository, context: { @@ -61,7 +61,7 @@ namespace Mega { }, }; - action "closePullRequest" appliesTo { + action "closeMergeRequest" appliesTo { principal: User, resource: Repository, context: { @@ -69,14 +69,54 @@ namespace Mega { }, }; - action "mergePullRequest" appliesTo { + action "mergeMergeRequest" appliesTo { principal: User, - resource: PullRequest, + resource: MergeRequest, context: { "authenticated": Bool, }, }; + action "createTeam" appliesTo { + principal: User, + resource: Team, + context: { + "authenticated": Bool, + }, + }; + + action "deleteTeam" appliesTo { + principal: User, + resource: Team, + context: { + "authenticated": Bool, + }, + }; + + action "viewTeam" appliesTo { + principal: User, + resource: Team, + context: { + "authenticated": Bool, + }, + }; + + action "addMember" appliesTo { + principal: User, + resource: Team, + context: { + "authenticated": Bool, + }, + }; + + action "removeMember" appliesTo { + principal: User, + resource: Team, + context: { + "authenticated": Bool, + }, + }; + action "createIssue" appliesTo { principal: User, resource: Issue, diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar index 91256b40e..661240b7f 100644 --- a/saturn/mega_policies.cedar +++ b/saturn/mega_policies.cedar @@ -32,7 +32,7 @@ when { context.authenticated == true && principal.role == "admin" }; // policy "collaboratorCanCreatePR" permit ( principal, - action in [Mega::Action::"createPullRequest"], + action in [Mega::Action::"createMergeRequest"], resource ) when { context.authenticated == true && principal in resource.collaborators }; @@ -40,11 +40,36 @@ when { context.authenticated == true && principal in resource.collaborators }; // policy "ownerCanMergePR" permit ( principal, - action in [Mega::Action::"mergePullRequest"], + action in [Mega::Action::"mergeMergeRequest"], resource ) when { context.authenticated == true && principal == resource.owner }; +// policy "adminCanManageTeams" +permit ( + principal, + action in [ + Mega::Action::"createTeam", + Mega::Action::"deleteTeam", + Mega::Action::"viewTeam", + Mega::Action::"addMember", + Mega::Action::"removeMember" + ], + resource +) +when { context.authenticated == true && principal.role == "admin" }; + +// policy "memberCanViewTeam" +permit ( + principal, + action in [ + Mega::Action::"viewTeam" + ], + resource +) +when { context.authenticated == true && principal in resource.members }; + + // policy "userCanManageIssues" permit ( From e416dce36ee464bb6af0aea424d16fd7874272ee Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Mon, 22 Jul 2024 16:53:09 +0800 Subject: [PATCH 3/4] update policy --- saturn/mega.cedarschema | 135 +++++++++---------------------------- saturn/mega_policies.cedar | 98 ++++++++++++++------------- saturn/src/context.rs | 2 +- 3 files changed, 82 insertions(+), 153 deletions(-) diff --git a/saturn/mega.cedarschema b/saturn/mega.cedarschema index 2188fb8aa..292856feb 100644 --- a/saturn/mega.cedarschema +++ b/saturn/mega.cedarschema @@ -1,143 +1,68 @@ namespace Mega { - entity User = { - "username": String, + // entity Public; + entity UserGroup in [UserGroup]; + entity User in [UserGroup, Team] { "role": String, }; - entity Team = { - "name": String, + entity Team in [UserGroup] { + "admins": UserGroup, "members": Set, }; - entity Repository in Team = { - "name": String, + entity Repository = { "private": Bool, - "owner": User, - "collaborators": Set, + "admins": UserGroup, + "maintainers": UserGroup, + "readers": UserGroup, + "triagers": UserGroup, + "writers": UserGroup, }; - entity MergeRequest in Repository = { - "title": String, + entity MergeRequest = { + "repo": Repository, "owner": User, "source": String, "target": String, }; - entity Issue in Repository = { - "title": String, + entity Issue = { + "repo": Repository, "owner": User, - "status": String, // E.g., "open", "closed" - }; - - action "createRepo" appliesTo { - principal: User, - resource: Repository, - context: { - "authenticated": Bool, - }, - }; - - action "deleteRepo" appliesTo { - principal: User, - resource: Repository, - context: { - "authenticated": Bool, - }, - }; - - action "viewRepo" appliesTo { - principal: User, - resource: Repository, - context: { - "authenticated": Bool, - }, - }; - - action "createMergeRequest" appliesTo { - principal: User, - resource: Repository, - context: { - "authenticated": Bool, - }, - }; - - action "closeMergeRequest" appliesTo { - principal: User, - resource: Repository, - context: { - "authenticated": Bool, - }, }; - action "mergeMergeRequest" appliesTo { - principal: User, - resource: MergeRequest, + action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { + principal: [User], + resource: [Repository], context: { "authenticated": Bool, }, }; - action "createTeam" appliesTo { - principal: User, - resource: Team, + action "createMergeRequest", "closeMergeRequest", "approveMergeRequest" appliesTo { + principal: [User], + resource: [MergeRequest], context: { "authenticated": Bool, }, }; - action "deleteTeam" appliesTo { - principal: User, - resource: Team, + action "createTeam", "viewTeam" ,"addMember", "removeMember", "deleteTeam" appliesTo { + principal: [User], + resource: [Team], context: { "authenticated": Bool, }, }; - action "viewTeam" appliesTo { - principal: User, - resource: Team, - context: { - "authenticated": Bool, - }, + action openIssue, assignIssue, deleteIssue, editIssue appliesTo { + principal: [User], + resource: [Issue] }; - action "addMember" appliesTo { - principal: User, - resource: Team, - context: { - "authenticated": Bool, - }, + action addReader, addWriter, addMaintainer, addAdmin, addTriager appliesTo { + principal: [User], + resource: [Repository] }; - action "removeMember" appliesTo { - principal: User, - resource: Team, - context: { - "authenticated": Bool, - }, - }; - - action "createIssue" appliesTo { - principal: User, - resource: Issue, - context: { - "authenticated": Bool, - }, - }; - - action "closeIssue" appliesTo { - principal: User, - resource: Issue, - context: { - "authenticated": Bool, - }, - }; - - action "viewIssue" appliesTo { - principal: User, - resource: Issue, - context: { - "authenticated": Bool, - }, - }; } \ No newline at end of file diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar index 661240b7f..893193f7f 100644 --- a/saturn/mega_policies.cedar +++ b/saturn/mega_policies.cedar @@ -1,91 +1,95 @@ // policy "anyoneCanViewPublicRepo" even if not login? permit ( principal, - action in [ - Mega::Action::"viewRepo" - ], + action in [Mega::Action::"viewRepo"], resource ) -when { resource.private == false }; +unless { resource.private }; -// policy A User can perform any action on a Repo they own -permit ( - principal, - action, - resource is Mega::Repository -) -when { context.authenticated == true && resource.owner == principal }; - - -// policy "adminCanManageRepos" +// policy "adminCanManageTeams" permit ( principal, action in - [Mega::Action::"createRepo", - Mega::Action::"deleteRepo", - Mega::Action::"viewRepo"], + [Mega::Action::"createTeam", + Mega::Action::"deleteTeam", + Mega::Action::"viewTeam", + Mega::Action::"addMember", + Mega::Action::"removeMember"], resource ) when { context.authenticated == true && principal.role == "admin" }; - -// policy "collaboratorCanCreatePR" +// policy "memberCanViewTeam" permit ( principal, - action in [Mega::Action::"createMergeRequest"], + action in [Mega::Action::"viewTeam"], resource ) -when { context.authenticated == true && principal in resource.collaborators }; +when { context.authenticated == true && principal in resource.members }; -// policy "ownerCanMergePR" +//Actions for readers permit ( principal, - action in [Mega::Action::"mergeMergeRequest"], + action in [Mega::Action::"pullRepo", Mega::Action::"forkRepo"], resource ) -when { context.authenticated == true && principal == resource.owner }; +when { principal in resource.readers }; -// policy "adminCanManageTeams" permit ( principal, - action in [ - Mega::Action::"createTeam", - Mega::Action::"deleteTeam", - Mega::Action::"viewTeam", - Mega::Action::"addMember", - Mega::Action::"removeMember" - ], + action in [Mega::Action::"openIssue", Mega::Action::"createMergeRequest"], resource ) -when { context.authenticated == true && principal.role == "admin" }; +when { principal in resource.repo.readers }; -// policy "memberCanViewTeam" permit ( principal, - action in [ - Mega::Action::"viewTeam" - ], + action in [Mega::Action::"editIssue"], resource ) -when { context.authenticated == true && principal in resource.members }; +when { principal in resource.repo.readers && principal == resource.owner }; +//Actions for triagers +permit ( + principal, + action == Mega::Action::"assignIssue", + resource +) +when { principal in resource.repo.triagers }; +//Actions for writers +permit ( + principal, + action == Mega::Action::"pushRepo", + resource +) +when { principal in resource.writers }; -// policy "userCanManageIssues" permit ( principal, - action in [ - Mega::Action::"createIssue", - Mega::Action::"closeIssue" - ], + action in [Mega::Action::"editIssue", Mega::Action::"approveMergeRequest"], resource ) -when { context.authenticated == true && principal == resource.owner }; +when { principal in resource.repo.writers }; +//Actions for maintainers +permit ( + principal, + action in + [Mega::Action::"deleteIssue", Mega::Action::"approveMergeRequest"], + resource +) +when { principal in resource.repo.maintainers }; -//policy "anyoneCanViewIssues" +//Actions for admins permit ( principal, - action in [Mega::Action::"viewIssue"], + action in + [Mega::Action::"addReader", + Mega::Action::"addTriager", + Mega::Action::"addWriter", + Mega::Action::"addMaintainer", + Mega::Action::"addAdmin"], resource -); \ No newline at end of file +) +when { principal in resource.admins }; \ No newline at end of file diff --git a/saturn/src/context.rs b/saturn/src/context.rs index 68c94954c..c947238f5 100644 --- a/saturn/src/context.rs +++ b/saturn/src/context.rs @@ -51,7 +51,7 @@ impl AppContext { let output = validator.validate(&policies, ValidationMode::default()); if output.validation_passed() { - tracing::info!("Validation passed!"); + tracing::info!("All policy validation passed!"); let authorizer = Authorizer::new(); let c = Self { // entities, From 09fab550351137d0799bdc3e32186322110e1927 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Thu, 25 Jul 2024 17:21:44 +0800 Subject: [PATCH 4/4] feat: convert json to cedar Entities --- Cargo.toml | 1 + saturn/Cargo.toml | 3 +- saturn/entities.json | 78 ++++++++++++++++++++ saturn/mega.cedarschema | 119 +++++++++++++----------------- saturn/mega_policies.cedar | 56 +++++++------- saturn/src/context.rs | 54 ++++++++++++-- saturn/src/entitystore.rs | 34 +++++++++ saturn/src/main.rs | 77 ++++++++++++++++++- saturn/src/objects.rs | 147 +++++++++++++++++++++++++++++++++++++ saturn/src/util.rs | 111 ++++++++++++++++++++++++++++ 10 files changed, 572 insertions(+), 108 deletions(-) create mode 100644 saturn/entities.json create mode 100644 saturn/src/entitystore.rs create mode 100644 saturn/src/objects.rs create mode 100644 saturn/src/util.rs diff --git a/Cargo.toml b/Cargo.toml index 42378a816..16d025ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,4 @@ num_cpus = "1.16.0" config = "0.14.0" shadow-rs = "0.29.0" reqwest = "0.12.5" +lazy_static = "1.5.0" diff --git a/saturn/Cargo.toml b/saturn/Cargo.toml index ae3453866..7492cc1b5 100644 --- a/saturn/Cargo.toml +++ b/saturn/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" [dependencies] common = { workspace = true } tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } +serde = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } thiserror = { workspace = true } -itertools = "0.10.5" +itertools = "0.13.0" cedar-policy = "3.2.1" diff --git a/saturn/entities.json b/saturn/entities.json new file mode 100644 index 000000000..ff6f040b0 --- /dev/null +++ b/saturn/entities.json @@ -0,0 +1,78 @@ +{ + "users": { + "User::\"kesha\"": { + "euid": "User::\"kesha\"", + "parents": [ + "UserGroup::\"mega_admin\"" + ] + }, + "User::\"alice\"": { + "euid": "User::\"alice\"", + "parents": [ + "UserGroup::\"mega_reader\"" + ] + } + }, + "repos": { + "Repository::\"mega\"": { + "euid": "Repository::\"mega\"", + "is_private": true, + "admins": "UserGroup::\"mega_admin\"", + "maintainers": "UserGroup::\"mega_matainer\"", + "writers": "UserGroup::\"mega_writer\"", + "triagers": "UserGroup::\"mega_triager\"", + "readers": "UserGroup::\"mega_reader\"", + "parents": [] + }, + "Repository::\"public\"": { + "euid": "Repository::\"public\"", + "is_private": false, + "admins": "UserGroup::\"placeholder\"", + "maintainers": "UserGroup::\"placeholder\"", + "writers": "UserGroup::\"placeholder\"", + "triagers": "UserGroup::\"placeholder\"", + "readers": "UserGroup::\"placeholder\"", + "parents": [] + } + }, + "user_groups": { + "UserGroup::\"mega_admin\"": { + "euid": "UserGroup::\"mega_admin\"", + "parents": [ + "UserGroup::\"mega_matainer\"" + ] + }, + "UserGroup::\"mega_matainer\"": { + "euid": "UserGroup::\"mega_matainer\"", + "parents": [ + "UserGroup::\"mega_writer\"" + ] + }, + "UserGroup::\"mega_writer\"": { + "euid": "UserGroup::\"mega_writer\"", + "parents": [ + "UserGroup::\"mega_triager\"" + ] + }, + "UserGroup::\"mega_triager\"": { + "euid": "UserGroup::\"mega_triager\"", + "parents": [ + "UserGroup::\"mega_reader\"" + ] + }, + "UserGroup::\"mega_reader\"": { + "euid": "UserGroup::\"mega_reader\"", + "parents": [] + }, + "UserGroup::\"placeholder\"": { + "euid": "UserGroup::\"placeholder\"", + "parents": [] + } + }, + "merge_requests": { + + }, + "issues": { + + } +} \ No newline at end of file diff --git a/saturn/mega.cedarschema b/saturn/mega.cedarschema index 292856feb..f3c5cd6a4 100644 --- a/saturn/mega.cedarschema +++ b/saturn/mega.cedarschema @@ -1,68 +1,51 @@ -namespace Mega { - // entity Public; - entity UserGroup in [UserGroup]; - entity User in [UserGroup, Team] { - "role": String, - }; - - entity Team in [UserGroup] { - "admins": UserGroup, - "members": Set, - }; - - entity Repository = { - "private": Bool, - "admins": UserGroup, - "maintainers": UserGroup, - "readers": UserGroup, - "triagers": UserGroup, - "writers": UserGroup, - }; - - entity MergeRequest = { - "repo": Repository, - "owner": User, - "source": String, - "target": String, - }; - - entity Issue = { - "repo": Repository, - "owner": User, - }; - - action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { - principal: [User], - resource: [Repository], - context: { - "authenticated": Bool, - }, - }; - - action "createMergeRequest", "closeMergeRequest", "approveMergeRequest" appliesTo { - principal: [User], - resource: [MergeRequest], - context: { - "authenticated": Bool, - }, - }; - - action "createTeam", "viewTeam" ,"addMember", "removeMember", "deleteTeam" appliesTo { - principal: [User], - resource: [Team], - context: { - "authenticated": Bool, - }, - }; - - action openIssue, assignIssue, deleteIssue, editIssue appliesTo { - principal: [User], - resource: [Issue] - }; - - action addReader, addWriter, addMaintainer, addAdmin, addTriager appliesTo { - principal: [User], - resource: [Repository] - }; - -} \ No newline at end of file +entity UserGroup in [UserGroup]; +entity User in [UserGroup, Team]; + +entity Team in [UserGroup] { + "admins": UserGroup, + "members": UserGroup, +}; + +entity Repository = { + "is_private": Bool, + "admins": UserGroup, + "maintainers": UserGroup, + "readers": UserGroup, + "triagers": UserGroup, + "writers": UserGroup, +}; + +entity MergeRequest = { + "repo": Repository, + "owner": User, +}; + +entity Issue = { + "repo": Repository, + "owner": User, +}; + +action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { + principal: [User], + resource: [Repository], +}; + +action "createMergeRequest", "closeMergeRequest", "approveMergeRequest" appliesTo { + principal: [User], + resource: [MergeRequest], +}; + +action "createTeam", "viewTeam", "addMember", "removeMember", "deleteTeam" appliesTo { + principal: [User], + resource: [Team], +}; + +action "openIssue", "assignIssue", "deleteIssue", "editIssue" appliesTo { + principal: [User], + resource: [Issue], +}; + +action "addReader", "addWriter", "addMaintainer", "addAdmin", "addTriager" appliesTo { + principal: [User], + resource: [Repository], +}; \ No newline at end of file diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar index 893193f7f..d8c549cfd 100644 --- a/saturn/mega_policies.cedar +++ b/saturn/mega_policies.cedar @@ -1,50 +1,50 @@ // policy "anyoneCanViewPublicRepo" even if not login? permit ( principal, - action in [Mega::Action::"viewRepo"], + action in [Action::"viewRepo"], resource ) -unless { resource.private }; +unless { resource.is_private }; // policy "adminCanManageTeams" -permit ( - principal, - action in - [Mega::Action::"createTeam", - Mega::Action::"deleteTeam", - Mega::Action::"viewTeam", - Mega::Action::"addMember", - Mega::Action::"removeMember"], - resource -) -when { context.authenticated == true && principal.role == "admin" }; +// permit ( +// principal, +// action in +// [Action::"createTeam", +// Action::"deleteTeam", +// Action::"viewTeam", +// Action::"addMember", +// Action::"removeMember"], +// resource +// ) +// when { principal.role == "admin" }; // policy "memberCanViewTeam" permit ( principal, - action in [Mega::Action::"viewTeam"], + action in [Action::"viewTeam"], resource ) -when { context.authenticated == true && principal in resource.members }; +when { principal in resource.members }; //Actions for readers permit ( principal, - action in [Mega::Action::"pullRepo", Mega::Action::"forkRepo"], + action in [Action::"viewRepo", Action::"pullRepo", Action::"forkRepo"], resource ) when { principal in resource.readers }; permit ( principal, - action in [Mega::Action::"openIssue", Mega::Action::"createMergeRequest"], + action in [Action::"openIssue", Action::"createMergeRequest"], resource ) when { principal in resource.repo.readers }; permit ( principal, - action in [Mega::Action::"editIssue"], + action in [Action::"editIssue"], resource ) when { principal in resource.repo.readers && principal == resource.owner }; @@ -52,7 +52,7 @@ when { principal in resource.repo.readers && principal == resource.owner }; //Actions for triagers permit ( principal, - action == Mega::Action::"assignIssue", + action == Action::"assignIssue", resource ) when { principal in resource.repo.triagers }; @@ -60,14 +60,14 @@ when { principal in resource.repo.triagers }; //Actions for writers permit ( principal, - action == Mega::Action::"pushRepo", + action == Action::"pushRepo", resource ) when { principal in resource.writers }; permit ( principal, - action in [Mega::Action::"editIssue", Mega::Action::"approveMergeRequest"], + action in [Action::"editIssue", Action::"approveMergeRequest"], resource ) when { principal in resource.repo.writers }; @@ -75,8 +75,7 @@ when { principal in resource.repo.writers }; //Actions for maintainers permit ( principal, - action in - [Mega::Action::"deleteIssue", Mega::Action::"approveMergeRequest"], + action in [Action::"deleteIssue", Action::"approveMergeRequest"], resource ) when { principal in resource.repo.maintainers }; @@ -85,11 +84,12 @@ when { principal in resource.repo.maintainers }; permit ( principal, action in - [Mega::Action::"addReader", - Mega::Action::"addTriager", - Mega::Action::"addWriter", - Mega::Action::"addMaintainer", - Mega::Action::"addAdmin"], + [Action::"addReader", + Action::"addTriager", + Action::"addWriter", + Action::"addMaintainer", + Action::"addAdmin", + Action::"deleteRepo"], resource ) when { principal in resource.admins }; \ No newline at end of file diff --git a/saturn/src/context.rs b/saturn/src/context.rs index c947238f5..fa88c5395 100644 --- a/saturn/src/context.rs +++ b/saturn/src/context.rs @@ -1,14 +1,16 @@ use cedar_policy::{ - Authorizer, HumanSchemaError, ParseErrors, PolicySet, PolicySetError, Schema, SchemaError, - ValidationMode, Validator, + Authorizer, Context, Decision, Diagnostics, HumanSchemaError, ParseErrors, PolicySet, + PolicySetError, Request, Schema, SchemaError, ValidationMode, Validator, }; use itertools::Itertools; use std::path::PathBuf; use thiserror::Error; +use crate::{entitystore::EntityStore, util::EntityUid}; + #[allow(dead_code)] pub struct AppContext { - // entities: EntityStore, + entities: EntityStore, authorizer: Authorizer, policies: PolicySet, schema: Schema, @@ -32,9 +34,17 @@ pub enum ContextError { Json(#[from] serde_json::Error), } +#[derive(Debug, Error)] +pub enum Error { + #[error("Authorization Denied")] + AuthDenied(Diagnostics), + #[error("Error constructing authorization request: {0}")] + Request(String), +} + impl AppContext { pub fn new( - _: impl Into, + entities_path: impl Into, schema_path: impl Into, policies_path: impl Into, ) -> Result { @@ -43,8 +53,8 @@ impl AppContext { let schema_file = std::fs::File::open(schema_path)?; let (schema, _) = Schema::from_file_natural(schema_file).unwrap(); - // let entities_file = std::fs::File::open(entities_path.into())?; - // let entities = serde_json::from_reader(entities_file)?; + let entities_file = std::fs::File::open(entities_path.into())?; + let entities = serde_json::from_reader(entities_file)?; let policy_src = std::fs::read_to_string(policies_path)?; let policies = policy_src.parse()?; let validator = Validator::new(schema.clone()); @@ -54,7 +64,7 @@ impl AppContext { tracing::info!("All policy validation passed!"); let authorizer = Authorizer::new(); let c = Self { - // entities, + entities, authorizer, policies, schema, @@ -69,4 +79,34 @@ impl AppContext { Err(ContextError::Validation(error_string)) } } + + pub fn is_authorized( + &self, + principal: impl AsRef, + action: impl AsRef, + resource: impl AsRef, + context: Context, + ) -> Result<(), Error> { + let es = self.entities.as_entities(&self.schema); + let q = Request::new( + Some(principal.as_ref().clone().into()), + Some(action.as_ref().clone().into()), + Some(resource.as_ref().clone().into()), + context, + Some(&self.schema), + ) + .map_err(|e| Error::Request(e.to_string()))?; + tracing::info!( + "is_authorized request: principal: {}, action: {}, resource: {}", + principal.as_ref(), + action.as_ref(), + resource.as_ref() + ); + let response = self.authorizer.is_authorized(&q, &self.policies, &es); + tracing::info!("Auth response: {:?}", response); + match response.decision() { + Decision::Allow => Ok(()), + Decision::Deny => Err(Error::AuthDenied(response.diagnostics().clone())), + } + } } diff --git a/saturn/src/entitystore.rs b/saturn/src/entitystore.rs new file mode 100644 index 000000000..bd0c0a2b9 --- /dev/null +++ b/saturn/src/entitystore.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +use cedar_policy::{Entities, Schema}; +use serde::{Deserialize, Serialize}; + +use crate::{ + objects::{Issue, MergeRequest, Repo, User, UserGroup}, + util::EntityUid, +}; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct EntityStore { + users: HashMap, + repos: HashMap, + merge_requests: HashMap, + issues: HashMap, + user_groups: HashMap, +} + +impl EntityStore { + pub fn as_entities(&self, schema: &Schema) -> Entities { + let users = self.users.values().map(|user| user.clone().into()); + let repos = self.repos.values().map(|repo| repo.clone().into()); + let merge_requests = self.merge_requests.values().map(|user| user.clone().into()); + let issues = self.issues.values().map(|repo| repo.clone().into()); + let user_groups = self.user_groups.values().map(|group| group.clone().into()); + let all = users + .chain(repos) + .chain(user_groups) + .chain(merge_requests) + .chain(issues); + Entities::from_entities(all, Some(schema)).unwrap() + } +} diff --git a/saturn/src/main.rs b/saturn/src/main.rs index 90e1477c4..e620e974a 100644 --- a/saturn/src/main.rs +++ b/saturn/src/main.rs @@ -1,30 +1,49 @@ +use cedar_policy::Context; use context::AppContext; +use util::EntityUid; mod context; +mod entitystore; +mod objects; +mod util; fn main() { tracing_subscriber::fmt().pretty().init(); let current_dir = env!("CARGO_MANIFEST_DIR"); - let (schema_path, policies_path) = ( + let (schema_path, policies_path, entities_path) = ( format!("{}/{}", current_dir, "mega.cedarschema"), format!("{}/{}", current_dir, "mega_policies.cedar"), + format!("{}/{}", current_dir, "./entities.json"), ); - match AppContext::new("./entities.json", schema_path, policies_path) { + let app_context = match AppContext::new(entities_path, schema_path, policies_path) { Ok(app) => app, Err(e) => { tracing::error!("Failed to load entities, policies, or schema: {e}"); std::process::exit(1); } }; + let anyone: EntityUid = r#"User::"anyone""#.parse().unwrap(); + let resource: EntityUid = r#"Repository::"public""#.parse().unwrap(); + // anyone can view public_repo + assert!(app_context + .is_authorized( + anyone, + r#"Action::"viewRepo""#.parse::().unwrap(), + resource.clone(), + Context::empty() + ) + .is_ok()); } #[cfg(test)] mod test { - use cedar_policy::*; + use cedar_policy::{Authorizer, Context, Entities, PolicySet, Request}; + + use crate::{context::AppContext, util::EntityUid}; #[test] - fn test_cedar() { + fn test_origin_policy() { const POLICY_SRC: &str = r#" permit(principal == User::"alice", action == Action::"view", resource == File::"93"); "#; @@ -60,4 +79,54 @@ mod test { // Should output `Deny` println!("{:?}", answer.decision()); } + + fn load_context() -> AppContext { + AppContext::new( + "./entities.json", + "./mega.cedarschema", + "./mega_policies.cedar", + ) + .unwrap() + } + #[test] + fn test_admin_policy() { + let app_context = load_context(); + let principal: EntityUid = r#"User::"kesha""#.parse().unwrap(); + let resource: EntityUid = r#"Repository::"mega""#.parse().unwrap(); + + // admin can view repo + assert!(app_context + .is_authorized( + principal.clone(), + r#"Action::"viewRepo""#.parse::().unwrap(), + resource.clone(), + Context::empty() + ) + .is_ok()); + // admin can delete repo + assert!(app_context + .is_authorized( + principal, + r#"Action::"deleteRepo""#.parse::().unwrap(), + resource.clone(), + Context::empty() + ) + .is_ok()); + } + + #[test] + fn test_anyone_policy() { + let app_context = load_context(); + let anyone: EntityUid = r#"User::"anyone""#.parse().unwrap(); + let resource: EntityUid = r#"Repository::"public""#.parse().unwrap(); + // anyone can view public_repo + assert!(app_context + .is_authorized( + anyone, + r#"Action::"viewRepo""#.parse::().unwrap(), + resource.clone(), + Context::empty() + ) + .is_ok()); + } } diff --git a/saturn/src/objects.rs b/saturn/src/objects.rs new file mode 100644 index 000000000..9efa04cb5 --- /dev/null +++ b/saturn/src/objects.rs @@ -0,0 +1,147 @@ +use std::collections::HashSet; + +use cedar_policy::{Entity, RestrictedExpression}; +use serde::{Deserialize, Serialize}; + +use crate::util::EntityUid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + euid: EntityUid, + parents: HashSet, +} + +impl From for Entity { + fn from(value: User) -> Entity { + Entity::new_no_attrs( + value.euid.into(), + value.parents.into_iter().map(|euid| euid.into()).collect(), + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserGroup { + euid: EntityUid, + parents: HashSet, +} + +impl From for Entity { + fn from(value: UserGroup) -> Entity { + Entity::new_no_attrs( + value.euid.into(), + value.parents.into_iter().map(|euid| euid.into()).collect(), + ) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repo { + euid: EntityUid, + is_private: bool, + admins: EntityUid, + maintainers: EntityUid, + readers: EntityUid, + triagers: EntityUid, + writers: EntityUid, + parents: HashSet, +} + +impl From for Entity { + fn from(value: Repo) -> Self { + let attrs = [ + ( + "is_private", + RestrictedExpression::new_bool(value.is_private), + ), + ( + "admins", + format!("{}", value.admins.as_ref()).parse().unwrap(), + ), + ( + "maintainers", + format!("{}", value.maintainers.as_ref()).parse().unwrap(), + ), + ( + "readers", + format!("{}", value.readers.as_ref()).parse().unwrap(), + ), + ( + "triagers", + format!("{}", value.triagers.as_ref()).parse().unwrap(), + ), + ( + "writers", + format!("{}", value.writers.as_ref()).parse().unwrap(), + ), + ] + .into_iter() + .map(|(x, v)| (x.into(), v)) + .collect(); + + let parents = value.parents.into_iter().map(|euid| euid.into()).collect(); + + // let euid: EntityUid = value.euid.into(); + Entity::new(value.euid.into(), attrs, parents).unwrap() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MergeRequest { + euid: EntityUid, + repo: EntityUid, + owner: EntityUid, + parents: HashSet, +} + +impl From for Entity { + fn from(value: MergeRequest) -> Entity { + let attrs = [ + ("repo", format!("{}", value.repo.as_ref()).parse().unwrap()), + ( + "owner", + format!("{}", value.owner.as_ref()).parse().unwrap(), + ), + ] + .into_iter() + .map(|(x, v)| (x.into(), v)) + .collect(); + + Entity::new( + value.euid.into(), + attrs, + value.parents.into_iter().map(|euid| euid.into()).collect(), + ) + .unwrap() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Issue { + euid: EntityUid, + repo: EntityUid, + owner: EntityUid, + parents: HashSet, +} + +impl From for Entity { + fn from(value: Issue) -> Entity { + let attrs = [ + ("repo", format!("{}", value.repo.as_ref()).parse().unwrap()), + ( + "owner", + format!("{}", value.owner.as_ref()).parse().unwrap(), + ), + ] + .into_iter() + .map(|(x, v)| (x.into(), v)) + .collect(); + + Entity::new( + value.euid.into(), + attrs, + value.parents.into_iter().map(|euid| euid.into()).collect(), + ) + .unwrap() + } +} diff --git a/saturn/src/util.rs b/saturn/src/util.rs new file mode 100644 index 000000000..63d8f062d --- /dev/null +++ b/saturn/src/util.rs @@ -0,0 +1,111 @@ +use std::{ops::Deref, str::FromStr}; + +use cedar_policy::ParseErrors; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[repr(transparent)] +#[serde(transparent)] +pub struct EntityUid( + #[serde(serialize_with = "serialize_euid")] + #[serde(deserialize_with = "deserialize_euid")] + cedar_policy::EntityUid, +); + +impl AsRef for EntityUid { + fn as_ref(&self) -> &EntityUid { + self + } +} + +impl FromStr for EntityUid { + type Err = ParseErrors; + + fn from_str(s: &str) -> Result { + let e: cedar_policy::EntityUid = s.parse()?; + Ok(e.into()) + } +} + +impl std::fmt::Display for EntityUid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for EntityUid { + fn from(value: cedar_policy::EntityUid) -> Self { + Self(value) + } +} + +impl Deref for EntityUid { + type Target = cedar_policy::EntityUid; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for cedar_policy::EntityUid { + fn from(value: EntityUid) -> Self { + value.0 + } +} + +pub fn serialize_euid(euid: &cedar_policy::EntityUid, s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&format!("{euid}")) +} + +pub fn deserialize_euid<'de, D>(d: D) -> Result +where + D: Deserializer<'de>, +{ + struct Visitor; + + impl<'ide> serde::de::Visitor<'ide> for Visitor { + type Value = cedar_policy::EntityUid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + "Expected string that could be parsed as an EntityUid" + ) + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + let euid = v + .parse() + .map_err(|e| serde::de::Error::custom(format!("{e}")))?; + Ok(euid) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let euid = v + .parse() + .map_err(|e| serde::de::Error::custom(format!("{e}")))?; + Ok(euid) + } + + fn visit_borrowed_str(self, v: &'ide str) -> Result + where + E: serde::de::Error, + { + let euid = v + .parse() + .map_err(|e| serde::de::Error::custom(format!("{e}")))?; + Ok(euid) + } + } + + d.deserialize_str(Visitor) +}