#[cfg(test)]
mod archive_security_tests;
pub(crate) mod log_entry;
#[cfg(test)]
pub(crate) mod test_utils;
pub(crate) mod token_entity_metadata;
use crate::common::{
default_entities::DefaultEntitiesWithWarns,
issuer_utils::IssClaim,
};
pub(crate) mod archive_handler;
pub(crate) mod entity_parser;
pub(crate) mod errors;
pub(crate) mod issuer_parser;
pub(crate) mod legacy_store;
pub(crate) mod loader;
pub(crate) mod manager;
pub(crate) mod metadata;
pub(crate) mod policy_parser;
pub(crate) mod schema_parser;
pub(crate) mod validator;
pub(crate) mod vfs_adapter;
use super::cedar_schema::CedarSchema;
use cedar_policy::{ActionConstraint, Effect, EntityTypeName, EntityUid, Policy, PolicyId};
use serde::Serialize;
use std::collections::{HashMap, HashSet};
use url::Url;
pub(crate) use token_entity_metadata::TokenEntityMetadata;
pub(crate) use manager::ConversionError;
pub(crate) use metadata::PolicyStoreMetadata;
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct PolicyStore {
pub(crate) version: Option<String>,
pub(crate) schema: Option<CedarSchema>,
pub(crate) schema_source_exists: bool,
pub(crate) policies: PoliciesContainer,
pub(crate) trusted_issuers: Option<HashMap<String, TrustedIssuer>>,
pub(crate) default_entities: DefaultEntitiesWithWarns,
}
impl PolicyStore {
pub(crate) fn get_store_version(&self) -> &str {
self.version.as_deref().unwrap_or("undefined")
}
pub(crate) fn validate_trusted_issuers(&self) -> Result<(), TrustedIssuersValidationError> {
let mut oidc_to_trusted_issuer: HashMap<String, String> = HashMap::new();
for (issuer_name, trusted_issuer) in self.trusted_issuers.iter().flatten() {
let oidc_url = trusted_issuer.oidc_endpoint.to_string();
if let Some(_previous_issuer_name) = oidc_to_trusted_issuer.get(&oidc_url) {
return Err(TrustedIssuersValidationError {
oidc_url: format!(
"openid_configuration_endpoint: '{oidc_url}' is used for more than one issuer"
),
});
}
oidc_to_trusted_issuer.insert(oidc_url, issuer_name.to_owned());
}
Ok(())
}
}
#[derive(Debug, derive_more::Display, derive_more::Error)]
#[display("openid_configuration_endpoint: '{oidc_url}' is used for more than one issuer")]
pub(crate) struct TrustedIssuersValidationError {
oidc_url: String,
}
#[derive(Clone, derive_more::Deref)]
#[cfg_attr(test, derive(Debug))]
pub(crate) struct PolicyStoreWithID {
pub(crate) id: String,
#[deref]
pub(crate) store: PolicyStore,
pub(crate) metadata: Option<metadata::PolicyStoreMetadata>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct TrustedIssuer {
pub(crate) name: String,
pub(crate) description: String,
oidc_endpoint: Url,
pub(crate) token_metadata: HashMap<String, TokenEntityMetadata>,
}
#[cfg(test)]
impl Default for TrustedIssuer {
fn default() -> Self {
Self {
name: "Jans".to_string(),
description: String::default(),
oidc_endpoint: Url::parse("https://test.jans.org/.well-known/openid-configuration")
.unwrap(),
token_metadata: HashMap::from([
("access_token".into(), TokenEntityMetadata::access_token()),
("id_token".into(), TokenEntityMetadata::id_token()),
(
"userinfo_token".into(),
TokenEntityMetadata::userinfo_token(),
),
]),
}
}
}
#[cfg(test)]
impl Default for &TrustedIssuer {
fn default() -> Self {
static DEFAULT: std::sync::LazyLock<TrustedIssuer> =
std::sync::LazyLock::new(TrustedIssuer::default);
&DEFAULT
}
}
impl TrustedIssuer {
pub(crate) fn new(
name: String,
description: String,
oidc_endpoint: Url,
metadata: HashMap<String, TokenEntityMetadata>,
) -> Self {
Self {
name,
description,
oidc_endpoint,
token_metadata: metadata,
}
}
#[cfg(test)]
pub(crate) fn set_oidc_endpoint(&mut self, url: Url) {
self.oidc_endpoint = url;
}
pub(crate) fn get_oidc_endpoint(&self) -> &Url {
&self.oidc_endpoint
}
pub(crate) fn iss_claim(&self) -> IssClaim {
IssClaim::new(&self.oidc_endpoint.origin().ascii_serialization())
}
}
#[derive(Debug, Clone)]
pub(crate) struct PoliciesContainer {
descriptions: HashMap<String, String>,
policy_set: cedar_policy::PolicySet,
}
#[cfg(test)]
impl PartialEq for PoliciesContainer {
fn eq(&self, other: &Self) -> bool {
use std::collections::BTreeMap;
let self_policies: BTreeMap<_, _> = self
.policy_set
.policies()
.map(|p| (p.id().clone(), p))
.collect();
let other_policies: BTreeMap<_, _> = other
.policy_set
.policies()
.map(|p| (p.id().clone(), p))
.collect();
self_policies == other_policies
}
}
impl PoliciesContainer {
pub(crate) fn new(policy_set: cedar_policy::PolicySet, descriptions: HashMap<String, String>) -> Self {
Self {
descriptions,
policy_set,
}
}
pub(crate) fn new_empty(policy_set: cedar_policy::PolicySet) -> Self {
Self {
policy_set,
descriptions: HashMap::new(),
}
}
pub(crate) fn get_set(&self) -> &cedar_policy::PolicySet {
&self.policy_set
}
pub(crate) fn get_policy_description(&self, id: &str) -> Option<&str> {
self.descriptions.get(id).map(String::as_str)
}
pub(crate) fn get_matching_policies(
&self,
principal_entity_type_names: &HashSet<EntityTypeName>,
action_uids: &HashSet<EntityUid>,
resource_entity_type_names: &HashSet<EntityTypeName>,
) -> Vec<PolicyMetadata> {
self.policy_set
.policies()
.filter(|policy| {
matches_principal(policy, principal_entity_type_names)
&& matches_action(policy, action_uids)
&& matches_resource(policy, resource_entity_type_names)
})
.map(PolicyMetadata::from_policy)
.collect()
}
pub(crate) fn annotations_map<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
) -> HashMap<String, String> {
ids.into_iter()
.filter_map(|id| self.policy_set.policy(id))
.flat_map(Policy::annotations)
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
pub(crate) fn annotation_values<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
key: &str,
) -> Vec<String> {
ids.into_iter()
.filter_map(|id| self.policy_set.policy(id))
.flat_map(Policy::annotations)
.filter(|(k, _)| *k == key)
.map(|(_, v)| v.to_string())
.collect()
}
pub(crate) fn annotations_by_policy<'a>(
&self,
ids: impl IntoIterator<Item = &'a PolicyId>,
) -> HashMap<String, HashMap<String, String>> {
ids.into_iter()
.filter_map(|id| self.policy_set.policy(id))
.map(|policy| {
(
policy.id().to_string(),
policy
.annotations()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
)
})
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PolicyEffect {
Permit,
Forbid,
}
impl From<Effect> for PolicyEffect {
fn from(effect: Effect) -> Self {
match effect {
Effect::Permit => PolicyEffect::Permit,
Effect::Forbid => PolicyEffect::Forbid,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct PolicyMetadata {
pub id: String,
pub effect: PolicyEffect,
pub annotations: HashMap<String, String>,
pub source: String,
}
impl PolicyMetadata {
fn from_policy(policy: &Policy) -> Self {
Self {
id: policy.id().to_string(),
effect: policy.effect().into(),
annotations: policy
.annotations()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
source: policy.to_string(),
}
}
}
fn matches_principal(policy: &Policy, principal_types: &HashSet<EntityTypeName>) -> bool {
use cedar_policy::PrincipalConstraint;
match policy.principal_constraint() {
PrincipalConstraint::Any => true,
PrincipalConstraint::Eq(euid) | PrincipalConstraint::In(euid) => {
principal_types.contains(euid.type_name())
},
PrincipalConstraint::Is(type_name) | PrincipalConstraint::IsIn(type_name, _) => {
principal_types.contains(&type_name)
},
}
}
fn matches_action(policy: &Policy, action_uids: &HashSet<EntityUid>) -> bool {
match policy.action_constraint() {
ActionConstraint::Any => true,
ActionConstraint::Eq(euid) => action_uids.contains(&euid),
ActionConstraint::In(euids) => euids.iter().any(|euid| action_uids.contains(euid)),
}
}
fn matches_resource(policy: &Policy, resource_types: &HashSet<EntityTypeName>) -> bool {
use cedar_policy::ResourceConstraint;
match policy.resource_constraint() {
ResourceConstraint::Any => true,
ResourceConstraint::Eq(euid) | ResourceConstraint::In(euid) => {
resource_types.contains(euid.type_name())
},
ResourceConstraint::Is(type_name) | ResourceConstraint::IsIn(type_name, _) => {
resource_types.contains(&type_name)
},
}
}
#[cfg(test)]
mod policy_metadata_tests {
use super::*;
use cedar_policy::{EntityTypeName, EntityUid, PolicyId, PolicySet};
use std::collections::HashSet;
use std::str::FromStr;
fn make_container(policies: &[(&str, &str)]) -> PoliciesContainer {
let mut policy_set = PolicySet::new();
for (id, src) in policies {
let policy = Policy::parse(Some(PolicyId::new(id)), *src).expect("bad policy");
policy_set.add(policy).expect("duplicate policy id");
}
PoliciesContainer::new_empty(policy_set)
}
fn type_names(names: &[&str]) -> HashSet<EntityTypeName> {
names
.iter()
.map(|n| EntityTypeName::from_str(n).unwrap())
.collect()
}
fn action_uids(actions: &[&str]) -> HashSet<EntityUid> {
actions
.iter()
.map(|a| EntityUid::from_str(a).unwrap())
.collect()
}
#[test]
fn matches_all_policies_with_any_constraints() {
let container = make_container(&[("open", "permit(principal, action, resource);")]);
let result = container.get_matching_policies(
&type_names(&["Foo::Bar"]),
&action_uids(&[r#"Foo::Action::"read""#]),
&type_names(&["Foo::Resource"]),
);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "open");
}
#[test]
fn filters_by_action() {
let container = make_container(&[
(
"read_policy",
r#"permit(principal, action == Jans::Action::"Read", resource);"#,
),
(
"write_policy",
r#"permit(principal, action == Jans::Action::"Write", resource);"#,
),
]);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Document"]),
);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "read_policy");
}
#[test]
fn filters_by_principal_type() {
let container = make_container(&[
(
"workload_policy",
"permit(principal is Jans::Workload, action, resource);",
),
(
"user_policy",
"permit(principal is Jans::User, action, resource);",
),
]);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Doc"]),
);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "user_policy");
}
#[test]
fn filters_by_resource_type() {
let container = make_container(&[
(
"issue_policy",
"permit(principal, action, resource is Jans::Issue);",
),
(
"doc_policy",
"permit(principal, action, resource is Jans::Document);",
),
]);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Issue"]),
);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "issue_policy");
}
#[test]
fn combined_filtering() {
let container = make_container(&[
(
"specific",
r#"permit(
principal is Jans::User,
action == Jans::Action::"Read",
resource is Jans::Document
);"#,
),
(
"other_action",
r#"permit(
principal is Jans::User,
action == Jans::Action::"Delete",
resource is Jans::Document
);"#,
),
(
"other_principal",
r#"permit(
principal is Jans::Workload,
action == Jans::Action::"Read",
resource is Jans::Document
);"#,
),
]);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Document"]),
);
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "specific");
}
#[test]
fn action_in_constraint() {
let container = make_container(&[(
"multi_action",
r#"permit(
principal,
action in [Jans::Action::"Read", Jans::Action::"List"],
resource
);"#,
)]);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Doc"]),
);
assert_eq!(result.len(), 1);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Delete""#]),
&type_names(&["Jans::Doc"]),
);
assert_eq!(result.len(), 0);
}
#[test]
fn annotations_are_extracted() {
let mut policy_set = PolicySet::new();
let policy = Policy::parse(
Some(PolicyId::new("annotated")),
r#"
@description("Allow users to read docs")
@category("access_control")
permit(principal, action, resource);
"#,
)
.unwrap();
policy_set.add(policy).unwrap();
let container = PoliciesContainer::new_empty(policy_set);
let result = container.get_matching_policies(
&type_names(&["Jans::User"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Doc"]),
);
assert_eq!(result.len(), 1);
let policy = &result[0];
assert_eq!(
policy.annotations.get("description").unwrap(),
"Allow users to read docs"
);
assert_eq!(
policy.annotations.get("category").unwrap(),
"access_control"
);
}
#[test]
fn source_is_recoverable() {
let container = make_container(&[("p", "permit(principal, action, resource);")]);
let result = container.get_matching_policies(
&type_names(&["Any::Type"]),
&action_uids(&[r#"Any::Action::"do""#]),
&type_names(&["Any::Res"]),
);
assert_eq!(result.len(), 1);
assert!(result[0].source.contains("permit"));
}
#[test]
fn empty_inputs_return_only_any_policies() {
let container = make_container(&[
("any_policy", "permit(principal, action, resource);"),
(
"specific_policy",
r#"permit(
principal is Jans::User,
action == Jans::Action::"Read",
resource is Jans::Doc
);"#,
),
]);
let result =
container.get_matching_policies(&HashSet::new(), &HashSet::new(), &HashSet::new());
assert_eq!(result.len(), 1);
assert_eq!(result[0].id, "any_policy");
}
#[test]
fn multiple_principals_match_union() {
let container = make_container(&[
(
"workload_policy",
"permit(principal is Jans::Workload, action, resource);",
),
(
"user_policy",
"permit(principal is Jans::User, action, resource);",
),
]);
let result = container.get_matching_policies(
&type_names(&["Jans::User", "Jans::Workload"]),
&action_uids(&[r#"Jans::Action::"Read""#]),
&type_names(&["Jans::Doc"]),
);
assert_eq!(result.len(), 2);
}
fn annotated_container() -> PoliciesContainer {
make_container(&[
(
"upgrade",
r#"
@redirect("/upgrade")
@tier("premium")
permit(principal, action, resource);
"#,
),
(
"trial",
r#"
@redirect("/trial")
@audit("true")
permit(principal, action, resource);
"#,
),
])
}
#[test]
fn annotations_map_merges_policies() {
let container = annotated_container();
let ids = [PolicyId::new("upgrade"), PolicyId::new("trial")];
let result = container.annotations_map(ids.iter());
assert_eq!(result.len(), 3);
assert!(["/upgrade", "/trial"].contains(&result["redirect"].as_str()));
assert_eq!(result["tier"], "premium");
assert_eq!(result["audit"], "true");
}
#[test]
fn annotations_map_skips_missing_ids() {
let container = annotated_container();
let ids = [PolicyId::new("upgrade"), PolicyId::new("no_such_policy")];
let result = container.annotations_map(ids.iter());
assert_eq!(result.len(), 2);
assert_eq!(result["redirect"], "/upgrade");
assert_eq!(result["tier"], "premium");
}
#[test]
fn annotation_values_preserves_duplicates() {
let container = annotated_container();
let ids = [PolicyId::new("upgrade"), PolicyId::new("trial")];
let mut result = container.annotation_values(ids.iter(), "redirect");
result.sort();
assert_eq!(result, ["/trial", "/upgrade"]);
assert_eq!(container.annotation_values(ids.iter(), "audit"), ["true"]);
assert!(container.annotation_values(ids.iter(), "absent").is_empty());
}
#[test]
fn annotations_by_policy_groups_per_policy() {
let container = annotated_container();
let ids = [
PolicyId::new("upgrade"),
PolicyId::new("trial"),
PolicyId::new("no_such_policy"),
];
let result = container.annotations_by_policy(ids.iter());
assert_eq!(result.len(), 2);
assert_eq!(result["upgrade"]["redirect"], "/upgrade");
assert_eq!(result["upgrade"]["tier"], "premium");
assert_eq!(result["trial"]["redirect"], "/trial");
assert_eq!(result["trial"]["audit"], "true");
}
#[test]
fn annotations_of_unannotated_policy_are_empty() {
let container = make_container(&[("plain", "permit(principal, action, resource);")]);
let ids = [PolicyId::new("plain")];
assert!(container.annotations_map(ids.iter()).is_empty());
assert!(container.annotation_values(ids.iter(), "any").is_empty());
let by_policy = container.annotations_by_policy(ids.iter());
assert_eq!(by_policy.len(), 1);
assert!(by_policy["plain"].is_empty());
}
}