From fd40eb70ae022a74f494459afc493b2c61920a40 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Fri, 17 May 2024 11:27:46 -0700 Subject: [PATCH 01/15] Add user CR APIs Signed-off-by: Shaobo He --- tinytodo/src/api.rs | 96 ++++++++++++++++++++++++++++------------- tinytodo/src/context.rs | 63 ++++++++++++++++++++++++--- 2 files changed, 123 insertions(+), 36 deletions(-) diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index b34a92d..93e2b22 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -22,12 +22,36 @@ use warp::Filter; use crate::{ context::{AppQuery, AppQueryKind, AppResponse, Error}, - objects::{List, TaskState}, + objects::{List, TaskState, User}, util::{EntityUid, ListUid, Lists, UserOrTeamUid, UserUid}, }; type AppChannel = mpsc::Sender; +#[derive(Debug, Clone, Deserialize)] +pub struct CreateUser { + pub id: String, + pub joblevel: i64, + pub location: String, +} + +impl From for AppQueryKind { + fn from(v: CreateUser) -> AppQueryKind { + AppQueryKind::CreateUser(v) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetUser { + pub id: String, +} + +impl From for AppQueryKind { + fn from(v: GetUser) -> AppQueryKind { + AppQueryKind::GetUser(v) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct GetList { pub uid: UserUid, @@ -178,45 +202,45 @@ pub async fn serve_api(chan: AppChannel, port: u16) { let filter = warp::path("api").and( // List CRUD (warp::path("list").and( - (warp::path("get") + warp::path("get") .and(warp::get()) .and(with_app(chan.clone())) .and(warp::query::query::()) - .and_then(simple_query::)) - .or(warp::path("create") - .and(warp::post()) - .and(with_app(chan.clone())) - .and(warp::body::json()) - .and_then(simple_query::)) - .or(warp::path("update") - .and(warp::post()) - .and(with_app(chan.clone())) - .and(warp::body::json()) - .and_then(simple_query::)) - .or(warp::path("delete") - .and(warp::delete()) - .and(with_app(chan.clone())) - .and(warp::body::json()) - .and_then(simple_query::)), - )) - .or( - // Task CRUD - warp::path("task").and( - (warp::path("create") + .and_then(simple_query::) + .or(warp::path("create") .and(warp::post()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)) + .and_then(simple_query::)) .or(warp::path("update") .and(warp::post()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)) + .and_then(simple_query::)) .or(warp::path("delete") .and(warp::delete()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)), + .and_then(simple_query::)), + )) + .or( + // Task CRUD + warp::path("task").and( + warp::path("create") + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::) + .or(warp::path("update") + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::)) + .or(warp::path("delete") + .and(warp::delete()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::)), ), ) .or(warp::path("lists") @@ -225,14 +249,26 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::query::query::()) .and_then(simple_query::)) .or(warp::path("share").and( - (warp::post() + warp::post() .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)) - .or(warp::delete() + .and_then(simple_query::) + .or(warp::delete() + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::)), + )) + .or(warp::path("user").and( + warp::path("create") + .and(warp::post()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)), + .and_then(simple_query::) + .or(warp::path("get") + .and(warp::get()) + .and(with_app(chan.clone())) + .and(warp::query::query::()) + .and_then(simple_query::)), )), ); diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index 32f74fb..06abe34 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -20,8 +20,9 @@ use std::path::PathBuf; use tracing::{error, info, trace}; use cedar_policy::{ - Authorizer, Context, Decision, Diagnostics, EntityTypeName, HumanSchemaError, ParseErrors, - PolicySet, PolicySetError, Request, Schema, SchemaError, ValidationMode, Validator, + Authorizer, Context, Decision, Diagnostics, EntityId, EntityTypeName, HumanSchemaError, + ParseErrors, PolicySet, PolicySetError, Request, Schema, SchemaError, ValidationMode, + Validator, }; use thiserror::Error; @@ -32,13 +33,13 @@ use tokio::sync::{ use crate::{ api::{ - AddShare, CreateList, CreateTask, DeleteList, DeleteShare, DeleteTask, Empty, GetList, - GetLists, UpdateList, UpdateTask, + AddShare, CreateList, CreateTask, CreateUser, DeleteList, DeleteShare, DeleteTask, Empty, + GetList, GetLists, GetUser, UpdateList, UpdateTask, }, entitystore::{EntityDecodeError, EntityStore}, - objects::List, + objects::{List, User}, policy_store, - util::{EntityUid, ListUid, Lists, TYPE_LIST}, + util::{EntityUid, ListUid, Lists, UserUid, TYPE_LIST, TYPE_USER}, }; #[cfg(feature = "use-templates")] @@ -57,6 +58,7 @@ pub enum AppResponse { Lists(Lists), TaskId(i64), Unit(()), + User(User), } impl AppResponse { @@ -108,6 +110,16 @@ impl TryInto for AppResponse { } } +impl TryInto for AppResponse { + type Error = Error; + fn try_into(self) -> std::result::Result { + match self { + AppResponse::User(u) => Ok(u), + _ => Err(Error::Type), + } + } +} + impl TryInto for AppResponse { type Error = Error; fn try_into(self) -> std::result::Result { @@ -140,6 +152,10 @@ pub enum AppQueryKind { // Policy Set Updates UpdatePolicySet(PolicySet), + + // User CRUD + CreateUser(CreateUser), + GetUser(GetUser), } #[derive(Debug)] @@ -178,6 +194,8 @@ pub enum ContextError { pub enum Error { #[error("No Such Entity: {0}")] NoSuchEntity(EntityUid), + #[error("Duplicate entity: {0}")] + DuplicateEntity(EntityUid), #[error("Entity Decode Error: {0}")] EntityDecode(#[from] EntityDecodeError), #[error("Authorization Denied")] @@ -355,6 +373,8 @@ impl AppContext { AppQueryKind::AddShare(r) => self.add_share(r), AppQueryKind::DeleteShare(r) => self.delete_share(r), AppQueryKind::UpdatePolicySet(set) => self.update_policy_set(set), + AppQueryKind::CreateUser(r) => self.create_user(r), + AppQueryKind::GetUser(r) => self.get_user(r), }; if let Err(e) = msg.sender.send(r) { trace!("Failed send response: {:?}", e); @@ -525,6 +545,37 @@ impl AppContext { )) } + fn create_user(&mut self, r: CreateUser) -> Result { + let u = User::new( + UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.id), + ), + )) + .unwrap(), + r.joblevel, + r.location, + ); + match self.entities.get_user(u.uid()) { + Ok(_) => Err(Error::DuplicateEntity(u.uid().clone().into())), + Err(_) => { + self.entities.insert_user(u); + Ok(AppResponse::Unit(())) + } + } + } + + fn get_user(&mut self, r: GetUser) -> Result { + let user_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id(TYPE_USER.clone(), EntityId::new(r.id)), + )) + .unwrap(); + self.entities + .get_user(&user_id) + .map(|v| AppResponse::User(v.clone())) + } + fn create_list(&mut self, r: CreateList) -> Result { self.is_authorized(&r.uid, &*ACTION_CREATE_LIST, &*APPLICATION_TINY_TODO)?; From 81dfd45e8ea030489c5f76b37b49ed5242a7b581 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Fri, 17 May 2024 14:23:31 -0700 Subject: [PATCH 02/15] added policies and schemas for team management Signed-off-by: Shaobo He --- tinytodo/policies.cedar | 50 ++++++++++++++++++++++++++++------- tinytodo/tinytodo.cedarschema | 17 ++++++++++-- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/tinytodo/policies.cedar b/tinytodo/policies.cedar index b33e1f6..697d1bd 100644 --- a/tinytodo/policies.cedar +++ b/tinytodo/policies.cedar @@ -1,16 +1,12 @@ // Policy 0: Any User can create a list and see what lists they own permit ( principal, - action in [Action::"CreateList", Action::"GetLists"], + action in [Action::"CreateList", Action::"GetLists", Action::"CreateTeam"], resource == Application::"TinyTodo" ); // Policy 1: A User can perform any action on a List they own -permit ( - principal, - action, - resource is List -) +permit (principal, action, resource is List) when { resource.owner == principal }; // Policy 2: A User can see a List if they are either a reader or editor @@ -40,7 +36,7 @@ when { principal in resource.editors }; // action, // resource in Application::"TinyTodo" // ); -// +// // Policy 5: Interns may not create new task lists // forbid ( // principal in Team::"interns", @@ -48,7 +44,7 @@ when { principal in resource.editors }; // resource == Application::"TinyTodo" // ); // -// Policy 6: No access if not high rank and at location DEF, +// Policy 6: No access if not high rank and at location DEF, // or at resource's owner's location // forbid( // principal, @@ -57,4 +53,40 @@ when { principal in resource.editors }; // ) unless { // principal.joblevel > 6 && principal.location like "DEF*" || // principal.location == resource.owner.location -// }; \ No newline at end of file +// }; + +// Only team owners can add admins +permit ( + principal, + action == Action::"AddAdmin", + resource +) +when { resource.owner == principal }; + +// Only team owners can remove admins but they can't remove themselves +permit ( + principal, + action == Action::"RemoveAdmin", + resource +) +when { resource.owner == principal && context.candidate != principal }; + +// Only team admins can add members +permit ( + principal, + action == Action::"AddMember", + resource +) +when { resource.admins.contains(principal) }; + +// Only team admins can remove members but they cannot remove other admins +permit ( + principal, + action == Action::"RemoveMember", + resource +) +when +{ + resource.admins.contains(principal) && + !resource.admins.contains(context.candidate) +}; \ No newline at end of file diff --git a/tinytodo/tinytodo.cedarschema b/tinytodo/tinytodo.cedarschema index 30ab973..15dfeca 100644 --- a/tinytodo/tinytodo.cedarschema +++ b/tinytodo/tinytodo.cedarschema @@ -14,13 +14,16 @@ entity User in [Team, Application] = { "joblevel": Long, "location": String, }; -entity Team in [Team, Application]; +entity Team in [Team, Application] = { + "owner": User, + "admins": Set, +}; action DeleteList, GetList, UpdateList appliesTo { principal: [User], resource: [List] }; -action CreateList, GetLists appliesTo { +action CreateList, GetLists, CreateTeam appliesTo { principal: [User], resource: [Application] }; @@ -32,3 +35,13 @@ action EditShare appliesTo { principal: [User], resource: [List] }; + +type Candidate = { + "candidate": User, +}; +action AddAdmin, RemoveAdmin, AddMember, RemoveMember appliesTo { + principal: User, + resource: Team, + context: Candidate, +}; + From 140aa033fabd4dcc49b9ec624859e9b4bd225886 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Mon, 20 May 2024 13:34:29 -0700 Subject: [PATCH 03/15] updates Signed-off-by: Shaobo He --- tinytodo/entities.json | 6 +++++ tinytodo/src/api.rs | 39 +++++++++++++++++++++++++++++-- tinytodo/src/context.rs | 51 +++++++++++++++++++++++++++++++++++++---- tinytodo/src/objects.rs | 10 +++++--- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/tinytodo/entities.json b/tinytodo/entities.json index f20de8f..5accff1 100644 --- a/tinytodo/entities.json +++ b/tinytodo/entities.json @@ -41,18 +41,24 @@ "teams": { "Team::\"temp\"": { "uid": "Team::\"temp\"", + "owner": "User::\"emina\"", + "admins": ["User::\"emina\""], "parents": [ "Application::\"TinyTodo\"" ] }, "Team::\"admin\"": { "uid": "Team::\"admin\"", + "owner": "User::\"emina\"", + "admins": ["User::\"emina\""], "parents": [ "Application::\"TinyTodo\"" ] }, "Team::\"interns\"": { "uid": "Team::\"interns\"", + "owner": "User::\"emina\"", + "admins": ["User::\"emina\""], "parents": [ "Application::\"TinyTodo\"", "Team::\"temp\"" diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index 93e2b22..40ed3f5 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -22,7 +22,7 @@ use warp::Filter; use crate::{ context::{AppQuery, AppQueryKind, AppResponse, Error}, - objects::{List, TaskState, User}, + objects::{List, TaskState, Team, User}, util::{EntityUid, ListUid, Lists, UserOrTeamUid, UserUid}, }; @@ -52,6 +52,29 @@ impl From for AppQueryKind { } } +#[derive(Debug, Clone, Deserialize)] +pub struct CreateTeam { + pub owner: String, + pub id: String, +} + +impl From for AppQueryKind { + fn from(v: CreateTeam) -> AppQueryKind { + AppQueryKind::CreateTeam(v) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetTeam { + pub id: String, +} + +impl From for AppQueryKind { + fn from(v: GetTeam) -> AppQueryKind { + AppQueryKind::GetTeam(v) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct GetList { pub uid: UserUid, @@ -263,12 +286,24 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::post()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::) + .and_then(simple_query::) .or(warp::path("get") .and(warp::get()) .and(with_app(chan.clone())) .and(warp::query::query::()) .and_then(simple_query::)), + )) + .or(warp::path("team").and( + warp::path("create") + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::) + .or(warp::path("get") + .and(warp::get()) + .and(with_app(chan.clone())) + .and(warp::query::query::()) + .and_then(simple_query::)), )), ); diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index 06abe34..f000d3e 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -33,13 +33,13 @@ use tokio::sync::{ use crate::{ api::{ - AddShare, CreateList, CreateTask, CreateUser, DeleteList, DeleteShare, DeleteTask, Empty, - GetList, GetLists, GetUser, UpdateList, UpdateTask, + AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, DeleteShare, + DeleteTask, Empty, GetList, GetLists, GetTeam, GetUser, UpdateList, UpdateTask, }, entitystore::{EntityDecodeError, EntityStore}, - objects::{List, User}, + objects::{List, Team, User}, policy_store, - util::{EntityUid, ListUid, Lists, UserUid, TYPE_LIST, TYPE_USER}, + util::{EntityUid, ListUid, Lists, TeamUid, UserUid, TYPE_LIST, TYPE_TEAM, TYPE_USER}, }; #[cfg(feature = "use-templates")] @@ -59,6 +59,7 @@ pub enum AppResponse { TaskId(i64), Unit(()), User(User), + Team(Team), } impl AppResponse { @@ -120,6 +121,16 @@ impl TryInto for AppResponse { } } +impl TryInto for AppResponse { + type Error = Error; + fn try_into(self) -> std::result::Result { + match self { + AppResponse::Team(t) => Ok(t), + _ => Err(Error::Type), + } + } +} + impl TryInto for AppResponse { type Error = Error; fn try_into(self) -> std::result::Result { @@ -156,6 +167,10 @@ pub enum AppQueryKind { // User CRUD CreateUser(CreateUser), GetUser(GetUser), + + // Team CRUD + CreateTeam(CreateTeam), + GetTeam(GetTeam), } #[derive(Debug)] @@ -375,6 +390,8 @@ impl AppContext { AppQueryKind::UpdatePolicySet(set) => self.update_policy_set(set), AppQueryKind::CreateUser(r) => self.create_user(r), AppQueryKind::GetUser(r) => self.get_user(r), + AppQueryKind::CreateTeam(r) => self.create_team(r), + AppQueryKind::GetTeam(r) => self.get_team(r), }; if let Err(e) = msg.sender.send(r) { trace!("Failed send response: {:?}", e); @@ -545,6 +562,32 @@ impl AppContext { )) } + fn create_team(&mut self, r: CreateTeam) -> Result { + let u = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.owner), + ), + )) + .unwrap(); + let t = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id(TYPE_TEAM.clone(), EntityId::new(r.id)), + )) + .unwrap(); + self.entities.insert_team(Team::new(t, u)); + Ok(AppResponse::Unit(())) + } + + fn get_team(&mut self, r: GetTeam) -> Result { + let team_id = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id(TYPE_USER.clone(), EntityId::new(r.id)), + )) + .unwrap(); + self.entities + .get_team(&team_id) + .map(|v| AppResponse::Team(v.clone())) + } + fn create_user(&mut self, r: CreateUser) -> Result { let u = User::new( UserUid::try_from(EntityUid::from( diff --git a/tinytodo/src/objects.rs b/tinytodo/src/objects.rs index 872c042..23abae7 100644 --- a/tinytodo/src/objects.rs +++ b/tinytodo/src/objects.rs @@ -115,14 +115,18 @@ impl UserOrTeam for User { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Team { uid: TeamUid, + owner: UserUid, + admins: HashSet, parents: HashSet, } impl Team { - pub fn new(euid: TeamUid) -> Team { + pub fn new(euid: TeamUid, owner: UserUid) -> Team { let parent = Application::default().euid().clone(); Self { uid: euid, + owner: owner.clone(), + admins: [owner].into_iter().collect(), parents: [parent].into_iter().collect(), } } @@ -170,9 +174,9 @@ impl List { #[cfg(not(feature = "use-templates"))] { let readers_uid = store.fresh_euid::(TYPE_TEAM.clone()).unwrap(); - let readers = Team::new(readers_uid.clone()); + let readers = Team::new(readers_uid.clone(), owner.clone()); let writers_uid = store.fresh_euid::(TYPE_TEAM.clone()).unwrap(); - let writers = Team::new(writers_uid.clone()); + let writers = Team::new(writers_uid.clone(), owner.clone()); store.insert_team(readers); store.insert_team(writers); Self { From f098d43561972bdef5a61b802e60be74446cc15a Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Mon, 20 May 2024 15:19:31 -0700 Subject: [PATCH 04/15] updates Signed-off-by: Shaobo He --- tinytodo/src/api.rs | 39 +++++++++++- tinytodo/src/context.rs | 118 ++++++++++++++++++++++++++++++++++-- tinytodo/src/entitystore.rs | 20 ++++++ tinytodo/src/objects.rs | 17 +++++- tinytodo/src/util.rs | 12 ++++ 5 files changed, 198 insertions(+), 8 deletions(-) diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index 40ed3f5..64d089f 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -75,6 +75,32 @@ impl From for AppQueryKind { } } +#[derive(Debug, Clone, Deserialize)] +pub struct AddAdmin { + pub team: String, + pub user: String, + pub candidate: String, +} + +impl From for AppQueryKind { + fn from(value: AddAdmin) -> Self { + AppQueryKind::AddAdmin(value) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RemoveAdmin { + pub team: String, + pub user: String, + pub candidate: String, +} + +impl From for AppQueryKind { + fn from(value: RemoveAdmin) -> Self { + AppQueryKind::RemoveAdmin(value) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct GetList { pub uid: UserUid, @@ -303,7 +329,18 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::get()) .and(with_app(chan.clone())) .and(warp::query::query::()) - .and_then(simple_query::)), + .and_then(simple_query::)) + .or(warp::path("admin") + .and(warp::path("add")) + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::) + .or(warp::path("remove") + .and(warp::delete()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::))), )), ); diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index f000d3e..11148f3 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -21,8 +21,8 @@ use tracing::{error, info, trace}; use cedar_policy::{ Authorizer, Context, Decision, Diagnostics, EntityId, EntityTypeName, HumanSchemaError, - ParseErrors, PolicySet, PolicySetError, Request, Schema, SchemaError, ValidationMode, - Validator, + ParseErrors, PolicySet, PolicySetError, Request, RestrictedExpression, Schema, SchemaError, + ValidationMode, Validator, }; use thiserror::Error; @@ -33,8 +33,9 @@ use tokio::sync::{ use crate::{ api::{ - AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, DeleteShare, - DeleteTask, Empty, GetList, GetLists, GetTeam, GetUser, UpdateList, UpdateTask, + AddAdmin, AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, + DeleteShare, DeleteTask, Empty, GetList, GetLists, GetTeam, GetUser, RemoveAdmin, + UpdateList, UpdateTask, }, entitystore::{EntityDecodeError, EntityStore}, objects::{List, Team, User}, @@ -171,6 +172,8 @@ pub enum AppQueryKind { // Team CRUD CreateTeam(CreateTeam), GetTeam(GetTeam), + AddAdmin(AddAdmin), + RemoveAdmin(RemoveAdmin), } #[derive(Debug)] @@ -250,6 +253,8 @@ lazy_static! { static ref ACTION_CREATE_LIST: EntityUid = r#"Action::"CreateList""#.parse().unwrap(); static ref ACTION_UPDATE_LIST: EntityUid = r#"Action::"UpdateList""#.parse().unwrap(); static ref ACTION_DELETE_LIST: EntityUid = r#"Action::"DeleteList""#.parse().unwrap(); + static ref ACTION_ADD_ADMIN: EntityUid = r#"Action::"AddAdmin""#.parse().unwrap(); + static ref ACTION_REMOVE_ADMIN: EntityUid = r#"Action::"RemoveAdmin""#.parse().unwrap(); } pub struct AppContext { @@ -392,6 +397,8 @@ impl AppContext { AppQueryKind::GetUser(r) => self.get_user(r), AppQueryKind::CreateTeam(r) => self.create_team(r), AppQueryKind::GetTeam(r) => self.get_team(r), + AppQueryKind::AddAdmin(r) => self.add_admin(r), + AppQueryKind::RemoveAdmin(r) => self.remove_admin(r), }; if let Err(e) = msg.sender.send(r) { trace!("Failed send response: {:?}", e); @@ -580,7 +587,7 @@ impl AppContext { fn get_team(&mut self, r: GetTeam) -> Result { let team_id = TeamUid::try_from(EntityUid::from( - cedar_policy::EntityUid::from_type_name_and_id(TYPE_USER.clone(), EntityId::new(r.id)), + cedar_policy::EntityUid::from_type_name_and_id(TYPE_TEAM.clone(), EntityId::new(r.id)), )) .unwrap(); self.entities @@ -588,6 +595,76 @@ impl AppContext { .map(|v| AppResponse::Team(v.clone())) } + fn add_admin(&mut self, r: AddAdmin) -> Result { + let team_id = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_TEAM.clone(), + EntityId::new(r.team), + ), + )) + .unwrap(); + let user_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.user), + ), + )) + .unwrap(); + let candidate_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.candidate), + ), + )) + .unwrap(); + + self.is_authorized_with_context( + user_id, + &*ACTION_ADD_ADMIN, + team_id.clone(), + Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(), + )?; + + self.entities.add_admin(team_id, candidate_id)?; + + Ok(AppResponse::Unit(())) + } + + fn remove_admin(&mut self, r: RemoveAdmin) -> Result { + let team_id = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_TEAM.clone(), + EntityId::new(r.team), + ), + )) + .unwrap(); + let user_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.user), + ), + )) + .unwrap(); + let candidate_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.candidate), + ), + )) + .unwrap(); + + self.is_authorized_with_context( + user_id, + &*ACTION_ADD_ADMIN, + team_id.clone(), + Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(), + )?; + + self.entities.remove_admin(team_id, candidate_id)?; + + Ok(AppResponse::Unit(())) + } + fn create_user(&mut self, r: CreateUser) -> Result { let u = User::new( UserUid::try_from(EntityUid::from( @@ -651,6 +728,37 @@ impl AppContext { Ok(AppResponse::Unit(())) } + #[tracing::instrument(skip_all)] + pub fn is_authorized_with_context( + &self, + principal: impl AsRef, + action: impl AsRef, + resource: impl AsRef, + context: cedar_policy::Context, + ) -> Result<()> { + 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()))?; + 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); + info!("Auth response: {:?}", response); + match response.decision() { + Decision::Allow => Ok(()), + Decision::Deny => Err(Error::AuthDenied(response.diagnostics().clone())), + } + } + #[tracing::instrument(skip_all)] pub fn is_authorized( &self, diff --git a/tinytodo/src/entitystore.rs b/tinytodo/src/entitystore.rs index 3200bab..36f0b66 100644 --- a/tinytodo/src/entitystore.rs +++ b/tinytodo/src/entitystore.rs @@ -84,6 +84,26 @@ impl EntityStore { self.lists.insert(e.uid().clone().into(), e); } + pub fn add_admin(&mut self, e: TeamUid, c: UserUid) -> Result<(), Error> { + match self.teams.get_mut(&e.clone().into()) { + Some(t) => { + t.add_admin(c); + Ok(()) + } + None => Err(Error::no_such_entity(e)), + } + } + + pub fn remove_admin(&mut self, e: TeamUid, c: UserUid) -> Result<(), Error> { + match self.teams.get_mut(&e.clone().into()) { + Some(t) => { + t.remove_admin(c); + Ok(()) + } + None => Err(Error::no_such_entity(e)), + } + } + pub fn delete_entity(&mut self, e: impl AsRef) -> Result<(), Error> { let r = e.as_ref(); if self.users.contains_key(r) { diff --git a/tinytodo/src/objects.rs b/tinytodo/src/objects.rs index 23abae7..cce835d 100644 --- a/tinytodo/src/objects.rs +++ b/tinytodo/src/objects.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use cedar_policy::{Entity, EvalResult, RestrictedExpression}; use serde::{Deserialize, Serialize}; @@ -134,15 +134,28 @@ impl Team { pub fn uid(&self) -> &TeamUid { &self.uid } + + pub fn add_admin(&mut self, candidate: UserUid) { + self.admins.insert(candidate); + } + + pub fn remove_admin(&mut self, candidate: UserUid) { + self.admins.remove(&candidate); + } } impl From for Entity { fn from(team: Team) -> Entity { let euid: EntityUid = team.uid.into(); - Entity::new_no_attrs( + Entity::new( euid.into(), + HashMap::from_iter([( + "admins".to_owned(), + RestrictedExpression::new_set(team.admins.into_iter().map(|u| u.into())), + )]), team.parents.into_iter().map(|euid| euid.into()).collect(), ) + .unwrap() } } diff --git a/tinytodo/src/util.rs b/tinytodo/src/util.rs index 98d0d38..6f9c9f9 100644 --- a/tinytodo/src/util.rs +++ b/tinytodo/src/util.rs @@ -255,6 +255,12 @@ impl From for EntityUid { } } +impl From for RestrictedExpression { + fn from(value: UserUid) -> Self { + RestrictedExpression::new_entity_uid(value.as_ref().clone().into()) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[serde(try_from = "EntityUid")] #[serde(into = "EntityUid")] @@ -268,6 +274,12 @@ impl TryFrom for TeamUid { } } +impl From for RestrictedExpression { + fn from(value: TeamUid) -> Self { + RestrictedExpression::new_entity_uid(value.as_ref().clone().into()) + } +} + impl AsRef for TeamUid { fn as_ref(&self) -> &EntityUid { &self.0 From b82d3e62635e30dece1703e68ba7c4395cc5871f Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Mon, 20 May 2024 16:16:24 -0700 Subject: [PATCH 05/15] updates Signed-off-by: Shaobo He --- tinytodo/src/api.rs | 34 ++++++++++++++- tinytodo/src/context.rs | 84 +++++++++++++++++++++++++++++++++++-- tinytodo/src/entitystore.rs | 30 +++++++++++++ 3 files changed, 143 insertions(+), 5 deletions(-) diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index 64d089f..f3bb3e9 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -101,6 +101,32 @@ impl From for AppQueryKind { } } +#[derive(Debug, Clone, Deserialize)] +pub struct AddMember { + pub team: String, + pub user: String, + pub candidate: String, +} + +impl From for AppQueryKind { + fn from(value: AddMember) -> Self { + AppQueryKind::AddMember(value) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RemoveMember { + pub team: String, + pub user: String, + pub candidate: String, +} + +impl From for AppQueryKind { + fn from(value: RemoveMember) -> Self { + AppQueryKind::RemoveMember(value) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct GetList { pub uid: UserUid, @@ -340,7 +366,13 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::delete()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::))), + .and_then(simple_query::))) + .or(warp::path("member") + .and(warp::path("add")) + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::)), )), ); diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index 11148f3..4ad127b 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -21,8 +21,8 @@ use tracing::{error, info, trace}; use cedar_policy::{ Authorizer, Context, Decision, Diagnostics, EntityId, EntityTypeName, HumanSchemaError, - ParseErrors, PolicySet, PolicySetError, Request, RestrictedExpression, Schema, SchemaError, - ValidationMode, Validator, + ParseErrors, PolicySet, PolicySetError, Request, Schema, SchemaError, ValidationMode, + Validator, }; use thiserror::Error; @@ -33,9 +33,9 @@ use tokio::sync::{ use crate::{ api::{ - AddAdmin, AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, + AddAdmin, AddMember, AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, DeleteShare, DeleteTask, Empty, GetList, GetLists, GetTeam, GetUser, RemoveAdmin, - UpdateList, UpdateTask, + RemoveMember, UpdateList, UpdateTask, }, entitystore::{EntityDecodeError, EntityStore}, objects::{List, Team, User}, @@ -174,6 +174,8 @@ pub enum AppQueryKind { GetTeam(GetTeam), AddAdmin(AddAdmin), RemoveAdmin(RemoveAdmin), + AddMember(AddMember), + RemoveMember(RemoveMember), } #[derive(Debug)] @@ -255,6 +257,8 @@ lazy_static! { static ref ACTION_DELETE_LIST: EntityUid = r#"Action::"DeleteList""#.parse().unwrap(); static ref ACTION_ADD_ADMIN: EntityUid = r#"Action::"AddAdmin""#.parse().unwrap(); static ref ACTION_REMOVE_ADMIN: EntityUid = r#"Action::"RemoveAdmin""#.parse().unwrap(); + static ref ACTION_ADD_MEMBER: EntityUid = r#"Action::"AddMember""#.parse().unwrap(); + static ref ACTION_REMOVE_MEMBER: EntityUid = r#"Action::"RemoveMember""#.parse().unwrap(); } pub struct AppContext { @@ -399,6 +403,8 @@ impl AppContext { AppQueryKind::GetTeam(r) => self.get_team(r), AppQueryKind::AddAdmin(r) => self.add_admin(r), AppQueryKind::RemoveAdmin(r) => self.remove_admin(r), + AppQueryKind::AddMember(r) => self.add_member(r), + AppQueryKind::RemoveMember(r) => self.remove_member(r), }; if let Err(e) = msg.sender.send(r) { trace!("Failed send response: {:?}", e); @@ -630,6 +636,76 @@ impl AppContext { Ok(AppResponse::Unit(())) } + fn remove_member(&mut self, r: RemoveMember) -> Result { + let team_id = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_TEAM.clone(), + EntityId::new(r.team), + ), + )) + .unwrap(); + let user_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.user), + ), + )) + .unwrap(); + let candidate_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.candidate), + ), + )) + .unwrap(); + + self.is_authorized_with_context( + user_id, + &*ACTION_ADD_ADMIN, + team_id.clone(), + Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(), + )?; + + self.entities.remove_user_from_team(candidate_id, team_id)?; + + Ok(AppResponse::Unit(())) + } + + fn add_member(&mut self, r: AddMember) -> Result { + let team_id = TeamUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_TEAM.clone(), + EntityId::new(r.team), + ), + )) + .unwrap(); + let user_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.user), + ), + )) + .unwrap(); + let candidate_id = UserUid::try_from(EntityUid::from( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + EntityId::new(r.candidate), + ), + )) + .unwrap(); + + self.is_authorized_with_context( + user_id, + &*ACTION_ADD_MEMBER, + team_id.clone(), + Context::from_pairs([("candidate".to_owned(), candidate_id.clone().into())]).unwrap(), + )?; + + self.entities.add_user_to_team(candidate_id, team_id)?; + + Ok(AppResponse::Unit(())) + } + fn remove_admin(&mut self, r: RemoveAdmin) -> Result { let team_id = TeamUid::try_from(EntityUid::from( cedar_policy::EntityUid::from_type_name_and_id( diff --git a/tinytodo/src/entitystore.rs b/tinytodo/src/entitystore.rs index 36f0b66..037b9e0 100644 --- a/tinytodo/src/entitystore.rs +++ b/tinytodo/src/entitystore.rs @@ -171,6 +171,36 @@ impl EntityStore { .get_mut(euid.as_ref()) .ok_or_else(|| Error::no_such_entity(euid.clone())) } + + pub fn add_user_to_team(&mut self, candidate: UserUid, team: TeamUid) -> Result<(), Error> { + // TODO: `get_team` and `get_mut` should trivially succeed after + // successful authorization + let _ = self.get_team(&team)?; + match self.users.get_mut(&candidate.clone().into()) { + Some(user) => { + user.insert_parent(team); + Ok(()) + } + None => Err(Error::no_such_entity(candidate)), + } + } + + pub fn remove_user_from_team( + &mut self, + candidate: UserUid, + team: TeamUid, + ) -> Result<(), Error> { + // TODO: `get_team` and `get_mut` should trivially succeed after + // successful authorization + let _ = self.get_team(&team)?; + match self.users.get_mut(&candidate.clone().into()) { + Some(user) => { + user.delete_parent(&team); + Ok(()) + } + None => Err(Error::no_such_entity(candidate)), + } + } } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] From 3bbacc5d3f2bceaf1f9bcb98896cb9c0b248603b Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Mon, 20 May 2024 22:50:18 -0700 Subject: [PATCH 06/15] fixes Signed-off-by: Shaobo He --- tinytodo/src/objects.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tinytodo/src/objects.rs b/tinytodo/src/objects.rs index cce835d..a8ab605 100644 --- a/tinytodo/src/objects.rs +++ b/tinytodo/src/objects.rs @@ -149,10 +149,13 @@ impl From for Entity { let euid: EntityUid = team.uid.into(); Entity::new( euid.into(), - HashMap::from_iter([( - "admins".to_owned(), - RestrictedExpression::new_set(team.admins.into_iter().map(|u| u.into())), - )]), + HashMap::from_iter([ + ( + "admins".to_owned(), + RestrictedExpression::new_set(team.admins.into_iter().map(|u| u.into())), + ), + ("owner".to_owned(), team.owner.into()), + ]), team.parents.into_iter().map(|euid| euid.into()).collect(), ) .unwrap() From a4f16a342938007bdbe206fc1b999c3573617d89 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 12:52:26 -0700 Subject: [PATCH 07/15] update the template schema Signed-off-by: Shaobo He --- tinytodo/tinytodo-templates.cedarschema | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tinytodo/tinytodo-templates.cedarschema b/tinytodo/tinytodo-templates.cedarschema index 8f120ae..25e3302 100644 --- a/tinytodo/tinytodo-templates.cedarschema +++ b/tinytodo/tinytodo-templates.cedarschema @@ -5,7 +5,11 @@ type Task = { }; type Tasks = Set; -entity Team in [Team, Application]; +entity Team in [Team, Application] { + "owner": User, + "admins": Set, +} + entity List in [Application] = { "name": String, "owner": User, From e7e3408ffac1cb49dbb0e74c528781f4e0360995 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 13:10:52 -0700 Subject: [PATCH 08/15] minor fix Signed-off-by: Shaobo He --- tinytodo/tinytodo-templates.cedarschema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinytodo/tinytodo-templates.cedarschema b/tinytodo/tinytodo-templates.cedarschema index 25e3302..57f23eb 100644 --- a/tinytodo/tinytodo-templates.cedarschema +++ b/tinytodo/tinytodo-templates.cedarschema @@ -8,7 +8,7 @@ type Tasks = Set; entity Team in [Team, Application] { "owner": User, "admins": Set, -} +}; entity List in [Application] = { "name": String, From a1808f6fcb580aa39f4cf851eacd14833abc0335 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 16:13:07 -0700 Subject: [PATCH 09/15] add team management APIs Signed-off-by: Shaobo He --- tinytodo/Cargo.toml | 1 + tinytodo/src/api.rs | 35 +++++++- tinytodo/src/context.rs | 191 ++++++++++++++++++++++++++++++++++++++-- tinytodo/src/util.rs | 11 +++ 4 files changed, 227 insertions(+), 11 deletions(-) diff --git a/tinytodo/Cargo.toml b/tinytodo/Cargo.toml index a06851c..dc55b4e 100644 --- a/tinytodo/Cargo.toml +++ b/tinytodo/Cargo.toml @@ -21,6 +21,7 @@ notify = { version = "5.1.0", default-features = false, features = ["macos_kqueu use-templates = [] [dependencies.cedar-policy] +features = ["partial-eval"] version = "4.0.0" git = "https://github.com/cedar-policy/cedar" branch = "main" diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index f3bb3e9..7cf3575 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -23,7 +23,7 @@ use warp::Filter; use crate::{ context::{AppQuery, AppQueryKind, AppResponse, Error}, objects::{List, TaskState, Team, User}, - util::{EntityUid, ListUid, Lists, UserOrTeamUid, UserUid}, + util::{EntityUid, ListUid, Lists, Teams, UserOrTeamUid, UserUid}, }; type AppChannel = mpsc::Sender; @@ -221,6 +221,28 @@ impl From for AppQueryKind { } } +#[derive(Debug, Clone, Deserialize)] +pub struct GetMemberTeams { + pub uid: UserUid, +} + +impl From for AppQueryKind { + fn from(v: GetMemberTeams) -> AppQueryKind { + AppQueryKind::GetMemberTeams(v) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct GetAdminTeams { + pub uid: UserUid, +} + +impl From for AppQueryKind { + fn from(v: GetAdminTeams) -> AppQueryKind { + AppQueryKind::GetAdminTeams(v) + } +} + #[derive(Debug, Clone, Deserialize)] pub struct UpdateTask { pub uid: UserUid, @@ -372,7 +394,16 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::post()) .and(with_app(chan.clone())) .and(warp::body::json()) - .and_then(simple_query::)), + .and_then(simple_query::)) + .or(warp::path("manage") + .and(warp::path("member")) + .and(with_app(chan.clone())) + .and(warp::query::query::()) + .and_then(simple_query::) + .or(warp::path("admin") + .and(with_app(chan.clone())) + .and(warp::query::query::()) + .and_then(simple_query::))), )), ); diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index 5b79509..ea15d2a 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -16,13 +16,13 @@ use itertools::Itertools; use lazy_static::lazy_static; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use tracing::{error, info, trace}; use cedar_policy::{ - schema_error::SchemaError, Authorizer, Context, Decision, Diagnostics, EntityId, - EntityTypeName, HumanSchemaError, ParseErrors, PolicySet, PolicySetError, Request, Schema, - ValidationMode, Validator, + schema_error::SchemaError, Authorizer, Context, Decision, Diagnostics, Entities, EntityId, + HumanSchemaError, ParseErrors, PartialResponse, PolicySet, PolicySetError, Request, + RequestBuilder, RestrictedExpression, Schema, ValidationMode, Validator, }; use thiserror::Error; @@ -34,13 +34,13 @@ use tokio::sync::{ use crate::{ api::{ AddAdmin, AddMember, AddShare, CreateList, CreateTask, CreateTeam, CreateUser, DeleteList, - DeleteShare, DeleteTask, Empty, GetList, GetLists, GetTeam, GetUser, RemoveAdmin, - RemoveMember, UpdateList, UpdateTask, + DeleteShare, DeleteTask, Empty, GetAdminTeams, GetList, GetLists, GetMemberTeams, GetTeam, + GetUser, RemoveAdmin, RemoveMember, UpdateList, UpdateTask, }, entitystore::{EntityDecodeError, EntityStore}, objects::{List, Team, User}, policy_store, - util::{EntityUid, ListUid, Lists, TeamUid, UserUid, TYPE_LIST, TYPE_TEAM, TYPE_USER}, + util::{EntityUid, ListUid, Lists, TeamUid, Teams, UserUid, TYPE_LIST, TYPE_TEAM, TYPE_USER}, }; #[cfg(feature = "use-templates")] @@ -61,6 +61,7 @@ pub enum AppResponse { Unit(()), User(User), Team(Team), + Teams(Teams), } impl AppResponse { @@ -142,6 +143,16 @@ impl TryInto for AppResponse { } } +impl TryInto for AppResponse { + type Error = Error; + fn try_into(self) -> std::result::Result { + match self { + AppResponse::Teams(l) => Ok(l), + _ => Err(Error::Type), + } + } +} + #[derive(Debug)] pub enum AppQueryKind { // List CRUD @@ -176,6 +187,8 @@ pub enum AppQueryKind { RemoveAdmin(RemoveAdmin), AddMember(AddMember), RemoveMember(RemoveMember), + GetMemberTeams(GetMemberTeams), + GetAdminTeams(GetAdminTeams), } #[derive(Debug)] @@ -405,6 +418,8 @@ impl AppContext { AppQueryKind::RemoveAdmin(r) => self.remove_admin(r), AppQueryKind::AddMember(r) => self.add_member(r), AppQueryKind::RemoveMember(r) => self.remove_member(r), + AppQueryKind::GetAdminTeams(r) => self.get_admin_teams(r), + AppQueryKind::GetMemberTeams(r) => self.get_member_teams(r), }; if let Err(e) = msg.sender.send(r) { trace!("Failed send response: {:?}", e); @@ -561,13 +576,12 @@ impl AppContext { } fn get_lists(&self, r: GetLists) -> Result { - let t: EntityTypeName = "List".parse().unwrap(); self.is_authorized(&r.uid, &*ACTION_GET_LISTS, &*APPLICATION_TINY_TODO)?; Ok(AppResponse::Lists( self.entities .euids() - .filter(|euid| euid.type_name() == &t) + .filter(|euid| euid.type_name() == &*TYPE_LIST) .filter(|euid| self.is_authorized(&r.uid, &*ACTION_GET_LIST, euid).is_ok()) .cloned() .collect::>() @@ -575,6 +589,165 @@ impl AppContext { )) } + fn make_dummy_request(&self) -> Request { + Request::new(None, None, None, Context::empty(), None).expect("should be a valid request") + } + + fn reauthorize_with_concrete_resource( + &self, + partial_response: &PartialResponse, + euid: &EntityUid, + entities: &Entities, + ) -> bool { + matches!( + partial_response.reauthorize( + HashMap::from_iter( + std::iter::once( + ("resource".into(), RestrictedExpression::new_entity_uid(cedar_policy::EntityUid::from((*euid).clone()))) + ) + ), + &self.authorizer, + self.make_dummy_request(), + &entities), + Ok(r) if matches!(r.decision(), Some(Decision::Allow))) + } + + fn get_admin_teams(&self, r: GetAdminTeams) -> Result { + let entities = self.entities.as_entities(&self.schema); + let partial_request_add = RequestBuilder::default() + .action(Some(ACTION_ADD_ADMIN.as_ref().clone().into())) + .principal(Some(cedar_policy::EntityUid::from(EntityUid::from( + r.uid.clone(), + )))) + .context( + Context::from_pairs(std::iter::once(( + "candidate".to_owned(), + RestrictedExpression::new_entity_uid( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + cedar_policy::EntityId::new(""), + ), + ), + ))) + .unwrap(), + ) + .build(); + let partial_request_remove = RequestBuilder::default() + .action(Some(ACTION_REMOVE_ADMIN.as_ref().clone().into())) + .principal(Some(cedar_policy::EntityUid::from(EntityUid::from( + r.uid.clone(), + )))) + .context( + Context::from_pairs(std::iter::once(( + "candidate".to_owned(), + RestrictedExpression::new_entity_uid( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + cedar_policy::EntityId::new(""), + ), + ), + ))) + .unwrap(), + ) + .build(); + let partial_response_add = + self.authorizer + .is_authorized_partial(&partial_request_add, &self.policies, &entities); + let partial_response_remove = self.authorizer.is_authorized_partial( + &partial_request_remove, + &self.policies, + &entities, + ); + + let slice = self + .entities + .euids() + .filter(|euid| euid.type_name() == &*TYPE_TEAM); + + Ok(AppResponse::Teams( + slice + .filter(|euid| { + self.reauthorize_with_concrete_resource(&partial_response_add, euid, &entities) + || self.reauthorize_with_concrete_resource( + &partial_response_remove, + euid, + &entities, + ) + }) + .cloned() + .collect::>() + .into(), + )) + } + + fn get_member_teams(&self, r: GetMemberTeams) -> Result { + let entities = self.entities.as_entities(&self.schema); + let partial_request_add = RequestBuilder::default() + .action(Some(ACTION_ADD_MEMBER.as_ref().clone().into())) + .principal(Some(cedar_policy::EntityUid::from(EntityUid::from( + r.uid.clone(), + )))) + .context( + Context::from_pairs(std::iter::once(( + "candidate".to_owned(), + RestrictedExpression::new_entity_uid( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + cedar_policy::EntityId::new(""), + ), + ), + ))) + .unwrap(), + ) + .build(); + let partial_request_remove = RequestBuilder::default() + .action(Some(ACTION_REMOVE_MEMBER.as_ref().clone().into())) + .principal(Some(cedar_policy::EntityUid::from(EntityUid::from( + r.uid.clone(), + )))) + .context( + Context::from_pairs(std::iter::once(( + "candidate".to_owned(), + RestrictedExpression::new_entity_uid( + cedar_policy::EntityUid::from_type_name_and_id( + TYPE_USER.clone(), + cedar_policy::EntityId::new(""), + ), + ), + ))) + .unwrap(), + ) + .build(); + let partial_response_add = + self.authorizer + .is_authorized_partial(&partial_request_add, &self.policies, &entities); + let partial_response_remove = self.authorizer.is_authorized_partial( + &partial_request_remove, + &self.policies, + &entities, + ); + + let slice = self + .entities + .euids() + .filter(|euid| euid.type_name() == &*TYPE_TEAM); + + Ok(AppResponse::Teams( + slice + .filter(|euid| { + self.reauthorize_with_concrete_resource(&partial_response_add, euid, &entities) + || self.reauthorize_with_concrete_resource( + &partial_response_remove, + euid, + &entities, + ) + }) + .cloned() + .collect::>() + .into(), + )) + } + fn create_team(&mut self, r: CreateTeam) -> Result { let u = UserUid::try_from(EntityUid::from( cedar_policy::EntityUid::from_type_name_and_id( diff --git a/tinytodo/src/util.rs b/tinytodo/src/util.rs index 6f9c9f9..fbbd09c 100644 --- a/tinytodo/src/util.rs +++ b/tinytodo/src/util.rs @@ -345,6 +345,17 @@ impl From> for Lists { } } +#[derive(Debug, Clone, Serialize)] +#[repr(transparent)] +#[serde(transparent)] +pub struct Teams(Vec); + +impl From> for Teams { + fn from(value: Vec) -> Self { + Self(value) + } +} + impl From for EntityUid { fn from(value: cedar_policy::EntityUid) -> Self { Self(value) From 8b412f617145c147d478501e1af8843c827d310f Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 16:24:27 -0700 Subject: [PATCH 10/15] fix Signed-off-by: Shaobo He --- tinytodo/src/context.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tinytodo/src/context.rs b/tinytodo/src/context.rs index ea15d2a..f2cc5b7 100644 --- a/tinytodo/src/context.rs +++ b/tinytodo/src/context.rs @@ -47,8 +47,6 @@ use crate::{ use crate::{api::ShareRole, util::UserOrTeamUid}; #[cfg(feature = "use-templates")] use cedar_policy::{PolicyId, SlotId}; -#[cfg(feature = "use-templates")] -use std::collections::HashMap; // There's almost certainly a nicer way to do this than having separate `sender` fields From 28a33f629791239cb6007c8129662326e2346735 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 18:11:16 -0700 Subject: [PATCH 11/15] bug fix Signed-off-by: Shaobo He --- tinytodo/src/api.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tinytodo/src/api.rs b/tinytodo/src/api.rs index 7cf3575..d149f6e 100644 --- a/tinytodo/src/api.rs +++ b/tinytodo/src/api.rs @@ -379,11 +379,13 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(warp::query::query::()) .and_then(simple_query::)) .or(warp::path("admin") - .and(warp::path("add")) - .and(warp::post()) - .and(with_app(chan.clone())) - .and(warp::body::json()) - .and_then(simple_query::) + .and( + warp::path("add") + .and(warp::post()) + .and(with_app(chan.clone())) + .and(warp::body::json()) + .and_then(simple_query::), + ) .or(warp::path("remove") .and(warp::delete()) .and(with_app(chan.clone())) @@ -395,15 +397,16 @@ pub async fn serve_api(chan: AppChannel, port: u16) { .and(with_app(chan.clone())) .and(warp::body::json()) .and_then(simple_query::)) - .or(warp::path("manage") - .and(warp::path("member")) - .and(with_app(chan.clone())) - .and(warp::query::query::()) - .and_then(simple_query::) - .or(warp::path("admin") + .or(warp::path("manage").and( + warp::path("member") .and(with_app(chan.clone())) - .and(warp::query::query::()) - .and_then(simple_query::))), + .and(warp::query::query::()) + .and_then(simple_query::) + .or(warp::path("admin") + .and(with_app(chan.clone())) + .and(warp::query::query::()) + .and_then(simple_query::)), + )), )), ); From e30eb4c83bdb5baf8e6f35084ce23880e19fa9d3 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Tue, 28 May 2024 18:11:26 -0700 Subject: [PATCH 12/15] add Python APIs Signed-off-by: Shaobo He --- tinytodo/tinytodo.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tinytodo/tinytodo.py b/tinytodo/tinytodo.py index 1c9356e..41b3ba3 100644 --- a/tinytodo/tinytodo.py +++ b/tinytodo/tinytodo.py @@ -239,6 +239,67 @@ def inner(list_of_lists): return inner +@web_req("Create User") +def create_user(_, name, job_level, location): + data = { + 'id': name, + 'joblevel': job_level, + 'location': location, + } + f = lambda _: 'Created User %s' % User(name) + return server.post('/api/user/create', data), f + +@web_req("Get User") +def get_user(_, name): + f = lambda u: str(u) + return server.get('/api/user/get?id=%s' % name), f + +@web_req("Create User") +def create_team(_, name, owner): + data = { + 'id': name, + 'owner': owner, + } + f = lambda _: 'Created Team %s' % Team(name) + return server.post('/api/team/create', data), f + +@web_req("Get Team") +def get_team(_, name): + f = lambda u: str(u) + return server.get('/api/team/get?id=%s' % name), f + +@web_req("Add Team Admin") +def add_admin(_, team, user, admin): + data = { + 'team': team, + 'user': user, + 'candidate': admin, + } + f = lambda _: 'Add admin {0} to team {1}'.format(User(admin), Team(team)) + return server.post('/api/team/admin/add', data), f + +@web_req("Add Team Member") +def add_member(_, team, user, admin): + data = { + 'team': team, + 'user': user, + 'candidate': admin, + } + f = lambda _: 'Add member {0} to team {1}'.format(User(admin), Team(team)) + return server.post('/api/team/member/add', data), f + +@web_req("Get Admin Teams") +def get_admin_teams(_, name): + user = User(name) + req = server.get('/api/team/manage/admin/get?uid=%s' % user.euid()) + return req, lambda v: str(v) + +@web_req("Get Member Teams") +def get_member_teams(_, name): + user = User(name) + req = server.get('/api/team/manage/member/get?uid=%s' % user.euid()) + return req, lambda v: str(v) + @web_req("Create List") def create_list(user, name): data = { From 27fa39e1ccf82f2c01dd557dc63f3e33fa6ccec5 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Thu, 30 May 2024 15:13:53 -0700 Subject: [PATCH 13/15] updates Signed-off-by: Shaobo He --- tinytodo/frontend/static/hello.css | 28 +++++++ tinytodo/frontend/templates/index.html | 5 ++ tinytodo/frontend/templates/list/create.html | 10 +++ tinytodo/frontend/templates/list/index.html | 84 ++++++++++++++++++++ tinytodo/frontend/templates/lists.html | 15 ++++ tinytodo/frontend/templates/login.html | 8 ++ tinytodo/frontend/templates/main.html | 30 +++++++ tinytodo/frontend/templates/teams.html | 24 ++++++ tinytodo/frontend/tinytodo-web.py | 73 +++++++++++++++++ 9 files changed, 277 insertions(+) create mode 100644 tinytodo/frontend/static/hello.css create mode 100644 tinytodo/frontend/templates/index.html create mode 100644 tinytodo/frontend/templates/list/create.html create mode 100644 tinytodo/frontend/templates/list/index.html create mode 100644 tinytodo/frontend/templates/lists.html create mode 100644 tinytodo/frontend/templates/login.html create mode 100644 tinytodo/frontend/templates/main.html create mode 100644 tinytodo/frontend/templates/teams.html create mode 100644 tinytodo/frontend/tinytodo-web.py diff --git a/tinytodo/frontend/static/hello.css b/tinytodo/frontend/static/hello.css new file mode 100644 index 0000000..789e04e --- /dev/null +++ b/tinytodo/frontend/static/hello.css @@ -0,0 +1,28 @@ + +ul.navbar { + list-style: none +} + +ul.navbar li { + display: inline-block; + padding: 0 7px; + position: relative; +} + +ul.navbar li:not(:last-child)::after { + content: ""; + border: 1px solid #e2e2e2; + border-width: 1px 1px 0 0 ; + position: absolute; + right: -3px; + top: 0; + height: 100%; +} + +.inlineSubmit { + background: none; + border: none; + color: blue; + text-decoration: underline; + cursor: pointer; +} diff --git a/tinytodo/frontend/templates/index.html b/tinytodo/frontend/templates/index.html new file mode 100644 index 0000000..85eb4d6 --- /dev/null +++ b/tinytodo/frontend/templates/index.html @@ -0,0 +1,5 @@ +{% extends "main.html" %} + {% block title %}Home{% endblock %} + {% block body %} + Tiny Todo Home + {% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/list/create.html b/tinytodo/frontend/templates/list/create.html new file mode 100644 index 0000000..053f9e4 --- /dev/null +++ b/tinytodo/frontend/templates/list/create.html @@ -0,0 +1,10 @@ +{% extends "main.html" %} +{% block title %}Create List{% endblock %} +{% block body %} +

Create A List

+
+

+

+
+ +{% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/list/index.html b/tinytodo/frontend/templates/list/index.html new file mode 100644 index 0000000..2c9ef54 --- /dev/null +++ b/tinytodo/frontend/templates/list/index.html @@ -0,0 +1,84 @@ +{% extends "main.html" %} +{% block title %} List Viewer {% endblock %} +{% block body %} + +

{{name}}

+

Contents

+
    + {% for item in items %} +
  • {{item.name}} +
    + + + +
    +
  • + {% endfor %} +
+

Add item:

+
+

+

+

+
+ +

Sharing

+

Readers

+Current Readers
+
    + {% for user in readers %} +
  • + {{ user.username }} +
    + + + + +
    +
  • + {% endfor %} +
+Add reader: +
+

+

+

+

+
+ +

Editors

+Current Editors
+
    + {% for user in editors%} +
  • + {{ user.username }} +
    + + + + +
    +
  • + {% endfor %} +
+Add editor: +
+

+

+

+

+
+ +{% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/lists.html b/tinytodo/frontend/templates/lists.html new file mode 100644 index 0000000..dca6c36 --- /dev/null +++ b/tinytodo/frontend/templates/lists.html @@ -0,0 +1,15 @@ +{% extends "main.html" %} +{% block title %} +Lists +{% endblock %} +{% block body %} +Create a new list +

Your Lists

+
    + {% for list in lists %} +
  • + {{list}} +
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/login.html b/tinytodo/frontend/templates/login.html new file mode 100644 index 0000000..267b938 --- /dev/null +++ b/tinytodo/frontend/templates/login.html @@ -0,0 +1,8 @@ +{% extends "main.html" %} +{% block title %} Login {% endblock %} +{% block body %} +
+

+

+
+{% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/main.html b/tinytodo/frontend/templates/main.html new file mode 100644 index 0000000..f409a3e --- /dev/null +++ b/tinytodo/frontend/templates/main.html @@ -0,0 +1,30 @@ + + + + + >TinyTodo - {% block title %}{% endblock %} + + + + + {% block body %} {% endblock %} + + \ No newline at end of file diff --git a/tinytodo/frontend/templates/teams.html b/tinytodo/frontend/templates/teams.html new file mode 100644 index 0000000..0a7ecca --- /dev/null +++ b/tinytodo/frontend/templates/teams.html @@ -0,0 +1,24 @@ +{% extends "main.html" %} + +{% block title %} My Teams {% endblock %} +{% block body %} +

My Teams

+Create a new team +

Teams you can add/remove members

+
    + {% for team in admin_teams %} +
  • + {{team}} +
  • + {% endfor %} +
+

Teams you can add/remove admins

+
    + {% for team in member_teams %} +
  • + {{team}} +
  • + {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/tinytodo-web.py b/tinytodo/frontend/tinytodo-web.py new file mode 100644 index 0000000..7309e41 --- /dev/null +++ b/tinytodo/frontend/tinytodo-web.py @@ -0,0 +1,73 @@ +import flask +import requests +import json + +APP = flask.Flask(__name__) +# Lol don't do this +APP.secret_key = b'1234' + +def render_page(page, **kwargs): + if 'username' in flask.session: + print('is logged in') + return flask.render_template(page, user=flask.session['username'], **kwargs) + else: + return flask.render_template(page, **kwargs) + +@APP.route("/") +def index(): + return render_page('index.html') + +@APP.route('/login', methods=['GET', 'POST']) +def login(): + if flask.request.method == 'POST': + username = flask.request.form['username'] + flask.session['username'] = username + return flask.redirect(flask.url_for('index')) + else: + return render_page('login.html') + +@APP.route("/logout") +def logout(): + flask.session.pop('username', None) + return flask.redirect(flask.url_for('index')) + +@APP.route('/lists/') +def get_lists(): + r = requests.get('http://localhost:8080/api/lists/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])}) + return render_page('lists.html', lists=r.json()) + +@APP.route('/teams') +def get_teams(): + r = requests.get('http://localhost:8080/api/team/manage/member/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])}) + member_teams = r.json() + r = requests.get('http://localhost:8080/api/team/manage/admin/get', params= {'uid': 'User::"{0}"'.format(flask.session['username'])}) + admin_teams = r.json() + return render_page('teams.html', member_teams=member_teams, admin_teams=admin_teams) + +@APP.route('/list/create', methods=['GET']) +def get_create_list(): + return render_page('list/create.html') + +@APP.route('/list/create', methods=['POST']) +def post_create_list(): + try: + name = flask.request.form['name'] + owner = 'User::"{0}"'.format(flask.session['username']) + data = {'uid': owner, 'name': name} + requests.post('http://localhost:8080/api/list/create', json=data) + return flask.redirect(f'/lists') + except KeyError: + return 'Bad args!', 403 + +@APP.route('/list/read') +def get_list(): + list = flask.request.args['name'] + r = requests.get('http://localhost:8080/api/list/get', params= {'uid': 'User::"{0}"'.format(flask.session['username']), 'list': list}) + l = r.json() + print(l) + return render_page('list/index.html', name=l['name'], items=l['tasks'], readers=l['readers'], editors=l['editors']) + + +if __name__ == '__main__': + APP.debug = True + APP.run() \ No newline at end of file From 1bea751a559e7123ed3ab6fe1a439e4e0445afd5 Mon Sep 17 00:00:00 2001 From: Shaobo He Date: Mon, 3 Jun 2024 10:31:38 -0700 Subject: [PATCH 14/15] draft Signed-off-by: Shaobo He --- tinytodo/entities.json | 3 + tinytodo/frontend/templates/list/index.html | 17 +++ tinytodo/frontend/templates/lists.html | 4 +- tinytodo/frontend/templates/main.html | 4 +- tinytodo/frontend/templates/team/create.html | 10 ++ tinytodo/frontend/templates/team/index.html | 38 +++++++ tinytodo/frontend/templates/teams.html | 8 +- tinytodo/frontend/tinytodo-web.py | 99 +++++++++++++++-- tinytodo/src/api.rs | 71 ++++++------ tinytodo/src/context.rs | 111 +++++++------------ tinytodo/src/entitystore.rs | 4 + tinytodo/src/objects.rs | 19 +++- tinytodo/src/util.rs | 12 -- 13 files changed, 264 insertions(+), 136 deletions(-) create mode 100644 tinytodo/frontend/templates/team/create.html create mode 100644 tinytodo/frontend/templates/team/index.html diff --git a/tinytodo/entities.json b/tinytodo/entities.json index 5accff1..0f3cca0 100644 --- a/tinytodo/entities.json +++ b/tinytodo/entities.json @@ -43,6 +43,7 @@ "uid": "Team::\"temp\"", "owner": "User::\"emina\"", "admins": ["User::\"emina\""], + "name": "temp", "parents": [ "Application::\"TinyTodo\"" ] @@ -51,6 +52,7 @@ "uid": "Team::\"admin\"", "owner": "User::\"emina\"", "admins": ["User::\"emina\""], + "name": "admin", "parents": [ "Application::\"TinyTodo\"" ] @@ -59,6 +61,7 @@ "uid": "Team::\"interns\"", "owner": "User::\"emina\"", "admins": ["User::\"emina\""], + "name": "interns", "parents": [ "Application::\"TinyTodo\"", "Team::\"temp\"" diff --git a/tinytodo/frontend/templates/list/index.html b/tinytodo/frontend/templates/list/index.html index 2c9ef54..7e622e6 100644 --- a/tinytodo/frontend/templates/list/index.html +++ b/tinytodo/frontend/templates/list/index.html @@ -21,7 +21,23 @@

Add item:

+

Sharing

+Add reader: +
+

+

+

+

+
+Add editor: +
+

+

+

+

+
+ {% endblock %} \ No newline at end of file diff --git a/tinytodo/frontend/templates/lists.html b/tinytodo/frontend/templates/lists.html index dca6c36..78939ad 100644 --- a/tinytodo/frontend/templates/lists.html +++ b/tinytodo/frontend/templates/lists.html @@ -4,11 +4,11 @@ {% endblock %} {% block body %} Create a new list -

Your Lists

+

My Lists

diff --git a/tinytodo/frontend/templates/main.html b/tinytodo/frontend/templates/main.html index f409a3e..730c197 100644 --- a/tinytodo/frontend/templates/main.html +++ b/tinytodo/frontend/templates/main.html @@ -10,11 +10,11 @@