Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ members = [
"libra",
"vault",
"neptune",
"saturn",
"lunar/src-tauri",
]
default-members = ["mega", "libra"]
Expand All @@ -27,6 +28,7 @@ callisto = { path = "jupiter/callisto" }
gemini = { path = "gemini" }
vault = { path = "vault" }
neptune = { path = "neptune" }
saturn = { path = "saturn" }
mega = { path = "mega" }
anyhow = "1.0.86"
serde = "1.0.203"
Expand Down Expand Up @@ -67,3 +69,4 @@ num_cpus = "1.16.0"
config = "0.14.0"
shadow-rs = "0.30.0"
reqwest = "0.12.5"
lazy_static = "1.5.0"
16 changes: 16 additions & 0 deletions saturn/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "saturn"
version = "0.1.0"
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.13.0"
cedar-policy = "3.2.1"
78 changes: 78 additions & 0 deletions saturn/entities.json
Original file line number Diff line number Diff line change
@@ -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": {

}
}
51 changes: 51 additions & 0 deletions saturn/mega.cedarschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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],
};
95 changes: 95 additions & 0 deletions saturn/mega_policies.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// policy "anyoneCanViewPublicRepo" even if not login?
permit (
principal,
action in [Action::"viewRepo"],
resource
)
unless { resource.is_private };

// policy "adminCanManageTeams"
// 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 [Action::"viewTeam"],
resource
)
when { principal in resource.members };

//Actions for readers
permit (
principal,
action in [Action::"viewRepo", Action::"pullRepo", Action::"forkRepo"],
resource
)
when { principal in resource.readers };

permit (
principal,
action in [Action::"openIssue", Action::"createMergeRequest"],
resource
)
when { principal in resource.repo.readers };

permit (
principal,
action in [Action::"editIssue"],
resource
)
when { principal in resource.repo.readers && principal == resource.owner };

//Actions for triagers
permit (
principal,
action == Action::"assignIssue",
resource
)
when { principal in resource.repo.triagers };

//Actions for writers
permit (
principal,
action == Action::"pushRepo",
resource
)
when { principal in resource.writers };

permit (
principal,
action in [Action::"editIssue", Action::"approveMergeRequest"],
resource
)
when { principal in resource.repo.writers };

//Actions for maintainers
permit (
principal,
action in [Action::"deleteIssue", Action::"approveMergeRequest"],
resource
)
when { principal in resource.repo.maintainers };

//Actions for admins
permit (
principal,
action in
[Action::"addReader",
Action::"addTriager",
Action::"addWriter",
Action::"addMaintainer",
Action::"addAdmin",
Action::"deleteRepo"],
resource
)
when { principal in resource.admins };
112 changes: 112 additions & 0 deletions saturn/src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use cedar_policy::{
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,
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),
}

#[derive(Debug, Error)]
pub enum Error {
#[error("Authorization Denied")]
AuthDenied(Diagnostics),
#[error("Error constructing authorization request: {0}")]
Request(String),
}

impl AppContext {
pub fn new(
entities_path: impl Into<PathBuf>,
schema_path: impl Into<PathBuf>,
policies_path: impl Into<PathBuf>,
) -> Result<Self, ContextError> {
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!("All policy 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))
}
}

pub fn is_authorized(
&self,
principal: impl AsRef<EntityUid>,
action: impl AsRef<EntityUid>,
resource: impl AsRef<EntityUid>,
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())),
}
}
}
Loading