#![allow(clippy::module_name_repetitions)]
use super::utils::{InterfaceResult, PolicySpecification};
use crate::api::EntityId;
use crate::api::EntityTypeName;
use crate::PolicyId;
use crate::{
Authorizer, Context, Decision, Entities, EntityUid, ParseErrors, Policy, PolicySet, Request,
Response, Schema, SlotId, Template,
};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
thread_local!(
static AUTHORIZER: Authorizer = Authorizer::new();
);
fn is_authorized(call: AuthorizationCall) -> Response {
match call.get_components() {
Ok((request, policies, entities)) => {
AUTHORIZER.with(|authorizer| authorizer.is_authorized(&request, &policies, &entities))
}
Err(ans) => ans,
}
}
pub fn json_is_authorized(input: &str) -> InterfaceResult {
serde_json::from_str::<AuthorizationCall>(input).map_or_else(
|e| InterfaceResult::fail_internally(format!("error parsing call: {e:}")),
|call| InterfaceResult::succeed(is_authorized(call)),
)
}
fn empty_failure<S: Into<String>>(msgs: impl IntoIterator<Item = S>) -> Response {
Response::new(
Decision::Deny,
HashSet::new(),
msgs.into_iter().map(Into::into).collect(),
)
}
fn error_to_response(e: impl std::error::Error, msg: String) -> Response {
empty_failure([msg, e.to_string()])
}
#[derive(Debug, Serialize, Deserialize)]
struct AuthorizationCall {
principal: Option<serde_json::Value>,
action: serde_json::Value,
resource: Option<serde_json::Value>,
context: serde_json::Value,
#[serde(rename = "schema")]
schema: Option<serde_json::Value>,
slice: RecvdSlice,
}
impl AuthorizationCall {
fn get_components(self) -> Result<(Request, PolicySet, Entities), Response> {
let schema = self
.schema
.map(Schema::from_json_value)
.transpose()
.map_err(|e| empty_failure([e.to_string()]))?;
let principal = match self.principal {
Some(p) => Some(
EntityUid::from_json(p)
.map_err(|e| error_to_response(e, "Failed to parse principal".into()))?,
),
None => None,
};
let action = EntityUid::from_json(self.action)
.map_err(|e| error_to_response(e, "Failed to parse action".into()))?;
let resource = match self.resource {
Some(r) => Some(
EntityUid::from_json(r)
.map_err(|e| error_to_response(e, "Failed to parse resource".into()))?,
),
None => None,
};
let context = Context::from_json_value(self.context, schema.as_ref().map(|s| (s, &action)))
.map_err(|e| empty_failure([e.to_string()]))?;
let q = Request::new(principal, Some(action), resource, context);
let (policies, entities) = self.slice.try_into(schema.as_ref())?;
Ok((q, policies, entities))
}
}
#[derive(Debug, Serialize, Deserialize)]
struct EntityUIDStrings {
ty: String,
eid: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct Link {
slot: String,
value: EntityUIDStrings,
}
#[derive(Debug, Serialize, Deserialize)]
struct TemplateLink {
template_id: String,
result_policy_id: String,
instantiations: Vec<Link>,
}
#[derive(Debug, Serialize, Deserialize)]
struct RecvdSlice {
policies: PolicySpecification,
entities: serde_json::Value,
templates: Option<HashMap<String, String>>,
template_instantiations: Option<Vec<TemplateLink>>,
}
impl RecvdSlice {
#[allow(clippy::too_many_lines)]
fn try_into(self, schema: Option<&Schema>) -> Result<(PolicySet, Entities), Response> {
fn parse_instantiation(v: &Link) -> Result<(SlotId, EntityUid), Vec<String>> {
let slot = match v.slot.as_str() {
"?principal" => SlotId::principal(),
"?resource" => SlotId::resource(),
_ => {
return Err(vec![
"Slot must by \"?principal\" or \"?resource\"".to_string()
]);
}
};
let type_name = EntityTypeName::from_str(v.value.ty.as_str());
let eid = EntityId::from_str(v.value.eid.as_str());
match (type_name, eid) {
(Ok(type_name), Ok(eid)) => {
let entity_uid = EntityUid::from_type_name_and_id(type_name, eid);
Ok((slot, entity_uid))
}
(Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e.errors_as_strings()),
(Err(mut e1), Err(mut e2)) => {
e1.0.append(&mut e2.0);
Err(ParseErrors(e1.0).errors_as_strings())
}
}
}
fn parse_instantiations(
policies: &mut PolicySet,
instantiation: TemplateLink,
) -> Result<(), Vec<String>> {
let template_id = PolicyId::from_str(instantiation.template_id.as_str());
let instance_id = PolicyId::from_str(instantiation.result_policy_id.as_str());
match (template_id, instance_id) {
(Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e.errors_as_strings()),
(Err(mut e1), Err(mut e2)) => {
e1.0.append(&mut e2.0);
Err(ParseErrors(e1.0).errors_as_strings())
}
(Ok(template_id), Ok(instance_id)) => {
let mut vals = HashMap::new();
for i in instantiation.instantiations {
match parse_instantiation(&i) {
Err(e) => return Err(e),
Ok(val) => vals.insert(val.0, val.1),
};
}
match policies.link(template_id, instance_id, vals) {
Ok(()) => Ok(()),
Err(e) => Err(vec![format!("Error instantiating template: {e}")]),
}
}
}
}
let Self {
policies,
entities,
templates,
template_instantiations,
} = self;
let policy_set = match policies {
PolicySpecification::Concatenated(policies) => match PolicySet::from_str(&policies) {
Ok(ps) => Ok(ps),
Err(parse_errors) => Err(std::iter::once(
"couldn't parse concatenated policies string".to_string(),
)
.chain(parse_errors.errors_as_strings())
.collect()),
},
PolicySpecification::Map(policies) => {
parse_policy_set_from_individual_policies(&policies, templates)
}
};
let mut errs = Vec::new();
let (mut policies, entities) =
match (Entities::from_json_value(entities, schema), policy_set) {
(Ok(entities), Ok(policies)) => (policies, entities),
(Ok(_), Err(policy_parse_errors)) => {
errs.extend(policy_parse_errors);
(PolicySet::new(), Entities::empty())
}
(Err(e), Ok(_)) => {
errs.push(e.to_string());
(PolicySet::new(), Entities::empty())
}
(Err(e), Err(policy_parse_errors)) => {
errs.push(e.to_string());
errs.extend(policy_parse_errors);
(PolicySet::new(), Entities::empty())
}
};
if let Some(t_inst_list) = template_instantiations {
for instantiation in t_inst_list {
match parse_instantiations(&mut policies, instantiation) {
Ok(()) => (),
Err(err) => errs.extend(err),
}
}
}
if errs.is_empty() {
Ok((policies, entities))
} else {
Err(empty_failure(errs))
}
}
}
fn parse_policy_set_from_individual_policies(
policies: &HashMap<String, String>,
templates: Option<HashMap<String, String>>,
) -> Result<PolicySet, Vec<String>> {
let mut policy_set = PolicySet::new();
let mut errs = Vec::new();
for (id, policy_src) in policies {
match Policy::parse(Some(id.clone()), policy_src) {
Ok(p) => match policy_set.add(p) {
Ok(()) => {}
Err(err) => {
errs.push(format!("couldn't add policy to set due to error: {err}"));
}
},
Err(pes) => errs.extend(
std::iter::once(format!("couldn't parse policy with id {id}"))
.chain(pes.errors_as_strings().into_iter()),
),
}
}
if let Some(templates) = templates {
for (id, policy_src) in templates {
match Template::parse(Some(id.clone()), policy_src) {
Ok(p) => match policy_set.add_template(p) {
Ok(()) => {}
Err(err) => {
errs.push(format!("couldn't add policy to set due to error: {err}"));
}
},
Err(pes) => errs.extend(
std::iter::once(format!("couldn't parse policy with id {id}"))
.chain(pes.errors_as_strings().into_iter()),
),
}
}
}
if errs.is_empty() {
Ok(policy_set)
} else {
Err(errs)
}
}
#[allow(clippy::panic)]
#[cfg(test)]
mod test {
use super::*;
use crate::EntityUid;
use std::collections::HashMap;
#[test]
fn test_slice_convert() {
let entities = serde_json::json!(
[
{
"uid": { "__expr": "user::\"alice\"" },
"attrs": { "foo": "bar" },
"parents": [{ "__expr": "user::\"bob\"" }]
},
{
"uid": { "__expr": "user::\"bob\"" },
"attrs": {},
"parents": []
}
]
);
let rslice = RecvdSlice {
policies: PolicySpecification::Map(HashMap::new()),
entities,
templates: None,
template_instantiations: None,
};
let (policies, entities) = rslice.try_into(None).expect("parse failed");
assert!(policies.is_empty());
entities
.get(&EntityUid::from_type_name_and_id(
"user".parse().unwrap(),
"alice".parse().unwrap(),
))
.map_or_else(
|| panic!("Missing user::alice Entity"),
|alice| {
assert!(entities.is_ancestor_of(
&EntityUid::from_type_name_and_id(
"user".parse().unwrap(),
"bob".parse().unwrap()
),
&alice.uid()
));
},
);
}
#[test]
fn test_failure_on_invalid_syntax() {
assert_is_failure(&json_is_authorized("iefjieoafiaeosij"));
}
#[test]
fn test_not_authorized_on_empty_slice() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {}
, "entities" : []
}
}
"#;
assert_is_not_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_simple_slice() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {
"ID1": "permit(principal == User::\"alice\", action, resource);"
}
, "entities" : []
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_simple_slice_with_string_policies() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : "permit(principal == User::\"alice\", action, resource);"
, "entities" : []
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_simple_slice_with_context() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {
"is_authenticated": true,
"source_ip": { "__expr": "ip(\"222.222.222.222\")" }
}
, "slice" : {
"policies" : "permit(principal == User::\"alice\", action, resource) when { context.is_authenticated && context.source_ip.isInRange(ip(\"222.222.222.0/24\")) };"
, "entities" : []
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_simple_slice_with_attrs_and_parents() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : "permit(principal, action, resource in Folder::\"house\") when { resource.owner == principal };"
, "entities" : [
{
"uid": { "__expr": "User::\"alice\"" },
"attrs": {},
"parents": []
},
{
"uid": { "__expr": "Photo::\"door\"" },
"attrs": {
"owner": { "__expr": "User::\"alice\"" }
},
"parents": [{ "__expr": "Folder::\"house\"" }]
},
{
"uid": { "__expr": "Folder::\"house\"" },
"attrs": {},
"parents": []
}
]
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_multi_policy_slice() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {
"ID0": "permit(principal == User::\"jerry\", action, resource == Photo::\"doorx\");",
"ID1": "permit(principal == User::\"tom\", action, resource == Photo::\"doory\");",
"ID2": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");"
}
, "entities" : []
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_multi_policy_slice_with_string_policies() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : "permit(principal == User::\"jerry\", action, resource == Photo::\"doorx\");permit(principal == User::\"tom\", action, resource == Photo::\"doory\");permit(principal == User::\"alice\", action, resource == Photo::\"door\");"
, "entities" : []
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_multi_policy_slice_denies_when_expected() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {
"ID0": "permit(principal, action, resource);",
"ID1": "forbid(principal == User::\"alice\", action, resource == Photo::\"door\");"
}
, "entities" : []
}
}
"#;
assert_is_not_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_on_multi_policy_slice_with_string_policies_denies_when_expected() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies": "permit(principal, action, resource);forbid(principal == User::\"alice\", action, resource);"
, "entities" : []
}
}
"#;
assert_is_not_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_with_template_as_policy_should_fail() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {
"ID0": "permit(principal == ?principal, action, resource);"
}
, "entities" : []
, "templates" : {}
}
}
"#;
assert_is_not_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_with_template_should_fail() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {}
, "entities" : []
, "templates" : {
"ID0": "permit(principal == ?principal, action, resource);"
}
}
}
"#;
assert_is_not_authorized(json_is_authorized(call));
}
#[test]
fn test_authorized_with_template_instantiation() {
let call = r#"
{ "principal": "User::\"alice\""
, "action" : "Photo::\"view\""
, "resource" : "Photo::\"door\""
, "context" : {}
, "slice" : {
"policies" : {}
, "entities" : []
, "templates" : {
"ID0": "permit(principal == ?principal, action, resource);"
}
, "template_instantiations" : [
{
"template_id" : "ID0",
"result_policy_id" : "ID0_User_alice",
"instantiations" : [
{
"slot": "?principal",
"value": {
"ty" : "User",
"eid" : "alice"
}
}
]
}
]
}
}
"#;
assert_is_authorized(json_is_authorized(call));
}
fn assert_is_authorized(result: InterfaceResult) {
match result {
InterfaceResult::Success { result } => {
let response: Response = serde_json::from_str(result.as_str()).unwrap();
println!("{response:?}");
assert_eq!(response.decision(), Decision::Allow);
assert_eq!(response.diagnostics().errors().count(), 0);
}
InterfaceResult::Failure { .. } => {
panic!("Expected a successful response, not {result:?}");
}
}
}
fn assert_is_not_authorized(result: InterfaceResult) {
match result {
InterfaceResult::Success { result } => {
let response: Response = serde_json::from_str(result.as_str()).unwrap();
assert_eq!(response.decision(), Decision::Deny);
}
InterfaceResult::Failure { .. } => {
panic!("Expected a successful response, not {result:?}");
}
}
}
fn assert_is_failure(result: &InterfaceResult) {
match result {
InterfaceResult::Success { .. } => {
panic!("Expected a failing response, not {result:?}")
}
InterfaceResult::Failure { .. } => {}
}
}
}