osauth/identity/
protocol.rs

1// Copyright 2019 Dmitry Tantsur <dtantsur@protonmail.com>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Identity V3 JSON structures and protocol bits.
16
17use chrono::{DateTime, FixedOffset};
18use serde::ser::SerializeStruct;
19use serde::{Deserialize, Serialize, Serializer};
20
21use crate::common::IdOrName;
22
23/// User and password.
24#[derive(Clone, Debug, Serialize)]
25pub struct UserAndPassword {
26    #[serde(flatten)]
27    pub user: IdOrName,
28    pub password: String,
29    #[serde(skip_serializing_if = "Option::is_none", default)]
30    pub domain: Option<IdOrName>,
31}
32
33/// Application credential.
34#[derive(Clone, Debug, Serialize)]
35pub struct ApplicationCredential {
36    #[serde(flatten)]
37    pub id: IdOrName,
38    pub secret: String,
39    #[serde(skip_serializing_if = "Option::is_none", default)]
40    pub user: Option<IdOrName>,
41}
42
43/// Authentication identity.
44#[derive(Clone, Debug)]
45pub enum Identity {
46    /// Authentication with a user and a password.
47    Password(UserAndPassword),
48    /// Authentication with a token.
49    Token(String),
50    /// Authentication with an application credential.
51    ApplicationCredential(ApplicationCredential),
52}
53
54/// A reference to a project in a domain.
55#[derive(Clone, Debug, Serialize)]
56pub struct Project {
57    #[serde(flatten)]
58    pub project: IdOrName,
59    #[serde(skip_serializing_if = "Option::is_none", default)]
60    pub domain: Option<IdOrName>,
61}
62
63/// A scope.
64#[allow(unused)]
65#[derive(Clone, Debug, Serialize)]
66pub enum Scope {
67    /// Project scope.
68    #[serde(rename = "project")]
69    Project(Project),
70    /// Domain scope.
71    #[serde(rename = "domain")]
72    Domain(IdOrName),
73    #[serde(rename = "system", serialize_with = "ser_system_scope")]
74    System,
75}
76
77/// An authentication object.
78#[derive(Clone, Debug, Serialize)]
79pub struct Auth {
80    /// Authentication identity.
81    pub identity: Identity,
82    /// Authentication scope (if needed).
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub scope: Option<Scope>,
85}
86
87/// An authentication request root.
88#[derive(Clone, Debug, Serialize)]
89pub struct AuthRoot {
90    pub auth: Auth,
91}
92
93/// An endpoint in the catalog.
94#[derive(Clone, Debug, Deserialize)]
95pub struct Endpoint {
96    pub interface: String,
97    pub region: String,
98    pub url: String,
99}
100
101/// A service catalog record.
102#[derive(Clone, Debug, Deserialize)]
103pub struct CatalogRecord {
104    #[serde(rename = "type")]
105    pub service_type: String,
106    pub endpoints: Vec<Endpoint>,
107}
108
109/// An authentication token with embedded catalog.
110#[derive(Clone, Debug, Deserialize)]
111pub struct Token {
112    pub expires_at: DateTime<FixedOffset>,
113    pub catalog: Vec<CatalogRecord>,
114}
115
116/// A token response root.
117#[derive(Clone, Debug, Deserialize)]
118pub struct TokenRoot {
119    pub token: Token,
120}
121
122#[derive(Debug, Serialize)]
123struct PasswordAuth<'a> {
124    user: &'a UserAndPassword,
125}
126
127#[derive(Debug, Serialize)]
128struct TokenAuth<'a> {
129    id: &'a str,
130}
131
132impl IdOrName {
133    /// Create an ID from anything that can be converted to a string.
134    #[inline]
135    pub fn from_id<T: Into<String>>(id: T) -> IdOrName {
136        IdOrName::Id(id.into())
137    }
138
139    /// Create a name from anything that can be converted to a string.
140    #[inline]
141    pub fn from_name<T: Into<String>>(name: T) -> IdOrName {
142        IdOrName::Name(name.into())
143    }
144}
145
146impl Serialize for Identity {
147    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
148    where
149        S: Serializer,
150    {
151        let mut inner = serializer.serialize_struct("Identity", 2)?;
152        match self {
153            Identity::Password(ref user) => {
154                inner.serialize_field("methods", &["password"])?;
155                inner.serialize_field("password", &PasswordAuth { user })?;
156            }
157            Identity::Token(ref token) => {
158                inner.serialize_field("methods", &["token"])?;
159                inner.serialize_field("token", &TokenAuth { id: token })?;
160            }
161            Identity::ApplicationCredential(ref cred) => {
162                inner.serialize_field("methods", &["application_credential"])?;
163                inner.serialize_field("application_credential", &cred)?;
164            }
165        }
166        inner.end()
167    }
168}
169
170fn ser_system_scope<S>(serializer: S) -> Result<S::Ok, S::Error>
171where
172    S: Serializer,
173{
174    let mut inner = serializer.serialize_struct("System", 1)?;
175    inner.serialize_field("all", &true)?;
176    inner.end()
177}
178
179#[cfg(test)]
180mod test {
181    use super::*;
182    use crate::common::test;
183
184    const PASSWORD_NAME_UNSCOPED: &str = r#"
185{
186    "auth": {
187        "identity": {
188            "methods": [
189                "password"
190            ],
191            "password": {
192                "user": {
193                    "name": "admin",
194                    "domain": {
195                        "name": "Default"
196                    },
197                    "password": "devstacker"
198                }
199            }
200        }
201    }
202}"#;
203
204    const PASSWORD_ID_SCOPED_WITH_ID: &str = r#"
205{
206    "auth": {
207        "identity": {
208            "methods": [
209                "password"
210            ],
211            "password": {
212                "user": {
213                    "id": "ee4dfb6e5540447cb3741905149d9b6e",
214                    "password": "devstacker"
215                }
216            }
217        },
218        "scope": {
219            "domain": {
220                "id": "default"
221            }
222        }
223    }
224}"#;
225
226    const PASSWORD_ID_SYSTEM_SCOPE: &str = r#"
227{
228    "auth": {
229        "identity": {
230            "methods": [
231                "password"
232            ],
233            "password": {
234                "user": {
235                    "id": "ee4dfb6e5540447cb3741905149d9b6e",
236                    "password": "devstacker"
237                }
238            }
239        },
240        "scope": {
241            "system": {
242                "all": true
243            }
244        }
245    }
246}"#;
247
248    const TOKEN_SCOPED_WITH_NAME: &str = r#"
249{
250    "auth": {
251        "identity": {
252            "methods": [
253                "token"
254            ],
255            "token": {
256                "id": "abcdef"
257            }
258        },
259        "scope": {
260            "domain": {
261                "name": "Default"
262            }
263        }
264    }
265}"#;
266
267    const APPLICATION_CREDENTIAL_ID: &str = r#"
268{
269    "auth": {
270        "identity": {
271            "methods": [
272                "application_credential"
273            ],
274            "application_credential": {
275                "id": "abcdef",
276                "secret": "shhhh"
277            }
278        }
279    }
280}"#;
281
282    const APPLICATION_CREDENTIAL_NAME: &str = r#"
283{
284    "auth": {
285        "identity": {
286            "methods": [
287                "application_credential"
288            ],
289            "application_credential": {
290                "name": "abcdef",
291                "secret": "shhhh",
292                "user": {
293                    "id": "a6b3c6e7a6d"
294                }
295            }
296        }
297    }
298}"#;
299
300    #[test]
301    fn test_password_name_unscoped() {
302        let value = AuthRoot {
303            auth: Auth {
304                identity: Identity::Password(UserAndPassword {
305                    user: IdOrName::Name("admin".to_string()),
306                    password: "devstacker".to_string(),
307                    domain: Some(IdOrName::from_name("Default")),
308                }),
309                scope: None,
310            },
311        };
312        test::compare(PASSWORD_NAME_UNSCOPED, value);
313    }
314
315    #[test]
316    fn test_password_id_scoped_with_id() {
317        let value = AuthRoot {
318            auth: Auth {
319                identity: Identity::Password(UserAndPassword {
320                    user: IdOrName::Id("ee4dfb6e5540447cb3741905149d9b6e".to_string()),
321                    password: "devstacker".to_string(),
322                    domain: None,
323                }),
324                scope: Some(Scope::Domain(IdOrName::from_id("default"))),
325            },
326        };
327        test::compare(PASSWORD_ID_SCOPED_WITH_ID, value);
328    }
329
330    #[test]
331    fn test_password_id_system_scope() {
332        let value = AuthRoot {
333            auth: Auth {
334                identity: Identity::Password(UserAndPassword {
335                    user: IdOrName::Id("ee4dfb6e5540447cb3741905149d9b6e".to_string()),
336                    password: "devstacker".to_string(),
337                    domain: None,
338                }),
339                scope: Some(Scope::System),
340            },
341        };
342        test::compare(PASSWORD_ID_SYSTEM_SCOPE, value);
343    }
344
345    #[test]
346    fn test_token_scoped_with_name() {
347        let value = AuthRoot {
348            auth: Auth {
349                identity: Identity::Token("abcdef".to_string()),
350                scope: Some(Scope::Domain(IdOrName::Name("Default".to_string()))),
351            },
352        };
353        test::compare(TOKEN_SCOPED_WITH_NAME, value);
354    }
355
356    #[test]
357    fn test_application_credential_id() {
358        let value = AuthRoot {
359            auth: Auth {
360                identity: Identity::ApplicationCredential(ApplicationCredential {
361                    id: IdOrName::Id("abcdef".to_string()),
362                    secret: "shhhh".to_string(),
363                    user: None,
364                }),
365                scope: None,
366            },
367        };
368        test::compare(APPLICATION_CREDENTIAL_ID, value);
369    }
370
371    #[test]
372    fn test_application_credential_name() {
373        let value = AuthRoot {
374            auth: Auth {
375                identity: Identity::ApplicationCredential(ApplicationCredential {
376                    id: IdOrName::Name("abcdef".to_string()),
377                    secret: "shhhh".to_string(),
378                    user: Some(IdOrName::Id("a6b3c6e7a6d".into())),
379                }),
380                scope: None,
381            },
382        };
383        test::compare(APPLICATION_CREDENTIAL_NAME, value);
384    }
385}