Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(jans-cedarling): handle recoverable errors gracefully in authorization #10721

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
40 changes: 2 additions & 38 deletions jans-cedarling/bindings/cedarling_python/PYTHON_TYPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,47 +205,11 @@ error : str
The error message describing the evaluation failure.
___

# authorize_errors.ActionError
Error encountered while parsing Action to EntityUid
___

# authorize_errors.AuthorizeError
Exception raised by authorize_errors
___

# authorize_errors.BuildContextError
Error encountered while building the request context
___

# authorize_errors.BuildEntityError
Error encountered while running on strict id token trust mode
___

# authorize_errors.CreateContextError
Error encountered while validating context according to the schema
___

# authorize_errors.EntitiesError
Error encountered while collecting all entities
___

# authorize_errors.EntitiesToJsonError
Error encountered while parsing all entities to json for logging
___

# authorize_errors.IdTokenTrustModeError
Error encountered while running on strict id token trust mode
___

# authorize_errors.ProcessTokens
Error encountered while processing JWT token data
___

# authorize_errors.UserRequestValidationError
Error encountered while creating cedar_policy::Request for user entity principal
___

# authorize_errors.WorkloadRequestValidationError
Error encountered while creating cedar_policy::Request for workload entity principal
# authorize_errors.LoggingError
Error encountered while trying to write logs
___

2 changes: 2 additions & 0 deletions jans-cedarling/bindings/cedarling_python/cedarling_python.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ class AuthorizeResult:

def person(self) -> AuthorizeResultResponse | None: ...

def reason_input(self) -> String | None: ...


@final
class AuthorizeResultResponse:
Expand Down
14 changes: 7 additions & 7 deletions jans-cedarling/bindings/cedarling_python/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,16 @@ def load_yaml_to_env(yaml_path):

print()

# watch on the decision for person
person_result = authorize_result.person()
print(f"Result of person authorization: {person_result.decision}")
person_diagnostic = person_result.diagnostics
# watch on the decision for user
user_result = authorize_result.user()
print(f"Result of user authorization: {user_result.decision}")
user_diagnostic = user_result.diagnostics
print("Policy ID used:")
for diagnostic in person_diagnostic.reason:
for diagnostic in user_diagnostic.reason:
print(diagnostic)

print(f"Errors during authorization: {len(person_diagnostic.errors)}")
for diagnostic in person_diagnostic.errors:
print(f"Errors during authorization: {len(user_diagnostic.errors)}")
for diagnostic in user_diagnostic.errors:
print(diagnostic)

print()
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,14 @@ impl AuthorizeResult {
self.inner.workload.clone().map(|v| v.into())
}

/// Get the decision value for person/user
fn person(&self) -> Option<AuthorizeResultResponse> {
self.inner.person.clone().map(|v| v.into())
/// Get the decision value for user
fn user(&self) -> Option<AuthorizeResultResponse> {
self.inner.user.clone().map(|v| v.into())
}

/// The reason why the result was DENY in case it was because of a bad input
fn reason_input(&self) -> Option<String> {
self.inner.reason_input.as_ref().map(|e| e.to_string())
}
}

Expand Down
85 changes: 3 additions & 82 deletions jans-cedarling/bindings/cedarling_python/src/authorize/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,9 @@ create_exception!(

create_exception!(
authorize_errors,
DecodeTokens,
LoggingError,
AuthorizeError,
"Error encountered while decoding JWT token data"
);

create_exception!(
authorize_errors,
ProcessTokens,
AuthorizeError,
"Error encountered while processing JWT token data"
);

create_exception!(
authorize_errors,
ActionError,
AuthorizeError,
"Error encountered while parsing Action to EntityUid"
);

create_exception!(
authorize_errors,
CreateContextError,
AuthorizeError,
"Error encountered while validating context according to the schema"
);

create_exception!(
authorize_errors,
WorkloadRequestValidationError,
AuthorizeError,
"Error encountered while creating cedar_policy::Request for workload entity principal"
);

create_exception!(
authorize_errors,
UserRequestValidationError,
AuthorizeError,
"Error encountered while creating cedar_policy::Request for user entity principal"
);

create_exception!(
authorize_errors,
EntitiesError,
AuthorizeError,
"Error encountered while collecting all entities"
);

create_exception!(
authorize_errors,
EntitiesToJsonError,
AuthorizeError,
"Error encountered while parsing all entities to json for logging"
);

create_exception!(
authorize_errors,
BuildContextError,
AuthorizeError,
"Error encountered while building the request context"
);

create_exception!(
authorize_errors,
IdTokenTrustModeError,
AuthorizeError,
"Error encountered while running on strict id token trust mode"
);

create_exception!(
authorize_errors,
BuildEntityError,
AuthorizeError,
"Error encountered while running on strict id token trust mode"
"Error encountered while trying to write logs"
);

#[pyclass]
Expand Down Expand Up @@ -131,16 +61,7 @@ macro_rules! errors_functions {
// This function is used to convert `cedarling::AuthorizeError` to a Python exception.
// For each possible case of `AuthorizeError`, we have created a corresponding Python exception that inherits from `cedarling::AuthorizeError`.
errors_functions! {
ProcessTokens => ProcessTokens,
Action => ActionError,
CreateContext => CreateContextError,
WorkloadRequestValidation => WorkloadRequestValidationError,
UserRequestValidation => UserRequestValidationError,
Entities => EntitiesError,
EntitiesToJson => EntitiesToJsonError,
BuildContext => BuildContextError,
IdTokenTrustMode => IdTokenTrustModeError,
BuildEntity => BuildEntityError
Logging => LoggingError
}

pub fn authorize_errors_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
Expand Down
10 changes: 7 additions & 3 deletions jans-cedarling/bindings/cedarling_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ pub struct AuthorizeResult {
pub workload: Option<AuthorizeResultResponse>,
/// Result of authorization where principal is `Jans::User`
#[wasm_bindgen(getter_with_clone)]
pub person: Option<AuthorizeResultResponse>,
pub user: Option<AuthorizeResultResponse>,

#[wasm_bindgen(getter_with_clone)]
pub reason_input: Option<String>,

/// Result of authorization
/// true means `ALLOW`
Expand All @@ -192,9 +195,10 @@ impl From<cedarling::AuthorizeResult> for AuthorizeResult {
workload: value
.workload
.map(|v| AuthorizeResultResponse { inner: Rc::new(v) }),
person: value
.person
user: value
.user
.map(|v| AuthorizeResultResponse { inner: Rc::new(v) }),
reason_input: value.reason_input.as_ref().map(|x| x.to_string()),
decision: value.decision,
}
}
Expand Down
91 changes: 80 additions & 11 deletions jans-cedarling/cedarling/src/authz/authorize_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,29 @@
* Copyright (c) 2024, Gluu, Inc.
*/

use cedar_policy::Decision;
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use std::collections::HashSet;

use super::BadInputError;
use crate::bootstrap_config::WorkloadBoolOp;
use cedar_policy::{Decision, PolicyId};
use serde::{Serialize, Serializer, ser::SerializeStruct};
use std::collections::HashSet;

/// Result of authorization and evaluation cedar policy
/// based on the [Request](crate::models::request::Request) and policy store
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Serialize)]
pub struct AuthorizeResult {
/// Result of authorization where principal is `Jans::Workload`
#[serde(serialize_with = "serialize_opt_response")]
pub workload: Option<cedar_policy::Response>,
/// Result of authorization where principal is `Jans::User`
#[serde(serialize_with = "serialize_opt_response")]
pub person: Option<cedar_policy::Response>,
pub user: Option<cedar_policy::Response>,

/// Reasons why the authorization failed due to malformed inputs
#[serde(
serialize_with = "serialize_reason_input",
skip_serializing_if = "Option::is_none"
)]
pub reason_input: Option<BadInputError>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think It should be Vec because it can be more that one recoverable errors.

And in the issue is said that for "decision": "DENY" will be key with "errors" and for "decision": "DENY" will be key with "warnings".

It is planning?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think It should be Vec because it can be more that one recoverable errors.

We return immediately instead of going through the request the in the first recoverable error since there's no point continuing if for example the access token is malformed. In that context, is does Vec still make sense since i think it will always only have 1 item.

And in the issue is said that for "decision": "DENY" will be key with "errors" and for "decision": "DENY" will be key with "warnings".

Yeah, i just changed it from "warnings" to "reason_input" after looking at authzen. It looks like they prefer using "reason_*" for adding additional context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the task handle recoverable errors gracefully in authorization. As I understand it means that if we have error on build role (for example) or other entity, and we can continue to execute authorize request we should continue. And this kind of errors should be collected in the vector (and be logged).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nynymike what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with your statement, but I think the scope of the issue was beyond authz. I don't want the Cedarling to crash ever... it should keep running. Your web server never crashes... A crash should only happen if something really catastrophic happens. Right now it seems like there are too many reasons the PDP might just crash.
That was my goal here...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SafinWasi does flask-sidecar crash on bad input? Do you use try ... expect ... statement like here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sidecar does not crash on invalid input. It can catch cedarling exceptions and puts them in the response.


/// Result of authorization
/// true means `ALLOW`
Expand Down Expand Up @@ -66,17 +72,80 @@ where
}
}

impl<E> From<E> for AuthorizeResult
where
E: Into<BadInputError>,
{
fn from(value: E) -> Self {
Self {
reason_input: Some(value.into()),
decision: false,
workload: None,
user: None,
}
}
}

#[derive(Debug, Clone, Serialize)]
pub struct PrincipalResult {
pub decision: Option<Decision>,
pub reason_input: Option<String>,
pub reason_policy: Option<HashSet<PolicyId>>,
#[serde(
serialize_with = "serialize_errors_policy",
skip_serializing_if = "Vec::is_empty"
)]
pub errors_policy: Vec<cedar_policy::AuthorizationError>,
}

impl From<cedar_policy::Response> for PrincipalResult {
fn from(response: cedar_policy::Response) -> Self {
let reason_policy = response.diagnostics().reason().cloned().collect();
let errors_policy = response.diagnostics().errors().cloned().collect();

Self {
decision: Some(response.decision()),
reason_input: None,
reason_policy: Some(reason_policy),
errors_policy,
}
}
}

fn serialize_reason_input<S>(
error: &Option<BadInputError>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
error.as_ref().map(|e| e.to_string()).serialize(serializer)
}

fn serialize_errors_policy<S>(
errors: &[cedar_policy::AuthorizationError],
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let str_errors = errors.iter().map(|e| e.to_string()).collect::<String>();
str_errors.serialize(serializer)
}

impl AuthorizeResult {
/// Builder function for AuthorizeResult
pub(crate) fn new(
user_workload_operator: WorkloadBoolOp,
workload: Option<cedar_policy::Response>,
person: Option<cedar_policy::Response>,
user: Option<cedar_policy::Response>,
) -> Self {
let decision = calc_decision(user_workload_operator, &workload, &user);
Self {
decision: calc_decision(&user_workload_operator, &workload, &person),
decision,
reason_input: None,
user,
workload,
person,
}
}

Expand All @@ -98,7 +167,7 @@ impl AuthorizeResult {
/// If person and workload is present will be used operator (AND or OR) based on `CEDARLING_USER_WORKLOAD_BOOLEAN_OPERATION` bootstrap property.
/// If none present return false.
fn calc_decision(
user_workload_operator: &WorkloadBoolOp,
user_workload_operator: WorkloadBoolOp,
workload: &Option<cedar_policy::Response>,
person: &Option<cedar_policy::Response>,
) -> bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ impl fmt::Display for BuildUserEntityError {
} else {
writeln!(
f,
"failed to create User Entity due to the following errors:"
"failed to create User Entity due to the following errors: [{}]",
self.errors
.iter()
.map(|(tkn, err)| format!(
"tried building user entity using `{}` but failed: {}",
tkn, err
))
.collect::<Vec<String>>()
.join(", ")
)?;
for (token_kind, error) in &self.errors {
writeln!(f, "- TokenKind {:?}: {}", token_kind, error)?;
}
}
Ok(())
}
Expand Down
Loading
Loading