rustfs_policy/auth/
credentials.rs

1// Copyright 2024 RustFS Team
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
15use crate::error::Error as IamError;
16use crate::error::{Error, Result};
17use crate::policy::{INHERITED_POLICY_TYPE, Policy, Validator, iam_policy_claim_name_sa};
18use crate::utils;
19use crate::utils::extract_claims;
20use serde::de::DeserializeOwned;
21use serde::{Deserialize, Serialize};
22use serde_json::{Value, json};
23use std::collections::HashMap;
24use time::OffsetDateTime;
25use time::macros::offset;
26
27const ACCESS_KEY_MIN_LEN: usize = 3;
28const ACCESS_KEY_MAX_LEN: usize = 20;
29const SECRET_KEY_MIN_LEN: usize = 8;
30const SECRET_KEY_MAX_LEN: usize = 40;
31
32pub const ACCOUNT_ON: &str = "on";
33pub const ACCOUNT_OFF: &str = "off";
34
35const RESERVED_CHARS: &str = "=,";
36
37// ContainsReservedChars - returns whether the input string contains reserved characters.
38pub fn contains_reserved_chars(s: &str) -> bool {
39    s.contains(RESERVED_CHARS)
40}
41
42// IsAccessKeyValid - validate access key for right length.
43pub fn is_access_key_valid(access_key: &str) -> bool {
44    access_key.len() >= ACCESS_KEY_MIN_LEN
45}
46
47// IsSecretKeyValid - validate secret key for right length.
48pub fn is_secret_key_valid(secret_key: &str) -> bool {
49    secret_key.len() >= SECRET_KEY_MIN_LEN
50}
51
52// #[cfg_attr(test, derive(PartialEq, Eq, Debug))]
53// struct CredentialHeader {
54//     access_key: String,
55//     scop: CredentialHeaderScope,
56// }
57
58// #[cfg_attr(test, derive(PartialEq, Eq, Debug))]
59// struct CredentialHeaderScope {
60//     date: Date,
61//     region: String,
62//     service: ServiceType,
63//     request: String,
64// }
65
66// impl TryFrom<&str> for CredentialHeader {
67//     type Error = Error;
68//     fn try_from(value: &str) -> Result<Self, Self::Error> {
69//         let mut elem = value.trim().splitn(2, '=');
70//         let (Some(h), Some(cred_elems)) = (elem.next(), elem.next()) else {
71//             return Err(IamError::ErrCredMalformed));
72//         };
73
74//         if h != "Credential" {
75//             return Err(IamError::ErrCredMalformed));
76//         }
77
78//         let mut cred_elems = cred_elems.trim().rsplitn(5, '/');
79
80//         let Some(request) = cred_elems.next() else {
81//             return Err(IamError::ErrCredMalformed));
82//         };
83
84//         let Some(service) = cred_elems.next() else {
85//             return Err(IamError::ErrCredMalformed));
86//         };
87
88//         let Some(region) = cred_elems.next() else {
89//             return Err(IamError::ErrCredMalformed));
90//         };
91
92//         let Some(date) = cred_elems.next() else {
93//             return Err(IamError::ErrCredMalformed));
94//         };
95
96//         let Some(ak) = cred_elems.next() else {
97//             return Err(IamError::ErrCredMalformed));
98//         };
99
100//         if ak.len() < 3 {
101//             return Err(IamError::ErrCredMalformed));
102//         }
103
104//         if request != "aws4_request" {
105//             return Err(IamError::ErrCredMalformed));
106//         }
107
108//         Ok(CredentialHeader {
109//             access_key: ak.to_owned(),
110//             scop: CredentialHeaderScope {
111//                 date: {
112//                     const FORMATTER: LazyCell<Vec<BorrowedFormatItem<'static>>> =
113//                         LazyCell::new(|| time::format_description::parse("[year][month][day]").unwrap());
114
115//                     Date::parse(date, &FORMATTER).map_err(|_| IamError::ErrCredMalformed))?
116//                 },
117//                 region: region.to_owned(),
118//                 service: service.try_into()?,
119//                 request: request.to_owned(),
120//             },
121//         })
122//     }
123// }
124
125#[derive(Serialize, Deserialize, Clone, Default, Debug)]
126pub struct Credentials {
127    pub access_key: String,
128    pub secret_key: String,
129    pub session_token: String,
130    pub expiration: Option<OffsetDateTime>,
131    pub status: String,
132    pub parent_user: String,
133    pub groups: Option<Vec<String>>,
134    pub claims: Option<HashMap<String, Value>>,
135    pub name: Option<String>,
136    pub description: Option<String>,
137}
138
139impl Credentials {
140    // pub fn new(elem: &str) -> Result<Self> {
141    //     let header: CredentialHeader = elem.try_into()?;
142    //     Self::check_key_value(header)
143    // }
144
145    // pub fn check_key_value(_header: CredentialHeader) -> Result<Self> {
146    //     todo!()
147    // }
148
149    pub fn is_expired(&self) -> bool {
150        if self.expiration.is_none() {
151            return false;
152        }
153
154        self.expiration
155            .as_ref()
156            .map(|e| time::OffsetDateTime::now_utc() > *e)
157            .unwrap_or(false)
158    }
159
160    pub fn is_temp(&self) -> bool {
161        !self.session_token.is_empty() && !self.is_expired()
162    }
163
164    pub fn is_service_account(&self) -> bool {
165        const IAM_POLICY_CLAIM_NAME_SA: &str = "sa-policy";
166        self.claims
167            .as_ref()
168            .map(|x| x.get(IAM_POLICY_CLAIM_NAME_SA).is_some_and(|_| !self.parent_user.is_empty()))
169            .unwrap_or_default()
170    }
171
172    pub fn is_implied_policy(&self) -> bool {
173        if self.is_service_account() {
174            return self
175                .claims
176                .as_ref()
177                .map(|x| x.get(&iam_policy_claim_name_sa()).is_some_and(|v| v == INHERITED_POLICY_TYPE))
178                .unwrap_or_default();
179        }
180
181        false
182    }
183
184    pub fn is_valid(&self) -> bool {
185        if self.status == "off" {
186            return false;
187        }
188
189        self.access_key.len() >= 3 && self.secret_key.len() >= 8 && !self.is_expired()
190    }
191
192    pub fn is_owner(&self) -> bool {
193        false
194    }
195}
196
197pub fn generate_credentials() -> Result<(String, String)> {
198    let ak = utils::gen_access_key(20)?;
199    let sk = utils::gen_secret_key(40)?;
200    Ok((ak, sk))
201}
202
203pub fn get_new_credentials_with_metadata(claims: &HashMap<String, Value>, token_secret: &str) -> Result<Credentials> {
204    let (ak, sk) = generate_credentials()?;
205
206    create_new_credentials_with_metadata(&ak, &sk, claims, token_secret)
207}
208
209pub fn create_new_credentials_with_metadata(
210    ak: &str,
211    sk: &str,
212    claims: &HashMap<String, Value>,
213    token_secret: &str,
214) -> Result<Credentials> {
215    if ak.len() < ACCESS_KEY_MIN_LEN || ak.len() > ACCESS_KEY_MAX_LEN {
216        return Err(IamError::InvalidAccessKeyLength);
217    }
218
219    if sk.len() < SECRET_KEY_MIN_LEN || sk.len() > SECRET_KEY_MAX_LEN {
220        return Err(IamError::InvalidAccessKeyLength);
221    }
222
223    if token_secret.is_empty() {
224        return Ok(Credentials {
225            access_key: ak.to_owned(),
226            secret_key: sk.to_owned(),
227            status: ACCOUNT_OFF.to_owned(),
228            ..Default::default()
229        });
230    }
231
232    let expiration = {
233        if let Some(v) = claims.get("exp") {
234            if let Some(expiry) = v.as_i64() {
235                Some(OffsetDateTime::from_unix_timestamp(expiry)?.to_offset(offset!(+8)))
236            } else {
237                None
238            }
239        } else {
240            None
241        }
242    };
243
244    let token = utils::generate_jwt(&claims, token_secret)?;
245
246    Ok(Credentials {
247        access_key: ak.to_owned(),
248        secret_key: sk.to_owned(),
249        session_token: token,
250        status: ACCOUNT_ON.to_owned(),
251        expiration,
252        ..Default::default()
253    })
254}
255
256pub fn get_claims_from_token_with_secret<T: DeserializeOwned>(token: &str, secret: &str) -> Result<T> {
257    let ms = extract_claims::<T>(token, secret)?;
258    // TODO SessionPolicyName
259    Ok(ms.claims)
260}
261
262pub fn jwt_sign<T: Serialize>(claims: &T, token_secret: &str) -> Result<String> {
263    let token = utils::generate_jwt(claims, token_secret)?;
264    Ok(token)
265}
266
267#[derive(Default)]
268pub struct CredentialsBuilder {
269    session_policy: Option<Policy>,
270    access_key: String,
271    secret_key: String,
272    name: Option<String>,
273    description: Option<String>,
274    expiration: Option<OffsetDateTime>,
275    allow_site_replicator_account: bool,
276    claims: Option<serde_json::Value>,
277    parent_user: String,
278    groups: Option<Vec<String>>,
279}
280
281impl CredentialsBuilder {
282    pub fn new() -> Self {
283        Self::default()
284    }
285
286    pub fn session_policy(mut self, policy: Option<Policy>) -> Self {
287        self.session_policy = policy;
288        self
289    }
290
291    pub fn access_key(mut self, access_key: String) -> Self {
292        self.access_key = access_key;
293        self
294    }
295
296    pub fn secret_key(mut self, secret_key: String) -> Self {
297        self.secret_key = secret_key;
298        self
299    }
300
301    pub fn name(mut self, name: String) -> Self {
302        self.name = Some(name);
303        self
304    }
305
306    pub fn description(mut self, description: String) -> Self {
307        self.description = Some(description);
308        self
309    }
310
311    pub fn expiration(mut self, expiration: Option<OffsetDateTime>) -> Self {
312        self.expiration = expiration;
313        self
314    }
315
316    pub fn allow_site_replicator_account(mut self, allow_site_replicator_account: bool) -> Self {
317        self.allow_site_replicator_account = allow_site_replicator_account;
318        self
319    }
320
321    pub fn claims(mut self, claims: serde_json::Value) -> Self {
322        self.claims = Some(claims);
323        self
324    }
325
326    pub fn parent_user(mut self, parent_user: String) -> Self {
327        self.parent_user = parent_user;
328        self
329    }
330
331    pub fn groups(mut self, groups: Vec<String>) -> Self {
332        self.groups = Some(groups);
333        self
334    }
335
336    pub fn try_build(self) -> Result<Credentials> {
337        self.try_into()
338    }
339}
340
341impl TryFrom<CredentialsBuilder> for Credentials {
342    type Error = Error;
343    fn try_from(mut value: CredentialsBuilder) -> std::result::Result<Self, Self::Error> {
344        if value.parent_user.is_empty() {
345            return Err(IamError::InvalidArgument);
346        }
347
348        if (value.access_key.is_empty() && !value.secret_key.is_empty())
349            || (!value.access_key.is_empty() && value.secret_key.is_empty())
350        {
351            return Err(Error::other("Either ak or sk is empty"));
352        }
353
354        if value.parent_user == value.access_key.as_str() {
355            return Err(IamError::InvalidArgument);
356        }
357
358        if value.access_key == "site-replicator-0" && !value.allow_site_replicator_account {
359            return Err(IamError::InvalidArgument);
360        }
361
362        let mut claim = serde_json::json!({
363            "parent": value.parent_user
364        });
365
366        if let Some(p) = value.session_policy {
367            p.is_valid()?;
368            let policy_buf = serde_json::to_vec(&p).map_err(|_| IamError::InvalidArgument)?;
369            if policy_buf.len() > 4096 {
370                return Err(Error::other("session policy is too large"));
371            }
372            claim["sessionPolicy"] = serde_json::json!(base64_simd::STANDARD.encode_to_string(&policy_buf));
373            claim["sa-policy"] = serde_json::json!("embedded-policy");
374        } else {
375            claim["sa-policy"] = serde_json::json!("inherited-policy");
376        }
377
378        if let Some(Value::Object(obj)) = value.claims {
379            for (key, value) in obj {
380                if claim.get(&key).is_some() {
381                    continue;
382                }
383                claim[key] = value;
384            }
385        }
386
387        if value.access_key.is_empty() {
388            value.access_key = utils::gen_access_key(20)?;
389        }
390
391        if value.secret_key.is_empty() {
392            value.access_key = utils::gen_secret_key(40)?;
393        }
394
395        claim["accessKey"] = json!(&value.access_key);
396
397        let mut cred = Credentials {
398            status: "on".into(),
399            parent_user: value.parent_user,
400            groups: value.groups,
401            name: value.name,
402            description: value.description,
403            ..Default::default()
404        };
405
406        if !value.secret_key.is_empty() {
407            let session_token = rustfs_crypto::jwt_encode(value.access_key.as_bytes(), &claim)
408                .map_err(|_| Error::other("session policy is too large"))?;
409            cred.session_token = session_token;
410            // cred.expiration = Some(
411            //     OffsetDateTime::from_unix_timestamp(
412            //         claim
413            //             .get("exp")
414            //             .and_then(|x| x.as_i64())
415            //             .ok_or(Error::StringError("invalid exp".into()))?,
416            //     )
417            //     .map_err(|_| Error::StringError("invalie timestamp".into()))?,
418            // );
419        } else {
420            // cred.expiration =
421            // Some(OffsetDateTime::from_unix_timestamp(0).map_err(|_| Error::StringError("invalie timestamp".into()))?);
422        }
423
424        cred.expiration = value.expiration;
425        cred.access_key = value.access_key;
426        cred.secret_key = value.secret_key;
427
428        Ok(cred)
429    }
430}
431
432// #[cfg(test)]
433// #[allow(non_snake_case)]
434// mod tests {
435//     use test_case::test_case;
436//     use time::Date;
437
438//     use super::CredentialHeader;
439//     use super::CredentialHeaderScope;
440//     use crate::service_type::ServiceType;
441
442//     #[test_case(
443//         "Credential=aaaaaaaaaaaaaaaaaaaa/20241127/us-east-1/s3/aws4_request" =>
444//         CredentialHeader{
445//             access_key: "aaaaaaaaaaaaaaaaaaaa".into(),
446//             scop: CredentialHeaderScope {
447//                 date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(),
448//                 region: "us-east-1".to_owned(),
449//                 service: ServiceType::S3,
450//                 request: "aws4_request".into(),
451//             }
452//         };
453//         "1")]
454//     #[test_case(
455//         "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/s3/aws4_request" =>
456//         CredentialHeader{
457//             access_key: "aaaaaaaaaaa/aaaaaaaaa".into(),
458//             scop: CredentialHeaderScope {
459//                 date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(),
460//                 region: "us-east-1".to_owned(),
461//                 service: ServiceType::S3,
462//                 request: "aws4_request".into(),
463//             }
464//         };
465//         "2")]
466//     #[test_case(
467//         "Credential=aaaaaaaaaaa/aaaaaaaaa/20241127/us-east-1/sts/aws4_request" =>
468//         CredentialHeader{
469//             access_key: "aaaaaaaaaaa/aaaaaaaaa".into(),
470//             scop: CredentialHeaderScope {
471//                 date: Date::from_calendar_date(2024, time::Month::November, 27).unwrap(),
472//                 region: "us-east-1".to_owned(),
473//                 service: ServiceType::STS,
474//                 request: "aws4_request".into(),
475//             }
476//         };
477//         "3")]
478//     fn test_CredentialHeader_from_str_successful(input: &str) -> CredentialHeader {
479//         CredentialHeader::try_from(input).unwrap()
480//     }
481
482//     #[test_case("Credential")]
483//     #[test_case("Cred=")]
484//     #[test_case("Credential=abc")]
485//     #[test_case("Credential=a/20241127/us-east-1/s3/aws4_request")]
486//     #[test_case("Credential=aa/20241127/us-east-1/s3/aws4_request")]
487//     #[test_case("Credential=aaaa/20241127/us-east-1/asa/aws4_request")]
488//     #[test_case("Credential=aaaa/20241127/us-east-1/sts/aws4a_request")]
489//     fn test_credential_header_from_str_failed(input: &str) {
490//         if CredentialHeader::try_from(input).is_ok() {
491//             unreachable!()
492//         }
493//     }
494// }