From 8f5026fdc857068bf0c4756449805800a29cee59 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Fri, 11 Oct 2024 17:31:25 +0800 Subject: [PATCH 1/2] add .mega_cedar.json file in init process --- ceres/src/api_service/mono_api_service.rs | 3 - common/src/config.rs | 11 +++- docker/config.toml | 10 ++- gateway/src/https_server.rs | 3 +- jupiter/Cargo.toml | 1 + jupiter/src/storage/mono_storage.rs | 5 +- jupiter/src/utils/converter.rs | 74 +++++++++++------------ mega/config.toml | 9 ++- mega/src/commands/init.rs | 26 -------- mega/src/commands/mod.rs | 4 +- mono/config.toml | 9 ++- mono/src/commands/init.rs | 27 --------- mono/src/commands/mod.rs | 4 +- mono/src/git_protocol/ssh.rs | 2 +- mono/src/server/https_server.rs | 2 +- mono/src/server/ssh_server.rs | 3 +- saturn/Cargo.toml | 2 +- saturn/src/entitystore.rs | 63 +++++++++++++++++++ saturn/src/lib.rs | 14 ++--- 19 files changed, 150 insertions(+), 122 deletions(-) delete mode 100644 mega/src/commands/init.rs delete mode 100644 mono/src/commands/init.rs diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index c32b04747..859a21987 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -195,9 +195,6 @@ impl ApiHandler for MonoApiService { } impl MonoApiService { - pub async fn init_monorepo(&self) { - self.context.services.mono_storage.init_monorepo().await - } pub async fn mr_list(&self, status: &str) -> Result, MegaError> { let status = if status == "open" { diff --git a/common/src/config.rs b/common/src/config.rs index 61b26b131..0dc70931b 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -210,6 +210,8 @@ impl Default for StorageConfig { pub struct MonoConfig { pub import_dir: PathBuf, pub disable_http_push: bool, + pub admin: String, + pub root_dirs: Vec, } impl Default for MonoConfig { @@ -217,6 +219,13 @@ impl Default for MonoConfig { Self { import_dir: PathBuf::from("/third-part"), disable_http_push: false, + admin: String::from("admin"), + root_dirs: vec![ + "third-part".to_string(), + "project".to_string(), + "doc".to_string(), + "release".to_string(), + ], } } } @@ -237,7 +246,7 @@ impl Default for PackConfig { pack_decode_cache_path: PathBuf::from("/tmp/.mega/cache"), clean_cache_after_decode: true, channel_message_size: 1_000_000, - maximum_pack_size: 1, + maximum_pack_size: 4, } } } diff --git a/docker/config.toml b/docker/config.toml index 1469cd59f..ce5826380 100644 --- a/docker/config.toml +++ b/docker/config.toml @@ -53,8 +53,15 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com" ## Mega treats files under this directory as import repo and other directories as monorepo import_dir = "/third-part" +# The current mono Git HTTP server does not support authentication, so we provided a disabled operations here disable_http_push = false +# Set System Admin in directory init, replace the admin's github username here +admin = "admin" + +# Set serveral root dirs in directory init +root_dirs = ["third-part", "project", "doc", "release"] + [pack] # The maximum memory used by decode, Unit is GB pack_decode_mem_size = 4 @@ -68,8 +75,7 @@ clean_cache_after_decode = true channel_message_size = 1_000_000 # Maximum pack size, unit GB, enforces to use LFS off the limit -maximum_pack_size = 1 - +maximum_pack_size = 4 [lfs] # LFS Server url diff --git a/gateway/src/https_server.rs b/gateway/src/https_server.rs index ab990c5e6..e4f72d8dd 100644 --- a/gateway/src/https_server.rs +++ b/gateway/src/https_server.rs @@ -120,7 +120,7 @@ pub async fn app( ztm: ZtmOptions, ) -> Router { let context = Context::new(config.clone()).await; - context.services.mono_storage.init_monorepo().await; + context.services.mono_storage.init_monorepo(&config.monorepo).await; let state = AppState { host, port, @@ -215,7 +215,6 @@ pub fn check_run_with_ztm(config: Config, ztm: ZtmOptions, http_port: u16) { thread::sleep(time::Duration::from_secs(3)); tokio::spawn(async move { let context = Context::new(config.clone()).await; - context.services.mono_storage.init_monorepo().await; cache_public_repository(bootstrap_node, context, ztm_agent).await }); } diff --git a/jupiter/Cargo.toml b/jupiter/Cargo.toml index cad0c26ed..b40d839c7 100644 --- a/jupiter/Cargo.toml +++ b/jupiter/Cargo.toml @@ -14,6 +14,7 @@ path = "src/lib.rs" callisto = { workspace = true } common = { workspace = true } mercury = { workspace = true } +saturn = { workspace = true } sea-orm = { workspace = true, features = [ "sqlx-postgres", diff --git a/jupiter/src/storage/mono_storage.rs b/jupiter/src/storage/mono_storage.rs index 11b9a7ad3..0d092c65f 100644 --- a/jupiter/src/storage/mono_storage.rs +++ b/jupiter/src/storage/mono_storage.rs @@ -11,6 +11,7 @@ use callisto::db_enums::{ConvType, MergeStatus}; use callisto::{ mega_blob, mega_commit, mega_mr, mega_mr_conv, mega_refs, mega_tag, mega_tree, raw_blob, }; +use common::config::MonoConfig; use common::errors::MegaError; use common::utils::generate_id; use mercury::internal::object::MegaObjectModel; @@ -267,12 +268,12 @@ impl MonoStorage { Ok(()) } - pub async fn init_monorepo(&self) { + pub async fn init_monorepo(&self, mono_config: &MonoConfig) { if self.get_ref("/").await.unwrap().is_some() { tracing::info!("Monorepo Directory Already Inited, skip init process!"); return; } - let converter = MegaModelConverter::init(); + let converter = MegaModelConverter::init(mono_config); let commit: mega_commit::Model = converter.commit.into(); mega_commit::Entity::insert(commit.into_active_model()) .exec(self.get_connection()) diff --git a/jupiter/src/utils/converter.rs b/jupiter/src/utils/converter.rs index 9385c51ed..6768caf56 100644 --- a/jupiter/src/utils/converter.rs +++ b/jupiter/src/utils/converter.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::collections::HashMap; use callisto::{mega_blob, mega_refs, mega_tree, raw_blob}; +use common::config::MonoConfig; use common::utils::generate_id; use mercury::hash::SHA1; use mercury::internal::object::blob::Blob; @@ -21,40 +22,35 @@ pub fn generate_git_keep_with_timestamp() -> Blob { Blob::from_content(&git_keep_content) } -pub fn init_trees(git_keep: &Blob) -> (HashMap, Tree) { - let tree_item = TreeItem { - mode: TreeItemMode::Blob, - id: git_keep.id, - name: String::from(".gitkeep"), - }; - let tree = Tree::from_tree_items(vec![tree_item.clone()]).unwrap(); - - let root_items = vec![ - TreeItem { - mode: TreeItemMode::Tree, - id: tree.id, - name: String::from("third-part"), - }, - TreeItem { - mode: TreeItemMode::Tree, - id: tree.id, - name: String::from("project"), - }, - TreeItem { - mode: TreeItemMode::Tree, - id: tree.id, - name: String::from("doc"), - }, - TreeItem { +pub fn init_trees(mono_config: &MonoConfig) -> (HashMap, HashMap, Tree) { + let mut root_items = Vec::new(); + let mut trees = Vec::new(); + let mut blobs = Vec::new(); + for dir in mono_config.root_dirs.clone() { + let entity_str = saturn::entitystore::generate_entity(&mono_config.admin, &dir).unwrap(); + let blob = Blob::from_content(&entity_str); + + let tree_item = TreeItem { + mode: TreeItemMode::Blob, + id: blob.id, + name: String::from(".mega_cedar.json"), + }; + let tree = Tree::from_tree_items(vec![tree_item.clone()]).unwrap(); + root_items.push(TreeItem { mode: TreeItemMode::Tree, id: tree.id, - name: String::from("release"), - }, - ]; + name: dir, + }); + trees.push(tree); + blobs.push(blob); + } let root = Tree::from_tree_items(root_items).unwrap(); - let trees = vec![tree]; - (trees.into_iter().map(|x| (x.id, x)).collect(), root) + ( + trees.into_iter().map(|x| (x.id, x)).collect(), + blobs.into_iter().map(|x| (x.id, x)).collect(), + root, + ) } pub struct MegaModelConverter { @@ -102,12 +98,9 @@ impl MegaModelConverter { } } - pub fn init() -> Self { - let git_keep = generate_git_keep(); - let (tree_maps, root_tree) = init_trees(&git_keep); + pub fn init(mono_config: &MonoConfig) -> Self { + let (tree_maps, blob_maps, root_tree) = init_trees(mono_config); let commit = Commit::from_tree_id(root_tree.id, vec![], "Init Mega Directory"); - let mut blob_maps = HashMap::new(); - blob_maps.insert(git_keep.id, git_keep); let mega_ref = mega_refs::Model { id: generate_id(), @@ -138,19 +131,22 @@ mod test { use std::str::FromStr; + use common::config::MonoConfig; use mercury::{hash::SHA1, internal::object::commit::Commit}; use crate::utils::converter::MegaModelConverter; #[test] pub fn test_init_mega_dir() { - let converter = MegaModelConverter::init(); + let mono_config = MonoConfig::default(); + let converter = MegaModelConverter::init(&mono_config); let mega_trees = converter.mega_trees.borrow().clone(); let mega_blobs = converter.mega_blobs.borrow().clone(); let raw_blob = converter.raw_blobs.borrow().clone(); - assert_eq!(mega_trees.len(), 2); - assert_eq!(mega_blobs.len(), 1); - assert_eq!(raw_blob.len(), 1); + let dir_nums = mono_config.root_dirs.len(); + assert_eq!(mega_trees.len(), dir_nums + 1); + assert_eq!(mega_blobs.len(), dir_nums); + assert_eq!(raw_blob.len(), dir_nums); } #[test] diff --git a/mega/config.toml b/mega/config.toml index 87d8b1d2c..3adfdc102 100644 --- a/mega/config.toml +++ b/mega/config.toml @@ -53,8 +53,15 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com" ## Mega treats files under this directory as import repo and other directories as monorepo import_dir = "/third-part" +# The current mono Git HTTP server does not support authentication, so we provided a disabled operations here disable_http_push = false +# Set System Admin in directory init, replace the admin's github username here +admin = "admin" + +# Set serveral root dirs in directory init +root_dirs = ["third-part", "project", "doc", "release"] + [pack] # The maximum memory used by decode, Unit is GB pack_decode_mem_size = 4 @@ -68,7 +75,7 @@ clean_cache_after_decode = true channel_message_size = 1_000_000 # Maximum pack size, unit GB, enforces to use LFS off the limit -maximum_pack_size = 1 +maximum_pack_size = 4 [lfs] # LFS Server url diff --git a/mega/src/commands/init.rs b/mega/src/commands/init.rs deleted file mode 100644 index e3ba5d8d6..000000000 --- a/mega/src/commands/init.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! This module is responsible for handling the 'init' command. -//! It initializes the mega monorepo structure. -//! -//! - -use clap::{ArgMatches, Command}; - -use common::{config::Config, errors::MegaResult}; -use jupiter::context::Context; - -// This function generates the CLI for the 'init' command. -pub fn cli() -> Command { - Command::new("init").about("Initialize the mega monorepo structure") -} - -// This function executes the 'init' command. -// It calls the `init_monorepo` function from the `gateway` module. -#[tokio::main] -pub(crate) async fn exec(config: Config, _: &ArgMatches) -> MegaResult { - let context = Context::new(config).await; - context.services.mono_storage.init_monorepo().await; - Ok(()) -} - -#[cfg(test)] -mod tests {} diff --git a/mega/src/commands/mod.rs b/mega/src/commands/mod.rs index a88ae6298..5caad768f 100644 --- a/mega/src/commands/mod.rs +++ b/mega/src/commands/mod.rs @@ -1,4 +1,3 @@ -mod init; mod service; use clap::{ArgMatches, Command}; @@ -6,13 +5,12 @@ use clap::{ArgMatches, Command}; use common::{config::Config, errors::MegaResult}; pub fn builtin() -> Vec { - vec![service::cli(), init::cli()] + vec![service::cli()] } pub(crate) fn builtin_exec(cmd: &str) -> Option MegaResult> { let f = match cmd { "service" => service::exec, - "init" => init::exec, _ => return None, }; diff --git a/mono/config.toml b/mono/config.toml index 4164f0223..69134d013 100644 --- a/mono/config.toml +++ b/mono/config.toml @@ -53,8 +53,15 @@ obs_endpoint = "https://obs.cn-east-3.myhuaweicloud.com" ## Mega treats files under this directory as import repo and other directories as monorepo import_dir = "/third-part" +# The current mono Git HTTP server does not support authentication, so we provided a disabled operations here disable_http_push = false +# Set System Admin in directory init, replace the admin's github username here +admin = "admin" + +# Set serveral root dirs in directory init +root_dirs = ["third-part", "project", "doc", "release"] + [pack] # The maximum memory used by decode, Unit is GB pack_decode_mem_size = 4 @@ -68,7 +75,7 @@ clean_cache_after_decode = true channel_message_size = 1_000_000 # Maximum pack size, unit GB, enforces to use LFS off the limit -maximum_pack_size = 1 +maximum_pack_size = 4 [lfs] # LFS Server url diff --git a/mono/src/commands/init.rs b/mono/src/commands/init.rs deleted file mode 100644 index 7d51ab5ca..000000000 --- a/mono/src/commands/init.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! This module is responsible for handling the 'init' command. -//! It initializes the monorepo structure. -//! -//! - -use clap::{ArgMatches, Command}; - -use common::{config::Config, errors::MegaResult}; -use jupiter::context::Context; - -// This function generates the CLI for the 'init' command. -pub fn cli() -> Command { - Command::new("init").about("Initialize the monorepo structure") -} - -// This function executes the 'init' command. -// It calls the `init_monorepo` function from the `gateway` module. -#[tokio::main] -pub(crate) async fn exec(config: Config, _: &ArgMatches) -> MegaResult { - - let context = Context::new(config).await; - context.services.mono_storage.init_monorepo().await; - Ok(()) -} - -#[cfg(test)] -mod tests {} diff --git a/mono/src/commands/mod.rs b/mono/src/commands/mod.rs index b30f432a1..e6a347d54 100644 --- a/mono/src/commands/mod.rs +++ b/mono/src/commands/mod.rs @@ -1,4 +1,3 @@ -pub mod init; pub mod service; use clap::{ArgMatches, Command}; @@ -7,13 +6,12 @@ use common::{config::Config, errors::MegaResult}; pub fn builtin() -> Vec { - vec![service::cli(), init::cli()] + vec![service::cli()] } pub(crate) fn builtin_exec(cmd: &str) -> Option MegaResult> { let f = match cmd { "service" => service::exec, - "init" => init::exec, _ => return None, }; diff --git a/mono/src/git_protocol/ssh.rs b/mono/src/git_protocol/ssh.rs index fec3f0186..d6de66baf 100644 --- a/mono/src/git_protocol/ssh.rs +++ b/mono/src/git_protocol/ssh.rs @@ -111,7 +111,7 @@ impl server::Handler for SshServer { let mut header = HashMap::new(); header.insert("Accept".to_string(), "application/vnd.git-lfs".to_string()); let link = Link { - href: "http://localhost:8000".to_string(), + href: smart_protocol.context.config.lfs.url, header, expires_at: { let expire_time: DateTime = diff --git a/mono/src/server/https_server.rs b/mono/src/server/https_server.rs index 8cfd1f39b..8e72b7416 100644 --- a/mono/src/server/https_server.rs +++ b/mono/src/server/https_server.rs @@ -135,7 +135,7 @@ pub async fn start_http(config: Config, options: HttpOptions) { /// - POST end of `Regex::new(r"/git-receive-pack$")` pub async fn app(config: Config, host: String, port: u16, common: CommonOptions) -> Router { let context = Context::new(config.clone()).await; - context.services.mono_storage.init_monorepo().await; + context.services.mono_storage.init_monorepo(&config.monorepo).await; let state = AppState { host, port, diff --git a/mono/src/server/ssh_server.rs b/mono/src/server/ssh_server.rs index b849daa53..392f02f43 100644 --- a/mono/src/server/ssh_server.rs +++ b/mono/src/server/ssh_server.rs @@ -58,7 +58,8 @@ pub async fn start_server(config: Config, command: &SshOptions) { common: CommonOptions { host, .. }, custom: SshCustom { ssh_port }, } = command; - let context = Context::new(config).await; + let context = Context::new(config.clone()).await; + context.services.mono_storage.init_monorepo(&config.monorepo).await; let mut ssh_server = SshServer { client_pubkey, clients: Arc::new(Mutex::new(HashMap::new())), diff --git a/saturn/Cargo.toml b/saturn/Cargo.toml index cd86c6448..41c95f535 100644 --- a/saturn/Cargo.toml +++ b/saturn/Cargo.toml @@ -13,4 +13,4 @@ tracing-subscriber = { workspace = true } thiserror = { workspace = true } itertools = "0.13.0" -cedar-policy = "4.0.0" +cedar-policy = "4.2.1" diff --git a/saturn/src/entitystore.rs b/saturn/src/entitystore.rs index 8d8a0500b..c3cee68a6 100644 --- a/saturn/src/entitystore.rs +++ b/saturn/src/entitystore.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use cedar_policy::{Entities, Schema}; use serde::{Deserialize, Serialize}; +use serde_json::{ json, to_string_pretty}; use crate::{ objects::{Issue, MergeRequest, Repo, User, UserGroup}, @@ -42,3 +43,65 @@ impl EntityStore { self.user_groups.extend(other.user_groups); } } + +pub fn generate_entity(user: &str, repo: &str) -> Result> { + let mut json_data = json!({ + "users": { + }, + "repos": { + }, + "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": { + } + }); + + if let Some(users) = json_data.get_mut("users") { + if let Some(users_map) = users.as_object_mut() { + users_map.insert( + format!("User::\"{}\"", user), + json!({ + "euid": format!("User::\"{}\"", user), + "parents": [ + "UserGroup::\"admin\"" + ] + }), + ); + } + } + + if let Some(repos) = json_data.get_mut("repos") { + if let Some(repos_map) = repos.as_object_mut() { + repos_map.insert( + format!("Repository::\"{}\"", repo), + json!({ + "euid": format!("Repository::\"{}\"", repo), + "is_private": true, + "admins": "UserGroup::\"admin\"", + "maintainers": "UserGroup::\"matainer\"", + "readers": "UserGroup::\"reader\"", + "parents": [] + }), + ); + } + } + Ok(to_string_pretty(&json_data)?) +} diff --git a/saturn/src/lib.rs b/saturn/src/lib.rs index 1d3d62713..ad773cba7 100644 --- a/saturn/src/lib.rs +++ b/saturn/src/lib.rs @@ -1,5 +1,5 @@ mod context; -mod entitystore; +pub mod entitystore; mod objects; mod util; @@ -19,9 +19,7 @@ mod test { fn init_tracing() { INIT.call_once(|| { - tracing_subscriber::fmt() - .pretty() - .init(); + tracing_subscriber::fmt().pretty().init(); }); } @@ -143,14 +141,14 @@ mod test { 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(); + let private_project: 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, + &private_project, Context::empty() ) .is_ok()); @@ -159,7 +157,7 @@ mod test { .is_authorized( &admin, r#"Action::"viewRepo""#.parse::().unwrap(), - &resource, + &private_project, Context::empty() ) .is_ok()); @@ -169,7 +167,7 @@ mod test { .is_authorized( &anyone, r#"Action::"viewRepo""#.parse::().unwrap(), - &resource, + &private_project, Context::empty() ) .is_err_and(|e| matches!(e, Error::AuthDenied(_)))); From 19278beaefc24648cda37e583611e0f904a58789 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Mon, 14 Oct 2024 18:49:31 +0800 Subject: [PATCH 2/2] check permission in api request --- Cargo.toml | 1 + ceres/src/api_service/mod.rs | 21 +++++--- ceres/src/api_service/mono_api_service.rs | 65 ++++++++++------------- jupiter/src/utils/converter.rs | 3 +- mono/Cargo.toml | 2 + mono/src/api/api_router.rs | 2 +- mono/src/api/mod.rs | 58 ++++++++++++++++++++ mono/src/api/mr_router.rs | 35 ++++++++---- mono/src/api/oauth/mod.rs | 2 +- mono/src/api/user/model.rs | 9 +++- mono/src/api/user/user_router.rs | 31 ++++++++++- moon/src/app/api/mr/[id]/merge/route.ts | 11 ++-- saturn/Cargo.toml | 2 +- saturn/src/context.rs | 7 +-- saturn/src/entitystore.rs | 11 +++- saturn/src/lib.rs | 54 ++++++++++++++++--- saturn/test/project/private/.mega.json | 4 +- 17 files changed, 241 insertions(+), 77 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2d6fa8a9..e1d42ce80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,3 +83,4 @@ git2 = "0.19.0" tempfile = "3.13.0" home = "0.5.9" ring = "0.17.8" +cedar-policy = "4.2.1" diff --git a/ceres/src/api_service/mod.rs b/ceres/src/api_service/mod.rs index 206fe7d12..dff6a4e8f 100644 --- a/ceres/src/api_service/mod.rs +++ b/ceres/src/api_service/mod.rs @@ -79,19 +79,20 @@ pub trait ApiHandler: Send + Sync { target: &TreeItem, ) -> Commit; - async fn get_blob_as_string(&self, file_path: PathBuf) -> Result { + async fn get_blob_as_string(&self, file_path: PathBuf) -> Result, GitError> { let filename = file_path.file_name().unwrap().to_str().unwrap(); let parent = file_path.parent().unwrap(); - let mut plain_text = String::new(); if let Some(tree) = self.search_tree_by_path(parent).await? { if let Some(item) = tree.tree_items.into_iter().find(|x| x.name == filename) { - plain_text = match self.get_raw_blob_by_hash(&item.id.to_plain_str()).await { - Ok(Some(model)) => String::from_utf8(model.data.unwrap()).unwrap(), - _ => String::new(), + match self.get_raw_blob_by_hash(&item.id.to_plain_str()).await { + Ok(Some(model)) => { + return Ok(Some(String::from_utf8(model.data.unwrap()).unwrap())) + } + _ => return Ok(None), }; } } - return Ok(plain_text); + return Ok(None); } async fn get_latest_commit(&self, path: PathBuf) -> Result { @@ -160,6 +161,7 @@ pub trait ApiHandler: Send + Sync { .map(|x| (x.id.to_plain_str(), x)) .collect(); + let root_commit: Option = None; for item in tree.tree_items { let mut info: TreeCommitItem = item.clone().into(); if let Some(commit_id) = item_to_commit.get(&item.id.to_plain_str()) { @@ -167,8 +169,13 @@ pub trait ApiHandler: Send + Sync { commit } else { tracing::warn!("failed fecth commit: {}", commit_id); + let root_commit = if let Some(ref root_commit) = root_commit { + root_commit.clone() + } else { + self.get_root_commit().await + }; &self - .traverse_commit_history(&path, self.get_root_commit().await, &item) + .traverse_commit_history(&path, root_commit, &item) .await }; info.oid = commit.id.to_plain_str(); diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index 859a21987..992221267 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -195,7 +195,6 @@ impl ApiHandler for MonoApiService { } impl MonoApiService { - pub async fn mr_list(&self, status: &str) -> Result, MegaError> { let status = if status == "open" { vec![MergeStatus::Open] @@ -276,48 +275,42 @@ impl MonoApiService { Err(MegaError::with_message("Can not find related MR by id")) } - pub async fn merge_mr(&self, mr_link: &str) -> Result<(), MegaError> { + pub async fn merge_mr(&self, mr: &mut MergeRequest) -> Result<(), MegaError> { let storage = self.context.services.mono_storage.clone(); - if let Some(model) = storage.get_open_mr_by_link(mr_link).await.unwrap() { - let mut mr: MergeRequest = model.into(); - let refs = storage.get_ref(&mr.path).await.unwrap().unwrap(); - - if mr.from_hash == refs.ref_commit_hash { - // update mr - mr.merge(); - storage.update_mr(mr.clone().into()).await.unwrap(); + let refs = storage.get_ref(&mr.path).await.unwrap().unwrap(); - let commit: Commit = storage - .get_commit_by_hash(&mr.to_hash) + if mr.from_hash == refs.ref_commit_hash { + // update mr + mr.merge(); + let commit: Commit = storage + .get_commit_by_hash(&mr.to_hash) + .await + .unwrap() + .unwrap() + .into(); + // add conversation + storage + .add_mr_conversation(&mr.mr_link, 0, ConvType::Merged, None) + .await + .unwrap(); + if mr.path != "/" { + let path = PathBuf::from(mr.path.clone()); + // beacuse only parent tree is needed so we skip current directory + let (tree_vec, _) = self + .search_tree_for_update(path.parent().unwrap()) .await - .unwrap() - .unwrap() - .into(); - - // add conversation - storage - .add_mr_conversation(&mr.mr_link, 0, ConvType::Merged, None) + .unwrap(); + self.update_parent_tree(path, tree_vec, commit) .await .unwrap(); - if mr.path != "/" { - let path = PathBuf::from(mr.path.clone()); - // beacuse only parent tree is needed so we skip current directory - let (tree_vec, _) = self - .search_tree_for_update(path.parent().unwrap()) - .await - .unwrap(); - self.update_parent_tree(path, tree_vec, commit) - .await - .unwrap(); - // remove refs start with path - storage.remove_refs(&mr.path).await.unwrap(); - // TODO: self.clean_dangling_commits().await; - } - } else { - return Err(MegaError::with_message("ref hash conflict")); + // remove refs start with path + storage.remove_refs(&mr.path).await.unwrap(); + // TODO: self.clean_dangling_commits().await; } + // update mr status last + storage.update_mr(mr.clone().into()).await.unwrap(); } else { - return Err(MegaError::with_message("Invalid mr id")); + return Err(MegaError::with_message("ref hash conflict")); } Ok(()) } diff --git a/jupiter/src/utils/converter.rs b/jupiter/src/utils/converter.rs index 6768caf56..736e3538f 100644 --- a/jupiter/src/utils/converter.rs +++ b/jupiter/src/utils/converter.rs @@ -27,7 +27,8 @@ pub fn init_trees(mono_config: &MonoConfig) -> (HashMap, HashMap CommonResult::success(Some(data)), + Ok(data) => CommonResult::success(data), Err(err) => CommonResult::failed(&err.to_string()), }; Ok(Json(res)) diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index efb15d43a..c87100c72 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -77,3 +77,61 @@ impl MonoApiServiceState { })) } } +pub mod util { + use std::path::PathBuf; + + use axum::extract::State; + + use cedar_policy::Context; + use ceres::api_service::ApiHandler; + use saturn::{context::CedarContext, entitystore::EntityStore, util::EntityUid, ActionEnum}; + + use crate::api::MonoApiServiceState; + + pub async fn get_entitystore(path: PathBuf, state: State) -> EntityStore { + let mut entities: EntityStore = EntityStore::new(); + for component in path.ancestors() { + if component != std::path::Path::new("/") { + let cedar_path = component.join(".mega_cedar.json"); + let entity_str = state + .monorepo() + .get_blob_as_string(cedar_path) + .await + .unwrap(); + if let Some(entity_str) = entity_str { + entities.merge(serde_json::from_str(&entity_str).unwrap()); + } + } + } + entities + } + + pub async fn check_permissions( + username: &str, + path: &str, + operation: ActionEnum, + state: State, + ) -> Result<(), saturn::context::Error> { + let entities = get_entitystore(path.into(), state).await; + let crate_root = env!("CARGO_MANIFEST_DIR"); + let cedar_context = CedarContext::new( + entities, + format!("{}/../saturn/mega.cedarschema", crate_root), + format!("{}/../saturn/mega_policies.cedar", crate_root), + ) + .unwrap(); + cedar_context.is_authorized( + format!(r#"User::"{}""#, username) + .to_owned() + .parse::() + .unwrap(), + format!(r#"Action::"{}""#, operation) + .parse::() + .unwrap(), + format!(r#"Repository::"{}""#, path) + .parse::() + .unwrap(), + Context::empty(), + ) + } +} diff --git a/mono/src/api/mr_router.rs b/mono/src/api/mr_router.rs index e01bcb06e..1a10ad3db 100644 --- a/mono/src/api/mr_router.rs +++ b/mono/src/api/mr_router.rs @@ -10,9 +10,12 @@ use bytes::Bytes; use ceres::model::mr::{MRDetail, MrInfoItem}; use common::model::CommonResult; +use saturn::ActionEnum; use taurus::event::api_request::{ApiRequestEvent, ApiType}; use crate::api::error::ApiError; +use crate::api::oauth::model::LoginUser; +use crate::api::util; use crate::api::MonoApiServiceState; pub fn routers() -> Router { @@ -26,18 +29,32 @@ pub fn routers() -> Router { } async fn merge( + user: LoginUser, Path(mr_link): Path, state: State, ) -> Result>, ApiError> { ApiRequestEvent::notify(ApiType::MergeRequest, &state.0.context.config); - - let res = state.monorepo().merge_mr(&mr_link).await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), - }; - ApiRequestEvent::notify(ApiType::MergeDone, &state.0.context.config); - Ok(Json(res)) + let storage = state.context.services.mono_storage.clone(); + if let Some(model) = storage.get_open_mr_by_link(&mr_link).await.unwrap() { + let path = model.path.clone(); + assert!(util::check_permissions( + &user.name, + // "admin", + &path, + ActionEnum::ApproveMergeRequest, + state.clone(), + ) + .await + .is_ok()); + let res = state.monorepo().merge_mr(&mut model.into()).await; + let res = match res { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + ApiRequestEvent::notify(ApiType::MergeDone, &state.0.context.config); + return Ok(Json(res)); + } + Ok(Json(CommonResult::failed("not found"))) } async fn get_mr_list( @@ -105,4 +122,4 @@ async fn delete_comment( Err(err) => CommonResult::failed(&err.to_string()), }; Ok(Json(res)) -} \ No newline at end of file +} diff --git a/mono/src/api/oauth/mod.rs b/mono/src/api/oauth/mod.rs index 3df5596ef..52da89227 100644 --- a/mono/src/api/oauth/mod.rs +++ b/mono/src/api/oauth/mod.rs @@ -187,7 +187,7 @@ pub struct AuthRedirect; impl IntoResponse for AuthRedirect { fn into_response(self) -> Response { - (StatusCode::UNAUTHORIZED, "Login in first").into_response() + (StatusCode::UNAUTHORIZED, "Login first").into_response() } } diff --git a/mono/src/api/user/model.rs b/mono/src/api/user/model.rs index 65794af6d..4de86bb20 100644 --- a/mono/src/api/user/model.rs +++ b/mono/src/api/user/model.rs @@ -1,6 +1,6 @@ use callisto::ssh_keys; -use serde::{Deserialize, Serialize}; use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct AddSSHKey { @@ -28,3 +28,10 @@ impl From for ListSSHKey { } } } + +#[derive(Debug, Serialize, Deserialize)] +pub struct RepoPermissions { + pub admin: Vec, + pub maintainer: Vec, + pub reader: Vec, +} diff --git a/mono/src/api/user/user_router.rs b/mono/src/api/user/user_router.rs index e2e93d2a3..c40e2c111 100644 --- a/mono/src/api/user/user_router.rs +++ b/mono/src/api/user/user_router.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, routing::{get, post}, Json, Router, }; @@ -10,7 +12,7 @@ use common::model::CommonResult; use crate::api::user::model::AddSSHKey; use crate::api::user::model::ListSSHKey; use crate::api::MonoApiServiceState; -use crate::api::{error::ApiError, oauth::model::LoginUser}; +use crate::api::{error::ApiError, oauth::model::LoginUser, util}; pub fn routers() -> Router { Router::new() @@ -18,6 +20,7 @@ pub fn routers() -> Router { .route("/user/ssh", get(list_key)) .route("/user/ssh", post(add_key)) .route("/user/ssh/:key_id/delete", post(remove_key)) + .route("/repo-permissions", get(repo_permissions)) } async fn user( @@ -92,3 +95,27 @@ async fn list_key( }; Ok(Json(res)) } + +async fn repo_permissions( + Query(query): Query>, + state: State, +) -> Result>, ApiError> { + let path = std::path::PathBuf::from(query.get("path").unwrap()); + let _ = util::get_entitystore(path, state).await; + Ok(Json(CommonResult::success(Some(String::new())))) +} + +#[cfg(test)] +mod test { + use std::path::{Path, PathBuf}; + + #[test] + fn test_parse_all_cedar_file() { + let path = PathBuf::from("/project/mega/src"); + for component in path.ancestors() { + if component != Path::new("/") { + println!("{:?}", component.join(".mega_cedar.json")); + } + } + } +} diff --git a/moon/src/app/api/mr/[id]/merge/route.ts b/moon/src/app/api/mr/[id]/merge/route.ts index 532b1aaf4..5ea50169b 100644 --- a/moon/src/app/api/mr/[id]/merge/route.ts +++ b/moon/src/app/api/mr/[id]/merge/route.ts @@ -3,18 +3,21 @@ import { verifySession } from "@/app/lib/dal"; export const dynamic = 'force-dynamic' // defaults to auto -export async function POST(request: Request, { params }: { params: { id: string } }) { +export async function POST(request: Request, { params }: { params: { id: string } }) { const session = await verifySession() - + // Check if the user is authenticated if (!session) { - // User is not authenticated - return new Response(null, { status: 401 }) + // User is not authenticated + return new Response(null, { status: 401 }) } const endpoint = process.env.MEGA_INTERNAL_HOST; const res = await fetch(`${endpoint}/api/v1/mr/${params.id}/merge`, { method: 'POST', + headers: { + 'Cookie': request.headers.get('cookie') || '', + }, }) const data = await res.json() diff --git a/saturn/Cargo.toml b/saturn/Cargo.toml index 41c95f535..1c7688cba 100644 --- a/saturn/Cargo.toml +++ b/saturn/Cargo.toml @@ -11,6 +11,6 @@ serde_json = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } thiserror = { workspace = true } +cedar-policy = { workspace = true } itertools = "0.13.0" -cedar-policy = "4.2.1" diff --git a/saturn/src/context.rs b/saturn/src/context.rs index 814e79fff..dd0d2eece 100644 --- a/saturn/src/context.rs +++ b/saturn/src/context.rs @@ -8,8 +8,7 @@ use thiserror::Error; use crate::{entitystore::EntityStore, util::EntityUid}; -#[allow(dead_code)] -pub struct AppContext { +pub struct CedarContext { pub entities: EntityStore, authorizer: Authorizer, policies: PolicySet, @@ -44,8 +43,7 @@ pub enum Error { Request(String), } -impl AppContext { - #[allow(dead_code)] +impl CedarContext { pub fn new( entities: EntityStore, schema_path: impl Into, @@ -81,7 +79,6 @@ impl AppContext { } } - #[allow(dead_code)] pub fn is_authorized( &self, principal: impl AsRef, diff --git a/saturn/src/entitystore.rs b/saturn/src/entitystore.rs index c3cee68a6..30153367d 100644 --- a/saturn/src/entitystore.rs +++ b/saturn/src/entitystore.rs @@ -19,6 +19,16 @@ pub struct EntityStore { } impl EntityStore { + pub fn new() -> Self { + Self { + users: HashMap::new(), + repos: HashMap::new(), + merge_requests: HashMap::new(), + issues: HashMap::new(), + user_groups: HashMap::new(), + } + } + #[allow(dead_code)] pub fn as_entities(&self, schema: &Schema) -> Entities { let users = self.users.values().map(|user| user.clone().into()); @@ -34,7 +44,6 @@ impl EntityStore { Entities::from_entities(all, Some(schema)).unwrap() } - #[allow(dead_code)] pub fn merge(&mut self, other: EntityStore) { self.users.extend(other.users); self.repos.extend(other.repos); diff --git a/saturn/src/lib.rs b/saturn/src/lib.rs index ad773cba7..5dffce46a 100644 --- a/saturn/src/lib.rs +++ b/saturn/src/lib.rs @@ -1,7 +1,49 @@ -mod context; +use std::fmt::{self, Display}; + +pub mod context; pub mod entitystore; mod objects; -mod util; +pub mod util; + + +pub enum ActionEnum { + // ** Anyone + // ViewRepo, + // PullRepo, + // ForkRepo, + // PushRepo, + // OpenIssue, + // ** Maintainer + CreateMergeRequest, + EditIssue, + EditMergeRequest, + AssignIssue, + ApproveMergeRequest, + // ** Admin + AddMaintainer, + AddAdmin, + DeleteRepo, + DeleteIssue, + DeleteMergeRequest, +} + +impl Display for ActionEnum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + ActionEnum::CreateMergeRequest => "Comment", + ActionEnum::EditIssue => "Deploy", + ActionEnum::EditMergeRequest => "Commit", + ActionEnum::AssignIssue => "ForcePush", + ActionEnum::ApproveMergeRequest => "approveMergeRequest", + ActionEnum::AddMaintainer => "Review", + ActionEnum::AddAdmin => "Approve", + ActionEnum::DeleteRepo => "MergeQueue", + ActionEnum::DeleteIssue => "Merged", + ActionEnum::DeleteMergeRequest => "Merged", + }; + write!(f, "{}", s) + } +} #[cfg(test)] mod test { @@ -10,7 +52,7 @@ mod test { use cedar_policy::{Authorizer, Context, Entities, PolicySet, Request}; use crate::{ - context::{AppContext, Error}, + context::{CedarContext, Error}, entitystore::EntityStore, util::EntityUid, }; @@ -53,8 +95,8 @@ mod test { println!("{:?}", answer.decision()); } - fn load_context(entities: EntityStore) -> AppContext { - AppContext::new(entities, "./mega.cedarschema", "./mega_policies.cedar").unwrap() + fn load_context(entities: EntityStore) -> CedarContext { + CedarContext::new(entities, "./mega.cedarschema", "./mega_policies.cedar").unwrap() } #[test] @@ -141,7 +183,7 @@ mod test { 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 private_project: EntityUid = r#"Repository::"project/private""#.parse().unwrap(); + let private_project: EntityUid = r#"Repository::"/project/bens_private""#.parse().unwrap(); // admin under project should also have permisisons assert!(app_context diff --git a/saturn/test/project/private/.mega.json b/saturn/test/project/private/.mega.json index 617684ea7..a5ec900b5 100644 --- a/saturn/test/project/private/.mega.json +++ b/saturn/test/project/private/.mega.json @@ -8,8 +8,8 @@ } }, "repos": { - "Repository::\"project/private\"": { - "euid": "Repository::\"project/private\"", + "Repository::\"project/bens_private\"": { + "euid": "Repository::\"/project/bens_private\"", "is_private": true, "admins": "UserGroup::\"admin\"", "maintainers": "UserGroup::\"matainer\"",