From 6fd6a5a204737f2f8dae362b0b3d40bdad592072 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 2 Aug 2024 17:24:44 +0800 Subject: [PATCH] update policy due to scenario --- ceres/src/protocol/mod.rs | 2 +- saturn/Cargo.toml | 1 - saturn/entities.json | 56 -------- saturn/mega.cedarschema | 7 +- saturn/mega_policies.cedar | 54 ++++---- saturn/src/context.rs | 6 +- saturn/src/entitystore.rs | 8 ++ saturn/src/lib.rs | 175 +++++++++++++++++++++++++ saturn/src/main.rs | 142 -------------------- saturn/src/objects.rs | 15 +-- saturn/test/project/.mega.json | 50 +++++++ saturn/test/project/private/.mega.json | 44 +++++++ 12 files changed, 314 insertions(+), 246 deletions(-) delete mode 100644 saturn/entities.json create mode 100644 saturn/src/lib.rs delete mode 100644 saturn/src/main.rs create mode 100644 saturn/test/project/.mega.json create mode 100644 saturn/test/project/private/.mega.json diff --git a/ceres/src/protocol/mod.rs b/ceres/src/protocol/mod.rs index 0b8f35795..ddc3a36ad 100644 --- a/ceres/src/protocol/mod.rs +++ b/ceres/src/protocol/mod.rs @@ -146,7 +146,7 @@ impl SmartProtocol { pub async fn pack_handler(&self) -> Result, ProtocolError> { let import_dir = self.context.config.monorepo.import_dir.clone(); - if self.path.starts_with(import_dir.clone()) && self.path != import_dir { + if self.path.starts_with(import_dir.clone()) { let storage = self.context.services.git_db_storage.clone(); let path_str = self.path.to_str().unwrap(); let model = storage.find_git_repo_exact_match(path_str).await.unwrap(); diff --git a/saturn/Cargo.toml b/saturn/Cargo.toml index 7492cc1b5..4a9befd10 100644 --- a/saturn/Cargo.toml +++ b/saturn/Cargo.toml @@ -5,7 +5,6 @@ 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 } diff --git a/saturn/entities.json b/saturn/entities.json deleted file mode 100644 index d30f39329..000000000 --- a/saturn/entities.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "users": { - "User::\"kesha\"": { - "euid": "User::\"kesha\"", - "parents": [ - "UserGroup::\"mega_admin\"" - ] - }, - "User::\"alice\"": { - "euid": "User::\"alice\"", - "parents": [ - "UserGroup::\"mega_matainer\"" - ] - } - }, - "repos": { - "Repository::\"mega\"": { - "euid": "Repository::\"mega\"", - "is_private": true, - "admins": "UserGroup::\"mega_admin\"", - "maintainers": "UserGroup::\"mega_matainer\"", - "parents": [] - }, - "Repository::\"public\"": { - "euid": "Repository::\"public\"", - "is_private": false, - "admins": "UserGroup::\"placeholder\"", - "maintainers": "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::\"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 da6564b92..a450680ee 100644 --- a/saturn/mega.cedarschema +++ b/saturn/mega.cedarschema @@ -5,16 +5,15 @@ entity Repository = { "is_private": Bool, "admins": UserGroup, "maintainers": UserGroup, + "readers": UserGroup, }; entity MergeRequest = { "repo": Repository, - "owner": User, }; entity Issue = { "repo": Repository, - "owner": User, }; action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { @@ -24,12 +23,12 @@ action "deleteRepo", "viewRepo", "forkRepo", "pullRepo", "pushRepo" appliesTo { action "createMergeRequest", "editMergeRequest", "deleteMergeRequest", "approveMergeRequest" appliesTo { principal: [User], - resource: [MergeRequest], + resource: [Repository], }; action "openIssue", "assignIssue", "deleteIssue", "editIssue" appliesTo { principal: [User], - resource: [Issue], + resource: [Repository], }; action "addMaintainer", "addAdmin" appliesTo { diff --git a/saturn/mega_policies.cedar b/saturn/mega_policies.cedar index f21d1f12e..da06f2c79 100644 --- a/saturn/mega_policies.cedar +++ b/saturn/mega_policies.cedar @@ -5,55 +5,53 @@ permit ( [Action::"viewRepo", Action::"pullRepo", Action::"forkRepo", - Action::"pushRepo"], + Action::"pushRepo", + Action::"openIssue", + Action::"createMergeRequest"], resource ) unless { resource.is_private }; +//Actions for readers permit ( principal, - action in [Action::"openIssue", Action::"createMergeRequest"], + action in + [Action::"viewRepo", + Action::"pullRepo", + Action::"forkRepo", + Action::"pushRepo", + Action::"openIssue", + Action::"createMergeRequest"], resource ) -unless { resource.repo.is_private }; +when { principal in resource.readers }; //Actions for maintainers permit ( principal, - action in [Action::"editIssue", Action::"editMergeRequest"], - resource -) -when { principal in resource.repo.maintainers || principal == resource.owner }; - -permit ( - principal, - action in [Action::"assignIssue", Action::"approveMergeRequest"], + action in + [Action::"editIssue", + Action::"editMergeRequest", + Action::"assignIssue", + Action::"approveMergeRequest"], resource ) -when { principal in resource.repo.maintainers }; +when { principal in resource.maintainers }; //Actions for admins permit ( principal, action in - [Action::"viewRepo", - Action::"pullRepo", - Action::"forkRepo", - Action::"pushRepo", - Action::"addMaintainer", + [Action::"addMaintainer", Action::"addAdmin", - Action::"deleteRepo"], - resource -) -when { principal in resource.admins }; - -permit ( - principal, - action in - [Action::"openIssue", - Action::"createMergeRequest", + Action::"deleteRepo", Action::"deleteIssue", Action::"deleteMergeRequest"], resource ) -when { principal in resource.repo.admins }; \ No newline at end of file +when { principal in resource.admins }; + + +// root admin can do anything +permit (principal, action, resource) +when { principal == User::"genedna" }; \ No newline at end of file diff --git a/saturn/src/context.rs b/saturn/src/context.rs index fa88c5395..cb010954d 100644 --- a/saturn/src/context.rs +++ b/saturn/src/context.rs @@ -10,7 +10,7 @@ use crate::{entitystore::EntityStore, util::EntityUid}; #[allow(dead_code)] pub struct AppContext { - entities: EntityStore, + pub entities: EntityStore, authorizer: Authorizer, policies: PolicySet, schema: Schema, @@ -44,7 +44,7 @@ pub enum Error { impl AppContext { pub fn new( - entities_path: impl Into, + entities: EntityStore, schema_path: impl Into, policies_path: impl Into, ) -> Result { @@ -53,8 +53,6 @@ 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 policy_src = std::fs::read_to_string(policies_path)?; let policies = policy_src.parse()?; let validator = Validator::new(schema.clone()); diff --git a/saturn/src/entitystore.rs b/saturn/src/entitystore.rs index bd0c0a2b9..a27901281 100644 --- a/saturn/src/entitystore.rs +++ b/saturn/src/entitystore.rs @@ -31,4 +31,12 @@ impl EntityStore { .chain(issues); Entities::from_entities(all, Some(schema)).unwrap() } + + pub fn merge(&mut self, other: EntityStore) { + self.users.extend(other.users); + self.repos.extend(other.repos); + self.merge_requests.extend(other.merge_requests); + self.issues.extend(other.issues); + self.user_groups.extend(other.user_groups); + } } diff --git a/saturn/src/lib.rs b/saturn/src/lib.rs new file mode 100644 index 000000000..695a69fb1 --- /dev/null +++ b/saturn/src/lib.rs @@ -0,0 +1,175 @@ +mod context; +mod entitystore; +mod objects; +mod util; + +#[cfg(test)] +mod test { + use std::fs; + + use cedar_policy::{Authorizer, Context, Entities, PolicySet, Request}; + + use crate::{ + context::{AppContext, Error}, + entitystore::EntityStore, + util::EntityUid, + }; + + #[test] + fn test_without_entity() { + 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()); + } + + fn load_context(entities: EntityStore) -> AppContext { + AppContext::new(entities, "./mega.cedarschema", "./mega_policies.cedar").unwrap() + } + + #[test] + fn test_project_path_policy() { + tracing_subscriber::fmt().pretty().init(); + let entities_path = "./test/project/.mega.json"; + let entities_file = fs::File::open(entities_path).unwrap(); + let entities = serde_json::from_reader(entities_file).unwrap(); + + let app_context = load_context(entities); + let admin: EntityUid = r#"User::"benjamin.747""#.parse().unwrap(); + let anyone: EntityUid = r#"User::"anyone""#.parse().unwrap(); + let resource: EntityUid = r#"Repository::"project""#.parse().unwrap(); + + // admin can view repo + assert!(app_context + .is_authorized( + &admin, + r#"Action::"viewRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_ok()); + // admin can delete repo + assert!(app_context + .is_authorized( + &admin, + r#"Action::"deleteRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_ok()); + + // anyone can view public_repo + assert!(app_context + .is_authorized( + &anyone, + r#"Action::"viewRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_ok()); + + assert!(app_context + .is_authorized( + &anyone, + r#"Action::"openIssue""#.parse::().unwrap(), + &resource, + Context::empty(), + ) + .is_ok()); + + // normal user can't assign issue + assert!(app_context + .is_authorized( + &anyone, + r#"Action::"assignIssue""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_err_and(|e| matches!(e, Error::AuthDenied(_)))); + assert!(app_context + .is_authorized( + &anyone, + r#"Action::"approveMergeRequest""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_err_and(|e| matches!(e, Error::AuthDenied(_)))); + } + + #[test] + fn test_private_path_policy() { + tracing_subscriber::fmt().pretty().init(); + let parent_entities_file = fs::File::open("./test/project/.mega.json").unwrap(); + let parent_entities: EntityStore = serde_json::from_reader(parent_entities_file).unwrap(); + + let entities_file = fs::File::open("./test/project/private/.mega.json").unwrap(); + let mut entities: EntityStore = serde_json::from_reader(entities_file).unwrap(); + + entities.merge(parent_entities); + + let app_context = load_context(entities); + let p_admin: EntityUid = r#"User::"benjamin.747""#.parse().unwrap(); + let admin: EntityUid = r#"User::"private""#.parse().unwrap(); + let anyone: EntityUid = r#"User::"anyone""#.parse().unwrap(); + let resource: EntityUid = r#"Repository::"project/private""#.parse().unwrap(); + + // admin under project should also have permisisons + assert!(app_context + .is_authorized( + &p_admin, + r#"Action::"viewRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_ok()); + + assert!(app_context + .is_authorized( + &admin, + r#"Action::"viewRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_ok()); + + // not public, should deny + assert!(app_context + .is_authorized( + &anyone, + r#"Action::"viewRepo""#.parse::().unwrap(), + &resource, + Context::empty() + ) + .is_err_and(|e| matches!(e, Error::AuthDenied(_)))); + } +} diff --git a/saturn/src/main.rs b/saturn/src/main.rs deleted file mode 100644 index 42fe996e0..000000000 --- a/saturn/src/main.rs +++ /dev/null @@ -1,142 +0,0 @@ -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, entities_path) = ( - format!("{}/{}", current_dir, "mega.cedarschema"), - format!("{}/{}", current_dir, "mega_policies.cedar"), - format!("{}/{}", current_dir, "./entities.json"), - ); - - 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::{Authorizer, Context, Entities, PolicySet, Request}; - - use crate::{context::AppContext, util::EntityUid}; - - #[test] - fn test_origin_policy() { - 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()); - } - - fn load_context() -> AppContext { - tracing_subscriber::fmt().pretty().init(); - 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.clone(), - r#"Action::"viewRepo""#.parse::().unwrap(), - resource.clone(), - Context::empty() - ) - .is_ok()); - // anyone can not view mega - assert!(app_context - .is_authorized( - anyone, - r#"Action::"viewRepo""#.parse::().unwrap(), - r#"Repository::"mega""#.parse::().unwrap(), - Context::empty() - ) - .is_err()); - } -} diff --git a/saturn/src/objects.rs b/saturn/src/objects.rs index fa932d880..006d7b593 100644 --- a/saturn/src/objects.rs +++ b/saturn/src/objects.rs @@ -41,6 +41,7 @@ pub struct Repo { is_private: bool, admins: EntityUid, maintainers: EntityUid, + readers: EntityUid, parents: HashSet, } @@ -58,6 +59,10 @@ impl From for Entity { ( "maintainers", format!("{}", value.maintainers.as_ref()).parse().unwrap(), + ), + ( + "readers", + format!("{}", value.readers.as_ref()).parse().unwrap(), ) ] .into_iter() @@ -74,7 +79,6 @@ impl From for Entity { pub struct MergeRequest { euid: EntityUid, repo: EntityUid, - owner: EntityUid, parents: HashSet, } @@ -82,10 +86,6 @@ 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)) @@ -104,7 +104,6 @@ impl From for Entity { pub struct Issue { euid: EntityUid, repo: EntityUid, - owner: EntityUid, parents: HashSet, } @@ -112,10 +111,6 @@ 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)) diff --git a/saturn/test/project/.mega.json b/saturn/test/project/.mega.json new file mode 100644 index 000000000..20e540cce --- /dev/null +++ b/saturn/test/project/.mega.json @@ -0,0 +1,50 @@ +{ + "users": { + "User::\"benjamin.747\"": { + "euid": "User::\"benjamin.747\"", + "parents": [ + "UserGroup::\"admin\"" + ] + }, + "User::\"genedna\"": { + "euid": "User::\"genedna\"", + "parents": [ + "UserGroup::\"admin\"" + ] + } + }, + "repos": { + "Repository::\"project\"": { + "euid": "Repository::\"project\"", + "is_private": false, + "admins": "UserGroup::\"admin\"", + "maintainers": "UserGroup::\"matainer\"", + "readers": "UserGroup::\"reader\"", + "parents": [] + } + }, + "user_groups": { + "UserGroup::\"admin\"": { + "euid": "UserGroup::\"admin\"", + "parents": [ + "UserGroup::\"matainer\"" + ] + }, + "UserGroup::\"matainer\"": { + "euid": "UserGroup::\"matainer\"", + "parents": [ + "UserGroup::\"reader\"" + ] + }, + "UserGroup::\"reader\"": { + "euid": "UserGroup::\"reader\"", + "parents": [] + } + }, + "merge_requests": { + + }, + "issues": { + + } +} \ No newline at end of file diff --git a/saturn/test/project/private/.mega.json b/saturn/test/project/private/.mega.json new file mode 100644 index 000000000..617684ea7 --- /dev/null +++ b/saturn/test/project/private/.mega.json @@ -0,0 +1,44 @@ +{ + "users": { + "User::\"private\"": { + "euid": "User::\"private\"", + "parents": [ + "UserGroup::\"admin\"" + ] + } + }, + "repos": { + "Repository::\"project/private\"": { + "euid": "Repository::\"project/private\"", + "is_private": true, + "admins": "UserGroup::\"admin\"", + "maintainers": "UserGroup::\"matainer\"", + "readers": "UserGroup::\"reader\"", + "parents": [] + } + }, + "user_groups": { + "UserGroup::\"admin\"": { + "euid": "UserGroup::\"admin\"", + "parents": [ + "UserGroup::\"matainer\"" + ] + }, + "UserGroup::\"matainer\"": { + "euid": "UserGroup::\"matainer\"", + "parents": [ + "UserGroup::\"reader\"" + ] + }, + "UserGroup::\"reader\"": { + "euid": "UserGroup::\"reader\"", + "parents": [] + } + }, + "merge_requests": { + + }, + "issues": { + + } +} \ No newline at end of file