openstack_keystone_core/auth/
mod.rs1use chrono::{DateTime, Utc};
25use derive_builder::Builder;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tracing::warn;
29
30use crate::application_credential::types::ApplicationCredential;
31use crate::error::BuilderError;
32use crate::identity::types::{Group, UserResponse};
33use crate::resource::types::{Domain, Project};
34use crate::trust::types::Trust;
35
36#[derive(Error, Debug)]
37pub enum AuthenticationError {
38 #[error("The domain is disabled.")]
40 DomainDisabled(String),
41
42 #[error("The project is disabled.")]
44 ProjectDisabled(String),
45
46 #[error(transparent)]
48 StructBuilder {
49 #[from]
51 source: BuilderError,
52 },
53
54 #[error("Token renewal (getting token from token) is prohibited.")]
56 TokenRenewalForbidden,
57
58 #[error("The request you have made requires authentication.")]
60 Unauthorized,
61
62 #[error("The account is disabled for user: {0}")]
64 UserDisabled(String),
65
66 #[error("The account is temporarily disabled for user: {0}")]
68 UserLocked(String),
69
70 #[error("wrong username or password")]
72 UserNameOrPasswordWrong,
73
74 #[error("The password is expired for user: {0}")]
76 UserPasswordExpired(String),
77}
78
79#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
81#[builder(build_fn(error = "BuilderError"))]
82#[builder(setter(into, strip_option))]
83pub struct AuthenticatedInfo {
84 #[builder(default)]
86 pub application_credential: Option<ApplicationCredential>,
87
88 #[builder(default)]
90 pub audit_ids: Vec<String>,
91
92 #[builder(default)]
94 pub expires_at: Option<DateTime<Utc>>,
95
96 #[builder(default)]
98 pub idp_id: Option<String>,
99
100 #[builder(default)]
102 pub methods: Vec<String>,
103
104 #[builder(default)]
106 pub protocol_id: Option<String>,
107
108 #[builder(default)]
110 pub token_restriction_id: Option<String>,
111
112 #[builder(default)]
114 pub user: Option<UserResponse>,
115
116 #[builder(default)]
118 pub user_domain: Option<Domain>,
119
120 #[builder(default)]
122 pub user_groups: Vec<Group>,
123
124 pub user_id: String,
126}
127
128impl AuthenticatedInfo {
129 pub fn builder() -> AuthenticatedInfoBuilder {
130 AuthenticatedInfoBuilder::default()
131 }
132
133 pub fn validate(&self) -> Result<(), AuthenticationError> {
139 if let Some(user) = &self.user {
143 if user.id != self.user_id {
144 warn!(
145 "User data does not match the user_id attribute: {} vs {}",
146 self.user_id, user.id
147 );
148 return Err(AuthenticationError::Unauthorized);
149 }
150 if !user.enabled {
151 return Err(AuthenticationError::UserDisabled(self.user_id.clone()));
152 }
153 } else {
154 warn!(
155 "User data must be resolved in the AuthenticatedInfo before validating: {:?}",
156 self
157 );
158 return Err(AuthenticationError::Unauthorized);
159 }
160
161 Ok(())
162 }
163}
164
165#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
167pub enum AuthzInfo {
168 Domain(Domain),
170 Project(Project),
172 System,
174 Trust(Trust),
176 Unscoped,
178}
179
180impl AuthzInfo {
181 pub fn validate(&self) -> Result<(), AuthenticationError> {
187 match self {
188 AuthzInfo::Domain(domain) => {
189 if !domain.enabled {
190 return Err(AuthenticationError::DomainDisabled(domain.id.clone()));
191 }
192 }
193 AuthzInfo::Project(project) => {
194 if !project.enabled {
195 return Err(AuthenticationError::ProjectDisabled(project.id.clone()));
196 }
197 }
198 AuthzInfo::System => {}
199 AuthzInfo::Trust(_) => {}
200 AuthzInfo::Unscoped => {}
201 }
202 Ok(())
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 use tracing_test::traced_test;
211
212 use crate::identity::types::{UserOptions, UserResponse};
213
214 #[test]
215 fn test_authn_validate_no_user() {
216 let authn = AuthenticatedInfo::builder().user_id("uid").build().unwrap();
217 if let Err(AuthenticationError::Unauthorized) = authn.validate() {
218 } else {
219 panic!("should be unauthorized");
220 }
221 }
222
223 #[test]
224 #[traced_test]
225 fn test_authn_validate_user_disabled() {
226 let authn = AuthenticatedInfo::builder()
227 .user_id("uid")
228 .user(UserResponse {
229 id: "uid".to_string(),
230 enabled: false,
231 default_project_id: None,
232 domain_id: "did".into(),
233 extra: None,
234 name: "foo".into(),
235 options: UserOptions::default(),
236 federated: None,
237 password_expires_at: None,
238 })
239 .build()
240 .unwrap();
241 if let Err(AuthenticationError::UserDisabled(uid)) = authn.validate() {
242 assert_eq!("uid", uid);
243 } else {
244 panic!("should fail for disabled user");
245 }
246 }
247
248 #[test]
249 #[traced_test]
250 fn test_authn_validate_user_mismatch() {
251 let authn = AuthenticatedInfo::builder()
252 .user_id("uid1")
253 .user(UserResponse {
254 id: "uid2".to_string(),
255 enabled: false,
256 default_project_id: None,
257 domain_id: "did".into(),
258 extra: None,
259 name: "foo".into(),
260 options: UserOptions::default(),
261 federated: None,
262 password_expires_at: None,
263 })
264 .build()
265 .unwrap();
266 if let Err(AuthenticationError::Unauthorized) = authn.validate() {
267 } else {
268 panic!("should fail when user_id != user.id");
269 }
270 }
271
272 #[test]
273 #[traced_test]
274 fn test_authz_validate_project() {
275 let authz = AuthzInfo::Project(Project {
276 id: "pid".into(),
277 domain_id: "pdid".into(),
278 enabled: true,
279 ..Default::default()
280 });
281 assert!(authz.validate().is_ok());
282 }
283
284 #[test]
285 #[traced_test]
286 fn test_authz_validate_project_disabled() {
287 let authz = AuthzInfo::Project(Project {
288 id: "pid".into(),
289 domain_id: "pdid".into(),
290 enabled: false,
291 ..Default::default()
292 });
293 if let Err(AuthenticationError::ProjectDisabled(..)) = authz.validate() {
294 } else {
295 panic!("should fail when project is not enabled");
296 }
297 }
298
299 #[test]
300 #[traced_test]
301 fn test_authz_validate_domain() {
302 let authz = AuthzInfo::Domain(Domain {
303 id: "id".into(),
304 name: "name".into(),
305 enabled: true,
306 ..Default::default()
307 });
308 assert!(authz.validate().is_ok());
309 }
310
311 #[test]
312 #[traced_test]
313 fn test_authz_validate_domain_disabled() {
314 let authz = AuthzInfo::Domain(Domain {
315 id: "id".into(),
316 name: "name".into(),
317 enabled: false,
318 ..Default::default()
319 });
320 if let Err(AuthenticationError::DomainDisabled(..)) = authz.validate() {
321 } else {
322 panic!("should fail when domain is not enabled");
323 }
324 }
325
326 #[test]
327 #[traced_test]
328 fn test_authz_validate_system() {
329 let authz = AuthzInfo::System;
330 assert!(authz.validate().is_ok());
331 }
332
333 #[test]
334 #[traced_test]
335 fn test_authz_validate_unscoped() {
336 let authz = AuthzInfo::Unscoped;
337 assert!(authz.validate().is_ok());
338 }
339}