From 9027e7c21522ea1a20bba7c72ebb379717344866 Mon Sep 17 00:00:00 2001 From: Siim Liiser Date: Wed, 13 Dec 2023 12:31:47 +0200 Subject: [PATCH 1/2] Add optional additional entities to authz request Added an option to pass in entities to authorization requests that would not overwrite the configuration already stored in memory, but modify it by adding new entities to it. Should be used if your use case has a large amount of entities that is infeasible to store in memory and you have easy access to all relevant entities when making the authorization request. --- src/routes/authorization.rs | 29 ++++++++++++----- src/schemas/authorization.rs | 62 +++++++++++++++++++++++++++--------- tests/services/data_tests.rs | 18 ++++++----- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/routes/authorization.rs b/src/routes/authorization.rs index f014772..c316b7a 100644 --- a/src/routes/authorization.rs +++ b/src/routes/authorization.rs @@ -1,4 +1,4 @@ -use cedar_policy::Authorizer; +use cedar_policy::{Authorizer, Entities}; use log::info; @@ -19,7 +19,7 @@ pub async fn is_authorized( data_store: &State>, authorizer: &State, authorization_call: Json, -) -> Result, AgentError> { +) -> Result, AgentError> { let policies = policy_store.policy_set().await; let query: AuthorizationRequest = match authorization_call.into_inner().try_into() { Ok(query) => query, @@ -30,16 +30,29 @@ pub async fn is_authorized( } }; - // Temporary solution to override fetching entities from the datastore by directly passing it to the REST body. - // Eventually this logic will be replaced in favor of performing live patch updates - let (request, entities) = &query.get_request_entities(); + // Temporary solution to override fetching entities from the datastore by directly passing it to the REST body. + // Eventually this logic will be replaced in favor of performing live patch updates + let (request, entities, additional_entities) = &query.get_request_entities(); let request_entities = match entities { None => data_store.entities().await, Some(ents) => ents.clone() - }; - + }; + let patched_entities = match additional_entities { + None => request_entities, + Some(ents) => { + match Entities::from_entities(request_entities.iter().chain(ents.iter()).cloned()) { + Ok(entities) => entities, + Err(err) => { + return Err(AgentError::BadRequest { + reason: err.to_string(), + }) + } + } + } + }; + info!("Querying cedar using {:?}", &request); - let answer = authorizer.is_authorized(&request, &policies, &request_entities); + let answer = authorizer.is_authorized(&request, &policies, &patched_entities); Ok(Json::from(AuthorizationAnswer::from(answer))) } diff --git a/src/schemas/authorization.rs b/src/schemas/authorization.rs index 531a3c3..2e6f2ae 100644 --- a/src/schemas/authorization.rs +++ b/src/schemas/authorization.rs @@ -19,34 +19,41 @@ pub struct AuthorizationCall { resource: Option, context: Option, entities: Option, + additional_entities: Option, /// Optional schema in JSON format. /// If present, this will inform the parsing: for instance, it will allow /// `__entity` and `__extn` escapes to be implicit, and it will error if /// attributes have the wrong types (e.g., string instead of integer). /// currently unsupported #[schemars(skip)] - policies: Option, - + policies: Option, } pub struct AuthorizationRequest { request: Request, - entities: Option + entities: Option, + additional_entities: Option, } impl AuthorizationRequest { - - pub fn new(request: Request, entities: Option) -> AuthorizationRequest { - AuthorizationRequest { request, entities } + pub fn new( + request: Request, + entities: Option, + additional_entities: Option, + ) -> AuthorizationRequest { + AuthorizationRequest { + request, + entities, + additional_entities, + } } pub fn get_entities(self) -> Option { self.entities } - pub fn get_request_entities(self) -> (Request, Option) { - - (self.request, self.entities) + pub fn get_request_entities(self) -> (Request, Option, Option) { + (self.request, self.entities, self.additional_entities) } } @@ -61,11 +68,25 @@ fn string_to_euid(optional_str: Option) -> Result, Par } impl AuthorizationCall { - - pub fn new(principal: Option, action: Option, resource: Option, context:Option, entities:Option, policies: Option) -> AuthorizationCall { - AuthorizationCall { principal, action, resource, context, entities, policies } + pub fn new( + principal: Option, + action: Option, + resource: Option, + context: Option, + entities: Option, + additional_entities: Option, + policies: Option, + ) -> AuthorizationCall { + AuthorizationCall { + principal, + action, + resource, + context, + entities, + additional_entities, + policies, + } } - } impl TryInto for AuthorizationCall { @@ -84,7 +105,7 @@ impl TryInto for AuthorizationCall { Ok(r) => r, Err(e) => return Err(e.into()), }; - let entities = match self.entities { + let entities = match self.entities { Some(et) => match Entities::from_json_value(et, None) { Ok(et) => { Some(et) @@ -93,6 +114,13 @@ impl TryInto for AuthorizationCall { }, None => None, }; + let additional_entities = match self.additional_entities { + Some(et) => match Entities::from_json_value(et, None) { + Ok(et) => Some(et), + Err(e) => return Err(e.into()), + }, + None => None, + }; let context = match self.context { Some(c) => match Context::from_json_value(c, None) { Ok(c) => c, @@ -100,7 +128,11 @@ impl TryInto for AuthorizationCall { }, None => Context::empty(), }; - Ok(AuthorizationRequest::new(Request::new(principal, action, resource, context), entities)) + Ok(AuthorizationRequest::new( + Request::new(principal, action, resource, context), + entities, + additional_entities, + )) } } diff --git a/tests/services/data_tests.rs b/tests/services/data_tests.rs index 217a613..fc32805 100644 --- a/tests/services/data_tests.rs +++ b/tests/services/data_tests.rs @@ -35,11 +35,11 @@ async fn test_load_entities_from_file() { } #[tokio::test] -async fn test_load_empty_entities_from_authz_call() { +async fn test_load_empty_entities_from_authz_call() { let entities: String = String::from("[]"); - let query = make_authz_call(entities); + let query = make_authz_call(entities); match query { Ok(req) => assert_eq!(req.get_entities().unwrap(), Entities::empty()), @@ -48,7 +48,7 @@ async fn test_load_empty_entities_from_authz_call() { } #[tokio::test] -async fn test_load_no_entities_from_authz_call() { +async fn test_load_no_entities_from_authz_call() { let query = make_authz_call_no_entities(); @@ -60,7 +60,7 @@ async fn test_load_no_entities_from_authz_call() { #[tokio::test] -async fn test_load_entities_from_authz_call() { +async fn test_load_entities_from_authz_call() { let entities: String = r#" [ @@ -89,13 +89,13 @@ async fn test_load_entities_from_authz_call() { "# .to_string(); - let query = make_authz_call(entities); + let query = make_authz_call(entities); match query { - Ok(req) => { - assert_ne!(req.get_entities(), None); + Ok(req) => { + assert_ne!(req.get_entities(), None); }, - _ => assert!(false) + _ => assert!(false) }; } @@ -111,6 +111,7 @@ fn make_authz_call_no_entities() -> Result> None, None, None, + None, ); return authorization_call.try_into(); } @@ -127,6 +128,7 @@ fn make_authz_call(entities: String) -> Result Date: Wed, 3 Jan 2024 11:14:53 +0200 Subject: [PATCH 2/2] fixup! Add optional additional entities to authz request --- src/routes/authorization.rs | 28 +++------ src/schemas/authorization.rs | 18 +++++- tests/services/data_tests.rs | 111 ++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 22 deletions(-) diff --git a/src/routes/authorization.rs b/src/routes/authorization.rs index c316b7a..f65f00f 100644 --- a/src/routes/authorization.rs +++ b/src/routes/authorization.rs @@ -1,4 +1,4 @@ -use cedar_policy::{Authorizer, Entities}; +use cedar_policy::Authorizer; use log::info; @@ -32,27 +32,17 @@ pub async fn is_authorized( // Temporary solution to override fetching entities from the datastore by directly passing it to the REST body. // Eventually this logic will be replaced in favor of performing live patch updates - let (request, entities, additional_entities) = &query.get_request_entities(); - - let request_entities = match entities { - None => data_store.entities().await, - Some(ents) => ents.clone() - }; - let patched_entities = match additional_entities { - None => request_entities, - Some(ents) => { - match Entities::from_entities(request_entities.iter().chain(ents.iter()).cloned()) { - Ok(entities) => entities, - Err(err) => { - return Err(AgentError::BadRequest { - reason: err.to_string(), - }) - } - } + let stored_entities = data_store.entities().await; + let (request, entities) = match query.get_request_entities(stored_entities) { + Ok(result) => result, + Err(err)=> { + return Err(AgentError::BadRequest { + reason: err.to_string(), + }) } }; info!("Querying cedar using {:?}", &request); - let answer = authorizer.is_authorized(&request, &policies, &patched_entities); + let answer = authorizer.is_authorized(&request, &policies, &entities); Ok(Json::from(AuthorizationAnswer::from(answer))) } diff --git a/src/schemas/authorization.rs b/src/schemas/authorization.rs index 2e6f2ae..61155c6 100644 --- a/src/schemas/authorization.rs +++ b/src/schemas/authorization.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use cedar_policy::{Context, EntityUid, EvaluationError, Request, Response, Entities}; use cedar_policy_core::authorizer::Decision; use cedar_policy_core::parser::err::ParseErrors; +use cedar_policy_core::entities::EntitiesError; use rocket::serde::json::serde_json; use rocket_okapi::okapi::schemars; @@ -52,8 +53,21 @@ impl AuthorizationRequest { self.entities } - pub fn get_request_entities(self) -> (Request, Option, Option) { - (self.request, self.entities, self.additional_entities) + pub fn get_request_entities(self, stored_entities: Entities) -> Result<(Request, Entities), EntitiesError> { + let request_entities = match self.entities { + None => stored_entities, + Some(ents) => ents.clone() + }; + let patched_entities = match self.additional_entities { + None => request_entities, + Some(ents) => { + match Entities::from_entities(request_entities.iter().chain(ents.iter()).cloned()) { + Ok(entities) => entities, + Err(err) => return Err(err) + } + } + }; + Ok((self.request, patched_entities)) } } diff --git a/tests/services/data_tests.rs b/tests/services/data_tests.rs index fc32805..d95f953 100644 --- a/tests/services/data_tests.rs +++ b/tests/services/data_tests.rs @@ -99,6 +99,97 @@ async fn test_load_entities_from_authz_call() { }; } +#[tokio::test] +async fn test_combine_entities_with_additional_entities(){ + let stored_entities: String = r#" + [ + { + "attrs": {}, + "parents": [ + { + "id": "Admin", + "type": "Role" + } + ], + "uid": { + "id": "admin.1@domain.com", + "type": "User" + } + }, + { + "attrs": {}, + "parents": [], + "uid": { + "id": "delete", + "type": "Action" + } + } + ] + "# + .to_string(); + + let additional_entities: String = r#" + [ + { + "attrs": {}, + "parents": [], + "uid": { + "id": "Admin", + "type": "Role" + } + } + ] + "#.to_string(); + + let expected_result: String = r#" + [ + { + "attrs": {}, + "parents": [ + { + "id": "Admin", + "type": "Role" + } + ], + "uid": { + "id": "admin.1@domain.com", + "type": "User" + } + }, + { + "attrs": {}, + "parents": [], + "uid": { + "id": "delete", + "type": "Action" + } + }, + { + "attrs": {}, + "parents": [], + "uid": { + "id": "Admin", + "type": "Role" + } + } + ] + "#.to_string(); + + let query = make_authz_call_with_additional_entities(additional_entities); + + match query { + Ok(req) => { + match req.get_request_entities(Entities::from_json_str(&stored_entities, None).unwrap()) { + Ok((_request, entities)) => { + assert_eq!(entities, Entities::from_json_str(&expected_result, None).unwrap()) + }, + _ => assert!(false) + }; + }, + _ => assert!(false) + }; +} + fn make_authz_call_no_entities() -> Result> { let principal: Option = Some("User::\"Test\"".to_string()); let action: Option = Some("Action::\"Delete\"".to_string()); @@ -131,5 +222,23 @@ fn make_authz_call(entities: String) -> Result Result> { + let principal: Option = Some("User::\"Test\"".to_string()); + let action: Option = Some("Action::\"Delete\"".to_string()); + let resource: Option = Some("Document::\"cedar-agent.pdf\"".to_string()); -} \ No newline at end of file + let authorization_call = AuthorizationCall::new( + principal, + action, + resource, + None, + None, + rocket::serde::json::from_str(&additional_entities).unwrap(), + None, + ); + return authorization_call.try_into(); +}