Skip to main content

openstack_keystone_core/auth/
mod.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! # Authorization and authentication information.
16//!
17//! Authentication and authorization types with corresponding validation.
18//! Authentication specific validation may stay in the corresponding provider
19//! (i.e. user password is expired), but general validation rules must be
20//! present here to be shared across different authentication methods. The
21//! same is valid for the authorization validation (project/domain must exist
22//! and be enabled).
23
24use 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    /// Domain is disabled.
39    #[error("The domain is disabled.")]
40    DomainDisabled(String),
41
42    /// Project is disabled.
43    #[error("The project is disabled.")]
44    ProjectDisabled(String),
45
46    /// Structures builder error.
47    #[error(transparent)]
48    StructBuilder {
49        /// The source of the error.
50        #[from]
51        source: BuilderError,
52    },
53
54    /// Token renewal is forbidden.
55    #[error("Token renewal (getting token from token) is prohibited.")]
56    TokenRenewalForbidden,
57
58    /// Unauthorized.
59    #[error("The request you have made requires authentication.")]
60    Unauthorized,
61
62    /// User is disabled.
63    #[error("The account is disabled for user: {0}")]
64    UserDisabled(String),
65
66    /// User is locked due to the multiple failed attempts.
67    #[error("The account is temporarily disabled for user: {0}")]
68    UserLocked(String),
69
70    /// User name password combination is wrong.
71    #[error("wrong username or password")]
72    UserNameOrPasswordWrong,
73
74    /// User password is expired.
75    #[error("The password is expired for user: {0}")]
76    UserPasswordExpired(String),
77}
78
79/// Information about successful authentication.
80#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
81#[builder(build_fn(error = "BuilderError"))]
82#[builder(setter(into, strip_option))]
83pub struct AuthenticatedInfo {
84    /// Application credential.
85    #[builder(default)]
86    pub application_credential: Option<ApplicationCredential>,
87
88    /// Audit IDs.
89    #[builder(default)]
90    pub audit_ids: Vec<String>,
91
92    /// Authentication expiration.
93    #[builder(default)]
94    pub expires_at: Option<DateTime<Utc>>,
95
96    /// Federated IDP id.
97    #[builder(default)]
98    pub idp_id: Option<String>,
99
100    /// Authentication methods.
101    #[builder(default)]
102    pub methods: Vec<String>,
103
104    /// Federated protocol id.
105    #[builder(default)]
106    pub protocol_id: Option<String>,
107
108    /// Token restriction.
109    #[builder(default)]
110    pub token_restriction_id: Option<String>,
111
112    /// Resolved user object.
113    #[builder(default)]
114    pub user: Option<UserResponse>,
115
116    /// Resolved user domain information.
117    #[builder(default)]
118    pub user_domain: Option<Domain>,
119
120    /// Resolved user object.
121    #[builder(default)]
122    pub user_groups: Vec<Group>,
123
124    /// User id.
125    pub user_id: String,
126}
127
128impl AuthenticatedInfo {
129    pub fn builder() -> AuthenticatedInfoBuilder {
130        AuthenticatedInfoBuilder::default()
131    }
132
133    /// Validate the authentication information:
134    ///
135    /// - User attribute must be set
136    /// - User must be enabled
137    /// - User object id must match user_id
138    pub fn validate(&self) -> Result<(), AuthenticationError> {
139        // TODO: all validations (disabled user, locked, etc) should be placed here
140        // since every authentication method goes different way and we risk
141        // missing validations
142        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/// Authorization information.
166#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
167pub enum AuthzInfo {
168    /// Domain scope.
169    Domain(Domain),
170    /// Project scope.
171    Project(Project),
172    /// System scope.
173    System,
174    /// Trust scope.
175    Trust(Trust),
176    /// Unscoped.
177    Unscoped,
178}
179
180impl AuthzInfo {
181    /// Validate the authorization information:
182    ///
183    /// - Unscoped: always valid
184    /// - Project: check if the project is enabled
185    /// - Domain: check if the domain is enabled
186    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}