openstack_keystone_core/
policy.rs1use async_trait::async_trait;
20#[cfg(any(test, feature = "mock"))]
21use mockall::mock;
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24use serde_json::Value;
25use thiserror::Error;
26
27use crate::token::Token;
28
29#[derive(Debug, Error)]
31pub enum PolicyError {
32 #[error("module compilation task crashed")]
34 Compilation(#[from] eyre::Report),
35
36 #[error("dummy (empty) policy enforcer")]
38 Dummy,
39
40 #[error("{}", .0.violations.as_ref().map(
42 |v| v.iter().cloned().map(|x| x.msg)
43 .reduce(|acc, s| format!("{acc}, {s}"))
44 .unwrap_or_default()
45 ).unwrap_or("The request you made requires authentication.".into()))]
46 Forbidden(PolicyEvaluationResult),
47
48 #[error(transparent)]
49 IO(#[from] std::io::Error),
50
51 #[error(transparent)]
52 Join(#[from] tokio::task::JoinError),
53
54 #[error(transparent)]
56 Json(#[from] serde_json::Error),
57
58 #[error(transparent)]
60 Reqwest(#[from] reqwest::Error),
61
62 #[error(transparent)]
64 UrlParse(#[from] url::ParseError),
65}
66
67#[async_trait]
68pub trait PolicyEnforcer: Send + Sync {
69 async fn enforce(
70 &self,
71 policy_name: &'static str,
72 credentials: &Token,
73 target: Value,
74 update: Option<Value>,
75 ) -> Result<PolicyEvaluationResult, PolicyError>;
76}
77
78#[cfg(any(test, feature = "mock"))]
79mock! {
80 pub Policy {}
81
82 #[async_trait]
83 impl PolicyEnforcer for Policy {
84 async fn enforce(
85 &self,
86 policy_name: &'static str,
87 credentials: &Token,
88 target: Value,
89 current: Option<Value>
90 ) -> Result<PolicyEvaluationResult, PolicyError>;
91 }
92}
93
94#[derive(Debug, Error)]
95#[error("failed to evaluate policy")]
96pub enum EvaluationError {
97 Serialization(#[from] serde_json::Error),
98 Evaluation(#[from] eyre::Report),
99}
100
101#[derive(Serialize, Debug)]
103pub struct Credentials {
104 pub user_id: String,
105 pub roles: Vec<String>,
106 #[serde(default)]
107 pub project_id: Option<String>,
108 #[serde(default)]
109 pub domain_id: Option<String>,
110 #[serde(default)]
111 pub system: Option<String>,
112}
113
114impl From<&Token> for Credentials {
115 fn from(token: &Token) -> Self {
116 Self {
117 user_id: token.user_id().clone(),
118 roles: token
119 .effective_roles()
120 .map(|x| {
121 x.iter()
122 .filter_map(|role| role.name.clone())
123 .collect::<Vec<_>>()
124 })
125 .unwrap_or_default(),
126 project_id: token.project().map(|val| val.id.clone()),
127 domain_id: token.domain().map(|val| val.id.clone()),
128 system: None,
129 }
130 }
131}
132
133#[derive(Clone, Deserialize, Debug, JsonSchema, Serialize)]
135pub struct Violation {
136 pub msg: String,
137 pub field: Option<String>,
138}
139
140#[derive(Deserialize, Debug)]
142pub struct OpaResponse {
143 pub result: PolicyEvaluationResult,
144}
145
146#[derive(Clone, Deserialize, Debug, Serialize)]
148pub struct PolicyEvaluationResult {
149 pub allow: bool,
151 #[serde(default)]
153 pub can_see_other_domain_resources: Option<bool>,
154 #[serde(rename = "violation")]
156 pub violations: Option<Vec<Violation>>,
157}
158
159impl std::fmt::Display for PolicyEvaluationResult {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 let mut first = true;
162 if let Some(violations) = &self.violations {
163 for violation in violations {
164 if first {
165 first = false;
166 } else {
167 write!(f, ", ")?;
168 }
169 write!(f, "{}", violation.msg)?;
170 }
171 }
172 Ok(())
173 }
174}
175
176impl PolicyEvaluationResult {
177 #[must_use]
178 pub fn allow(&self) -> bool {
179 self.allow
180 }
181
182 #[must_use]
184 pub fn valid(&self) -> bool {
185 self.violations
186 .as_deref()
187 .map(|x| x.is_empty())
188 .unwrap_or(false)
189 }
190
191 #[cfg(any(test, feature = "mock"))]
192 pub fn allowed() -> Self {
193 Self {
194 allow: true,
195 can_see_other_domain_resources: None,
196 violations: None,
197 }
198 }
199
200 #[cfg(any(test, feature = "mock"))]
201 pub fn allowed_admin() -> Self {
202 Self {
203 allow: true,
204 can_see_other_domain_resources: Some(true),
205 violations: None,
206 }
207 }
208
209 #[cfg(any(test, feature = "mock"))]
210 pub fn forbidden() -> Self {
211 Self {
212 allow: false,
213 can_see_other_domain_resources: Some(false),
214 violations: None,
215 }
216 }
217}