use cedar_policy::{Entity, EntityId, EntityUid, RestrictedExpression};
use std::collections::{HashMap, HashSet};
use crate::models::Asset;
use pep::cedar::build_principal_entity;
use pep::oidc::types::JwtClaims;
pub fn asset_to_cedar_entity(asset: &Asset) -> Entity {
let type_name: cedar_policy::EntityTypeName = "Asset".parse().unwrap();
let uid = EntityUid::from_type_name_and_id(
type_name,
EntityId::new(&asset.id),
);
let mut attrs: HashMap<String, RestrictedExpression> = HashMap::new();
let creator_uid: EntityUid = format!("User::\"{}\"", asset.created_by)
.parse()
.expect("Invalid creator UID");
attrs.insert(
"created_by".to_string(),
RestrictedExpression::new_set(vec![
RestrictedExpression::new_entity_uid(creator_uid),
]),
);
if let Some(ref ctx) = asset.auth_context {
attrs.insert(
"visibility".to_string(),
RestrictedExpression::new_string(format!("{}", ctx.visibility)),
);
attrs.insert(
"confidentiality".to_string(),
RestrictedExpression::new_string(format!("{}", ctx.confidentiality)),
);
if !ctx.owner_groups.is_empty() {
let set_exprs: Vec<RestrictedExpression> = ctx
.owner_groups
.iter()
.map(|g| RestrictedExpression::new_string(g.clone()))
.collect();
attrs.insert(
"owner_groups".to_string(),
RestrictedExpression::new_set(set_exprs),
);
}
}
if let Some(t) = asset.tags.iter().find(|t| t.category == "status") {
attrs.insert(
"status".to_string(),
RestrictedExpression::new_string(t.value.clone()),
);
}
if let Some(t) = asset.tags.iter().find(|t| t.category == "asset_type") {
attrs.insert(
"asset_type".to_string(),
RestrictedExpression::new_string(t.value.clone()),
);
}
let parents: HashSet<EntityUid> = HashSet::new();
Entity::new(uid, attrs, parents).expect("Failed to build Cedar entity from Asset")
}
pub fn build_asset_resource_uid(asset: &Asset) -> Result<EntityUid, pep::cedar::CedarError> {
pep::cedar::entity::ResourceInfo::new("Asset", &asset.id).to_cedar_uid()
}
pub fn user_to_cedar_principal(claims: &JwtClaims) -> Entity {
let base_entity = match build_principal_entity(claims) {
Ok(e) => e,
Err(_) => return Entity::new(
pep::cedar::build_principal_uid(claims).unwrap(),
HashMap::new(),
HashSet::new(),
).unwrap(),
};
let uid = base_entity.uid().clone();
let mut attrs: HashMap<String, RestrictedExpression> = HashMap::new();
if let Some(role) = claims.extra.get("role") {
let role_str = match role {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.next(),
_ => None,
};
if let Some(role_str) = role_str {
attrs.insert(
"role".to_string(),
RestrictedExpression::new_string(role_str),
);
}
}
if let Some(serde_json::Value::Array(groups)) = claims.extra.get("groups") {
let set_exprs: Vec<RestrictedExpression> = groups
.iter()
.filter_map(|g| {
if let serde_json::Value::String(s) = g {
Some(RestrictedExpression::new_string(s.clone()))
} else {
None
}
})
.collect();
if !set_exprs.is_empty() {
attrs.insert("groups".to_string(), RestrictedExpression::new_set(set_exprs));
}
}
let parents: HashSet<EntityUid> = HashSet::new();
Entity::new(uid, attrs, parents).expect("Failed to build Cedar principal from claims")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::AuthContext;
use crate::models::Tag;
use chrono::Utc;
use std::collections::HashMap as StdHashMap;
fn make_claims(sub: &str, role: &str) -> JwtClaims {
let mut extra = StdHashMap::new();
extra.insert(
"role".to_string(),
serde_json::Value::String(role.to_string()),
);
extra.insert(
"groups".to_string(),
serde_json::Value::Array(vec![serde_json::Value::String(
"developers".to_string(),
)]),
);
JwtClaims {
sub: sub.to_string(),
iss: "test-issuer".to_string(),
aud: None,
exp: 9999999999,
iat: None,
email: Some(format!("{}@example.com", sub)),
name: None,
preferred_username: None,
extra,
}
}
fn make_asset(
id: &str,
created_by: &str,
auth_ctx: Option<AuthContext>,
tags: Vec<Tag>,
) -> Asset {
Asset {
id: id.to_string(),
title: "Test Asset".to_string(),
content: None,
tags,
metadata: StdHashMap::new(),
created_at: Utc::now(),
updated_at: Utc::now(),
created_by: created_by.to_string(),
updated_by: created_by.to_string(),
deleted_at: None,
auth_context: auth_ctx,
}
}
fn make_tag(category: &str, value: &str) -> Tag {
Tag {
id: uuid::Uuid::new_v4().to_string(),
category: category.to_string(),
value: value.to_string(),
added_by: "test-user".to_string(),
added_at: Utc::now(),
}
}
#[test]
fn test_asset_to_cedar_entity_basic() {
let asset = make_asset(
"asset-123",
"alice@idm.tanbal.ir",
None,
vec![make_tag("asset_type", "document")],
);
let entity = asset_to_cedar_entity(&asset);
assert_eq!(entity.uid().to_string(), r#"Asset::"asset-123""#);
}
#[test]
fn test_asset_to_cedar_entity_with_auth_context() {
let asset = make_asset(
"asset-456",
"bob@idm.tanbal.ir",
Some(AuthContext {
visibility: "team".to_string(),
owner_groups: vec!["developers".to_string()],
confidentiality: "internal".to_string(),
}),
vec![
make_tag("asset_type", "document"),
make_tag("status", "draft"),
],
);
let entity = asset_to_cedar_entity(&asset);
assert_eq!(entity.uid().to_string(), r#"Asset::"asset-456""#);
}
#[test]
fn test_user_to_cedar_principal() {
let claims = make_claims("alice@idm.tanbal.ir", "admin");
let entity = user_to_cedar_principal(&claims);
assert_eq!(
entity.uid().to_string(),
r#"User::"alice@idm.tanbal.ir""#
);
}
}