#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::string::String;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::sync::Arc;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::vec::Vec;
use core::fmt;
#[cfg(feature = "std")]
use std::sync::Arc;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::audit::Principal;
use crate::org_id::OrgId;
use crate::request_id::RequestId;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(transparent))]
pub struct Role(Arc<str>);
impl Role {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for Role {
fn from(s: &str) -> Self {
Self(Arc::from(s))
}
}
impl From<String> for Role {
fn from(s: String) -> Self {
Self(Arc::from(s.as_str()))
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(feature = "serde")]
impl Serialize for Role {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Role {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self(Arc::from(s.as_str())))
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum RoleScope {
Self_,
Subtree,
Specific(OrgId),
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RoleBinding {
pub role: Role,
pub scope: RoleScope,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum AttestationKind {
Biscuit,
Jwt,
ApiKey,
Mtls,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Attestation {
pub kind: AttestationKind,
pub raw: Vec<u8>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OrganizationContext {
pub org_id: OrgId,
pub principal: Principal,
pub request_id: RequestId,
pub roles: Vec<RoleBinding>,
#[cfg_attr(feature = "serde", serde(default))]
pub org_path: Vec<OrgId>,
pub attestation: Option<Attestation>,
}
impl OrganizationContext {
#[must_use]
pub fn new(org_id: OrgId, principal: Principal, request_id: RequestId) -> Self {
Self {
org_id,
principal,
request_id,
roles: Vec::new(),
org_path: Vec::new(),
attestation: None,
}
}
#[must_use]
pub fn with_roles(mut self, roles: Vec<RoleBinding>) -> Self {
self.roles = roles;
self
}
#[must_use]
pub fn with_org_path(mut self, org_path: Vec<OrgId>) -> Self {
self.org_path = org_path;
self
}
#[must_use]
pub fn with_attestation(mut self, attestation: Attestation) -> Self {
self.attestation = Some(attestation);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
#[test]
fn role_scope_self_clone_eq() {
let s1 = RoleScope::Self_;
let s2 = s1.clone();
assert_eq!(s1, s2);
}
#[test]
fn role_scope_subtree_clone_eq() {
let s1 = RoleScope::Subtree;
let s2 = s1.clone();
assert_eq!(s1, s2);
}
#[test]
fn role_scope_specific_eq() {
let id = OrgId::generate();
let s1 = RoleScope::Specific(id);
let s2 = RoleScope::Specific(id);
assert_eq!(s1, s2);
}
#[cfg(feature = "serde")]
#[test]
fn role_scope_serde_roundtrip_self() {
let scope = RoleScope::Self_;
let json = serde_json::to_string(&scope).unwrap();
let back: RoleScope = serde_json::from_str(&json).unwrap();
assert_eq!(scope, back);
}
#[cfg(feature = "serde")]
#[test]
fn role_scope_serde_roundtrip_subtree() {
let scope = RoleScope::Subtree;
let json = serde_json::to_string(&scope).unwrap();
let back: RoleScope = serde_json::from_str(&json).unwrap();
assert_eq!(scope, back);
}
#[cfg(feature = "serde")]
#[test]
fn role_scope_serde_roundtrip_specific() {
let id = OrgId::generate();
let scope = RoleScope::Specific(id);
let json = serde_json::to_string(&scope).unwrap();
let back: RoleScope = serde_json::from_str(&json).unwrap();
assert_eq!(scope, back);
}
#[test]
fn role_binding_construction() {
let binding = RoleBinding {
role: Role::from("admin"),
scope: RoleScope::Self_,
};
assert_eq!(binding.role, Role::from("admin"));
assert_eq!(binding.scope, RoleScope::Self_);
}
#[cfg(feature = "serde")]
#[test]
fn role_binding_serde_roundtrip() {
let binding = RoleBinding {
role: Role::from("editor"),
scope: RoleScope::Subtree,
};
let json = serde_json::to_string(&binding).unwrap();
let back: RoleBinding = serde_json::from_str(&json).unwrap();
assert_eq!(binding, back);
}
#[test]
fn role_construction_from_str() {
let role = Role::from("admin");
assert_eq!(role.as_str(), "admin");
}
#[test]
fn role_construction_from_string() {
let role = Role::from("viewer".to_owned());
assert_eq!(role.as_str(), "viewer");
}
#[test]
fn role_clone_eq() {
let role1 = Role::from("editor");
let role2 = role1.clone();
assert_eq!(role1, role2);
}
#[test]
fn role_hash_eq() {
let role1 = Role::from("admin");
let role2 = Role::from("admin");
let mut hasher1 = DefaultHasher::new();
role1.hash(&mut hasher1);
let hash1 = hasher1.finish();
let mut hasher2 = DefaultHasher::new();
role2.hash(&mut hasher2);
let hash2 = hasher2.finish();
assert_eq!(hash1, hash2);
}
#[test]
fn role_display() {
let role = Role::from("admin");
assert_eq!(format!("{role}"), "admin");
}
#[test]
fn role_debug() {
let role = Role::from("admin");
let debug_str = format!("{role:?}");
assert!(debug_str.contains("admin"));
}
#[test]
fn attestation_kind_copy() {
let kind1 = AttestationKind::Jwt;
let kind2 = kind1;
assert_eq!(kind1, kind2);
}
#[test]
fn attestation_kind_all_variants() {
match AttestationKind::Biscuit {
AttestationKind::Biscuit => {}
_ => panic!("expected Biscuit"),
}
match AttestationKind::Jwt {
AttestationKind::Jwt => {}
_ => panic!("expected Jwt"),
}
match AttestationKind::ApiKey {
AttestationKind::ApiKey => {}
_ => panic!("expected ApiKey"),
}
match AttestationKind::Mtls {
AttestationKind::Mtls => {}
_ => panic!("expected Mtls"),
}
}
#[test]
fn attestation_construction() {
let att = Attestation {
kind: AttestationKind::Jwt,
raw: vec![1, 2, 3],
};
assert_eq!(att.kind, AttestationKind::Jwt);
assert_eq!(att.raw, vec![1, 2, 3]);
}
#[test]
fn attestation_clone_eq() {
let att1 = Attestation {
kind: AttestationKind::ApiKey,
raw: vec![42],
};
let att2 = att1.clone();
assert_eq!(att1, att2);
}
#[test]
fn attestation_debug() {
let att = Attestation {
kind: AttestationKind::Jwt,
raw: vec![],
};
let debug_str = format!("{att:?}");
assert!(debug_str.contains("Jwt"));
}
#[test]
fn org_context_construction() {
let org_id = OrgId::new(uuid::Uuid::nil());
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx = OrganizationContext::new(org_id, principal.clone(), request_id);
assert_eq!(ctx.org_id, org_id);
assert_eq!(ctx.principal, principal);
assert_eq!(ctx.request_id, request_id);
assert!(ctx.roles.is_empty());
assert!(ctx.attestation.is_none());
}
#[test]
fn org_context_with_role_bindings() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let roles = vec![
RoleBinding {
role: Role::from("admin"),
scope: RoleScope::Self_,
},
RoleBinding {
role: Role::from("editor"),
scope: RoleScope::Subtree,
},
];
let ctx = OrganizationContext::new(org_id, principal, request_id).with_roles(roles);
assert_eq!(ctx.roles.len(), 2);
assert_eq!(ctx.roles[0].role, Role::from("admin"));
assert_eq!(ctx.roles[1].role, Role::from("editor"));
}
#[test]
fn org_context_default_empty_org_path() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx = OrganizationContext::new(org_id, principal, request_id);
assert!(ctx.org_path.is_empty());
}
#[test]
fn org_context_with_org_path() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx =
OrganizationContext::new(org_id, principal, request_id).with_org_path(vec![org_id]);
assert_eq!(ctx.org_path.len(), 1);
assert_eq!(ctx.org_path[0], org_id);
}
#[test]
fn org_context_with_attestation() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let att = Attestation {
kind: AttestationKind::ApiKey,
raw: vec![42],
};
let ctx =
OrganizationContext::new(org_id, principal, request_id).with_attestation(att.clone());
assert!(ctx.attestation.is_some());
assert_eq!(ctx.attestation.unwrap(), att);
}
#[test]
fn org_context_clone_eq() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx1 =
OrganizationContext::new(org_id, principal, request_id).with_roles(vec![RoleBinding {
role: Role::from("viewer"),
scope: RoleScope::Self_,
}]);
let ctx2 = ctx1.clone();
assert_eq!(ctx1, ctx2);
}
#[test]
fn org_context_debug() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx = OrganizationContext::new(org_id, principal, request_id);
let debug_str = format!("{ctx:?}");
assert!(debug_str.contains("OrganizationContext"));
}
#[test]
fn org_context_no_attestation() {
let org_id = OrgId::generate();
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx = OrganizationContext::new(org_id, principal, request_id);
assert!(ctx.attestation.is_none());
}
#[cfg(feature = "serde")]
#[test]
fn role_serde_roundtrip() {
let role = Role::from("admin");
let json = serde_json::to_string(&role).unwrap();
let back: Role = serde_json::from_str(&json).unwrap();
assert_eq!(role, back);
}
#[cfg(feature = "serde")]
#[test]
fn attestation_kind_serde_roundtrip_jwt() {
let kind = AttestationKind::Jwt;
let json = serde_json::to_string(&kind).unwrap();
let back: AttestationKind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, back);
}
#[cfg(feature = "serde")]
#[test]
fn attestation_serde_roundtrip() {
let att = Attestation {
kind: AttestationKind::ApiKey,
raw: vec![1, 2, 3],
};
let json = serde_json::to_string(&att).unwrap();
let back: Attestation = serde_json::from_str(&json).unwrap();
assert_eq!(att, back);
}
#[cfg(feature = "serde")]
#[test]
fn org_context_serde_roundtrip() {
let org_id = OrgId::new(uuid::Uuid::nil());
let principal = Principal::system("test");
let request_id = RequestId::new();
let ctx = OrganizationContext::new(org_id, principal, request_id)
.with_roles(vec![RoleBinding {
role: Role::from("admin"),
scope: RoleScope::Self_,
}])
.with_org_path(vec![org_id])
.with_attestation(Attestation {
kind: AttestationKind::Jwt,
raw: vec![42],
});
let json = serde_json::to_string(&ctx).unwrap();
let back: OrganizationContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx, back);
}
}