Skip to content

Commit

Permalink
feat(jans-cedarling): pass entities data into the context (#10275)
Browse files Browse the repository at this point in the history
* feat(jans-cedarling): pass entities data into the context

- automatically pass in the `user`, `workload`, `access_token`,
  `id_token`, and `userinfo_token` entities into the Cedar context to be
  able to be used for ABAC.

Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com>

* chore(jans-cedarling): remove print statement

Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com>

* chore(jans-cedarling): rename add_entities_to_context to build_context

Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com>

---------

Signed-off-by: rmarinn <34529290+rmarinn@users.noreply.github.com>
Co-authored-by: Oleh <olehbozhok@gmail.com>
  • Loading branch information
rmarinn and olehbozhok authored Nov 28, 2024
1 parent 16371ee commit e2e4f89
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ create_exception!(
"Error encountered while collecting all entities"
);

create_exception!(
authorize_errors,
AddEntitiesIntoContextError,
AuthorizeError,
"Error encountered while adding entities into context"
);

#[pyclass]
#[derive()]
pub struct ErrorPayload(CedarlingAuthorizeError);
Expand Down Expand Up @@ -157,7 +164,8 @@ errors_functions! {
CreateRequestWorkloadEntity => CreateRequestWorkloadEntityError,
CreateRequestUserEntity => CreateRequestUserEntityError,
CreateRequestRoleEntity => CreateRequestRoleEntityError,
Entities => EntitiesError
Entities => EntitiesError,
AddEntitiesIntoContext => AddEntitiesIntoContextError
}

pub fn authorize_errors_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
Expand Down
52 changes: 52 additions & 0 deletions jans-cedarling/cedarling/src/authz/merge_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use serde_json::Value;

#[derive(Debug, thiserror::Error)]
pub enum MergeError {
#[error("Failed to merge JSON objects due to conflicting keys: {0}")]
KeyConflict(String),
}

pub fn merge_json_values(mut base: Value, other: Value) -> Result<Value, MergeError> {
if let (Some(base_map), Some(additional_map)) = (base.as_object_mut(), other.as_object()) {
for (key, value) in additional_map {
if base_map.contains_key(key) {
return Err(MergeError::KeyConflict(key.clone()));
}
base_map.insert(key.clone(), value.clone());
}
}
Ok(base)
}

#[cfg(test)]
mod test {
use serde_json::json;

use crate::authz::merge_json::MergeError;

use super::merge_json_values;

#[test]
fn can_merge_json_objects() {
let obj1 = json!({ "a": 1, "b": 2 });
let obj2 = json!({ "c": 3, "d": 4 });
let expected = json!({"a": 1, "b": 2, "c": 3, "d": 4});

let result = merge_json_values(obj1, obj2).expect("Should merge JSON objects");

assert_eq!(result, expected);
}

#[test]
fn errors_on_same_keys() {
// Test for only two objects
let obj1 = json!({ "a": 1, "b": 2 });
let obj2 = json!({ "b": 3, "c": 4 });
let result = merge_json_values(obj1, obj2);

assert!(
matches!(result, Err(MergeError::KeyConflict(key)) if key.as_str() == "b"),
"Expected an error due to conflicting keys"
);
}
}
57 changes: 50 additions & 7 deletions jans-cedarling/cedarling/src/authz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::log::{
};

mod authorize_result;
mod merge_json;

mod entities;
pub(crate) mod request;
Expand All @@ -36,7 +37,9 @@ use entities::{
create_userinfo_token_entity, AccessTokenEntitiesError, RoleEntityError,
};
use entities::{create_resource_entity, AccessTokenEntities};
use merge_json::{merge_json_values, MergeError};
use request::Request;
use serde_json::{json, Value};
use token_data::{AccessTokenData, IdTokenData, UserInfoTokenData};

/// Configuration to Authz to initialize service without errors
Expand Down Expand Up @@ -85,12 +88,6 @@ impl Authz {
let action = cedar_policy::EntityUid::from_str(request.action.as_str())
.map_err(AuthorizeError::Action)?;

// Parse context.
let context: cedar_policy::Context = cedar_policy::Context::from_json_value(
request.context.clone(),
Some((&schema.schema, &action)),
)?;

// Parse [`cedar_policy::Entity`]-s to [`AuthorizeEntitiesData`] that hold all entities (for usability).
let entities_data: AuthorizeEntitiesData = self.authorize_entities_data(&request)?;

Expand All @@ -99,6 +96,13 @@ impl Authz {
let resource_uid = entities_data.resource_entity.uid();
let principal_user_entity_uid = entities_data.user_entity.uid();

let context = add_entities_to_context(
request.context.clone(),
&entities_data,
&schema.schema,
&action,
)?;

// Convert [`AuthorizeEntitiesData`] to [`cedar_policy::Entities`] structure,
// hold all entities that will be used on authorize check.
let entities = entities_data.entities(Some(&schema.schema))?;
Expand Down Expand Up @@ -218,7 +222,6 @@ impl Authz {

let role_entities = create_role_entities(policy_store, &decode_result, trusted_issuer)?;

// Populate the `AuthorizeEntitiesData` structure using the builder pattern
let data = AuthorizeEntitiesData::builder()
// Populate the structure with entities derived from the access token
.access_token_entities(create_access_token_entities(
Expand Down Expand Up @@ -259,6 +262,43 @@ impl Authz {
}
}

/// Constructs the authorization context by adding the built entities from the tokens
fn add_entities_to_context(
request_context: Value,
entities_data: &AuthorizeEntitiesData,
schema: &cedar_policy::Schema,
action: &cedar_policy::EntityUid,
) -> Result<cedar_policy::Context, AuthorizeError> {
let entities_context = json!({
"user": {"__entity": {
"type": entities_data.user_entity.uid().type_name().to_string(),
"id": entities_data.user_entity.uid().id().escaped(),
}},
"workload": {"__entity": {
"type": entities_data.access_token_entities.workload_entity.uid().type_name().to_string(),
"id": entities_data.access_token_entities.workload_entity.uid().id().escaped(),
}},
"access_token": {"__entity": {
"type": entities_data.access_token_entities.access_token_entity.uid().type_name().to_string(),
"id": entities_data.access_token_entities.access_token_entity.uid().id().escaped(),
}},
"id_token": {"__entity": {
"type": entities_data.id_token_entity.uid().type_name().to_string(),
"id": entities_data.id_token_entity.uid().id().escaped(),
}},
"userinfo_token": {"__entity": {
"type": entities_data.userinfo_token.uid().type_name().to_string(),
"id": entities_data.userinfo_token.uid().id().escaped(),
}},
});
let context = merge_json_values(entities_context, request_context)?;

let context: cedar_policy::Context =
cedar_policy::Context::from_json_value(context, Some((schema, action)))?;

Ok(context)
}

/// Helper struct to hold named parameters for [`Authz::execute_authorize`] method.
struct ExecuteAuthorizeParameters<'a> {
entities: &'a Entities,
Expand Down Expand Up @@ -350,6 +390,9 @@ pub enum AuthorizeError {
/// Error encountered while collecting all entities
#[error("could not collect all entities: {0}")]
Entities(#[from] cedar_policy::entities_errors::EntitiesError),
/// Error encountered while validating context according to the schema
#[error("could not add entities into context")]
AddEntitiesIntoContext(#[from] MergeError),
}

#[derive(Debug, derive_more::Error, derive_more::Display)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{cmp_decision, cmp_policy}; // macros is defined in the cedarling\src
use test_utils::assert_eq;

static POLICY_STORE_RAW_YAML: &str = include_str!("../../../test_files/policy-store_ok_2.yaml");
static POLICY_STORE_ABAC_YAML: &str = include_str!("../../../test_files/policy-store_ok_abac.yaml");

/// Success test case where all check a successful
/// role field in the `userinfo_token` because we search here by default
Expand Down Expand Up @@ -847,3 +848,70 @@ fn only_workload_and_role_permit() {

assert!(result.is_allowed(), "request result should be allowed");
}

#[test]
fn success_test_role_string_with_abac() {
let cedarling = get_cedarling(PolicyStoreSource::Yaml(POLICY_STORE_ABAC_YAML.to_string()));

// deserialize `Request` from json
let request = Request::deserialize(serde_json::json!(
{
"access_token": generate_token_using_claims(json!({
"org_id": "some_long_id",
"jti": "token1",
"client_id": "some_client_id",
"iss": "https://account.gluu.org",
"aud": "client123",
"exp": i64::MAX,
"iat": 0,
"name": "Worker123",
})),
"id_token": generate_token_using_claims(json!({
"jti": "token2",
"iss": "https://account.gluu.org",
"aud": "client123",
"sub": "some_sub",
"exp": i64::MAX,
"iat": 0,
"amr": "some_amr",
"acr": "some_acr",
})),
"userinfo_token": generate_token_using_claims(json!({
"jti": "token3",
"iss": "https://account.gluu.org",
"sub": "some_sub",
"country": "US",
"role": "Worker",
"email": "some_email@gluu.org",
"client_id": "some_client_id",
"username": "worker123",
"exp": i64::MAX,
"iat": 0,
})),
"action": "Jans::Action::\"Update\"",
"resource": {
"id": "random_id",
"type": "Jans::Issue",
"org_id": "some_long_id",
"country": "US"
},
"context": {},
}
))
.expect("Request should be deserialized from json");

let result = cedarling
.authorize(request)
.expect("request should be parsed without errors");

cmp_decision!(
result.workload,
Decision::Allow,
"request result should be allowed for workload"
);
cmp_policy!(
result.workload,
vec!["1"],
"reason of permit workload should be '1'"
);
}
9 changes: 8 additions & 1 deletion jans-cedarling/test_files/policy-store_ok.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ policy_stores:
entity Workload = {"client_id": String, "iss": TrustedIssuer, "name": String, "org_id": String};
entity id_token = {"acr": String, "amr": String, "aud": String, "exp": Long, "iat": Long, "iss": TrustedIssuer, "jti": String, "sub": String};
entity Userinfo_token = {"iss": String, "jti": String, "client_id": String};
type Context = {
user: User,
workload: Workload,
access_token: Access_token,
id_token: id_token,
userinfo_token: Userinfo_token,
};
action "Update" appliesTo {
principal: [Workload, User, Role],
resource: [Issue],
context: {}
context: Context
};
}
21 changes: 14 additions & 7 deletions jans-cedarling/test_files/policy-store_ok_2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,39 +72,46 @@ policy_stores:
entity Access_token = {"aud": String,"iss": String, "jti": String, "client_id": String,"org_id": String};
entity Userinfo_token = {"iss": String, "jti": String, "client_id": String};
entity Empty;
type Context = {
user: User,
workload: Workload,
access_token: Access_token,
id_token: id_token,
userinfo_token: Userinfo_token,
};
action "Update" appliesTo {
principal: [Workload, User, Role],
resource: [Issue],
context: {}
context: Context
};
action "UpdateForWorkload" appliesTo {
principal: [Workload],
resource: [Issue],
context: {}
context: Context
};
action "UpdateForUser" appliesTo {
principal: [User],
resource: [Issue],
context: {}
context: Context
};
action "UpdateForRole" appliesTo {
principal: [Role],
resource: [Issue],
context: {}
context: Context
};
action "UpdateForUserAndRole" appliesTo {
principal: [User,Role],
resource: [Issue],
context: {}
context: Context
};
action "UpdateForWorkloadAndRole" appliesTo {
principal: [Workload,Role],
resource: [Issue],
context: {}
context: Context
};
action "NoApplies" appliesTo {
principal: [Empty],
resource: [Issue],
context: {}
context: Context
};
}
Loading

0 comments on commit e2e4f89

Please sign in to comment.