Skip to main content

rc_core/admin/
access_keys.rs

1//! Typed, secret-free contracts for bulk access-key inspection.
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use crate::{Error, Result};
8
9/// Capability for the builtin bulk access-key route.
10pub const IAM_ACCESS_KEYS_BULK_CAPABILITY: &str = "admin.iam.access-keys-bulk";
11/// Capability for the LDAP-scoped bulk access-key route.
12pub const IAM_ACCESS_KEYS_BULK_LDAP_CAPABILITY: &str = "admin.iam.access-keys-bulk.ldap";
13/// Capability for the OpenID-scoped bulk access-key route.
14pub const IAM_ACCESS_KEYS_BULK_OPENID_CAPABILITY: &str = "admin.iam.access-keys-bulk.openid";
15
16/// Maximum encoded response accepted from one bulk route.
17pub const MAX_IAM_ACCESS_KEYS_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
18/// Maximum selectors accepted by one logical command.
19pub const MAX_IAM_ACCESS_KEY_SELECTORS: usize = 1_000;
20/// Maximum UTF-8 byte length of one parent selector.
21pub const MAX_IAM_ACCESS_KEY_SELECTOR_BYTES: usize = 1_024;
22/// Maximum number of decoded keys accepted from one server response.
23pub const MAX_IAM_ACCESS_KEY_RESULTS: usize = 10_000;
24
25/// Identity provider owning an access key.
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum AccessKeyProvider {
29    #[default]
30    Builtin,
31    Ldap,
32    Openid,
33}
34
35impl AccessKeyProvider {
36    /// Capability that must be available before this provider route is called.
37    pub const fn capability(self) -> &'static str {
38        match self {
39            Self::Builtin => IAM_ACCESS_KEYS_BULK_CAPABILITY,
40            Self::Ldap => IAM_ACCESS_KEYS_BULK_LDAP_CAPABILITY,
41            Self::Openid => IAM_ACCESS_KEYS_BULK_OPENID_CAPABILITY,
42        }
43    }
44}
45
46impl fmt::Display for AccessKeyProvider {
47    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48        formatter.write_str(match self {
49            Self::Builtin => "builtin",
50            Self::Ldap => "ldap",
51            Self::Openid => "openid",
52        })
53    }
54}
55
56/// Access-key class represented by a RustFS bulk response.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum AccessKeyKind {
60    ServiceAccount,
61    Sts,
62}
63
64impl fmt::Display for AccessKeyKind {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str(match self {
67            Self::ServiceAccount => "service-account",
68            Self::Sts => "sts",
69        })
70    }
71}
72
73/// Server-side access-key class filter.
74#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
75pub enum AccessKeyListType {
76    #[default]
77    All,
78    UsersOnly,
79    StsOnly,
80    ServiceAccountsOnly,
81}
82
83impl AccessKeyListType {
84    pub const fn wire_value(self) -> &'static str {
85        match self {
86            Self::All => "all",
87            Self::UsersOnly => "users-only",
88            Self::StsOnly => "sts-only",
89            Self::ServiceAccountsOnly => "svcacc-only",
90        }
91    }
92}
93
94/// One bounded bulk-list request.
95#[derive(Debug, Clone, Default, PartialEq, Eq)]
96pub struct BulkAccessKeyQuery {
97    pub provider: AccessKeyProvider,
98    pub users: Vec<String>,
99    pub all: bool,
100    pub list_type: AccessKeyListType,
101}
102
103impl BulkAccessKeyQuery {
104    pub fn validate(&self) -> Result<()> {
105        if self.all && !self.users.is_empty() {
106            return Err(Error::InvalidPath(
107                "bulk access-key inspection accepts either --all or --user, not both".to_string(),
108            ));
109        }
110        if self.users.len() > MAX_IAM_ACCESS_KEY_SELECTORS {
111            return Err(Error::InvalidPath(format!(
112                "bulk access-key inspection accepts at most {MAX_IAM_ACCESS_KEY_SELECTORS} user selectors"
113            )));
114        }
115        for user in &self.users {
116            if user.trim().is_empty() {
117                return Err(Error::InvalidPath(
118                    "bulk access-key user selector cannot be empty".to_string(),
119                ));
120            }
121            if user.len() > MAX_IAM_ACCESS_KEY_SELECTOR_BYTES {
122                return Err(Error::InvalidPath(format!(
123                    "bulk access-key user selector exceeds {MAX_IAM_ACCESS_KEY_SELECTOR_BYTES} bytes"
124                )));
125            }
126        }
127        Ok(())
128    }
129}
130
131/// A safe projection of one key returned by RustFS.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133pub struct AccessKeyRecord {
134    pub access_key: String,
135    pub kind: AccessKeyKind,
136    pub provider: AccessKeyProvider,
137    pub parent_user: String,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub account_status: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub expiration: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub name: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub description: Option<String>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub implied_policy: Option<bool>,
148}
149
150/// Typed access-key bulk operations.
151#[async_trait]
152pub trait BulkAccessKeyApi: Send + Sync {
153    async fn list_access_keys_bulk(
154        &self,
155        query: &BulkAccessKeyQuery,
156    ) -> Result<Vec<AccessKeyRecord>>;
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn query_validation_rejects_conflicting_empty_and_oversized_selectors() {
165        let conflict = BulkAccessKeyQuery {
166            users: vec!["alice".to_string()],
167            all: true,
168            ..Default::default()
169        };
170        assert!(matches!(conflict.validate(), Err(Error::InvalidPath(_))));
171
172        let empty = BulkAccessKeyQuery {
173            users: vec![" ".to_string()],
174            ..Default::default()
175        };
176        assert!(matches!(empty.validate(), Err(Error::InvalidPath(_))));
177
178        let oversized = BulkAccessKeyQuery {
179            users: vec!["u".repeat(MAX_IAM_ACCESS_KEY_SELECTOR_BYTES + 1)],
180            ..Default::default()
181        };
182        assert!(matches!(oversized.validate(), Err(Error::InvalidPath(_))));
183    }
184
185    #[test]
186    fn provider_capability_mapping_is_stable() {
187        assert_eq!(
188            AccessKeyProvider::Builtin.capability(),
189            IAM_ACCESS_KEYS_BULK_CAPABILITY
190        );
191        assert_eq!(
192            AccessKeyProvider::Ldap.capability(),
193            IAM_ACCESS_KEYS_BULK_LDAP_CAPABILITY
194        );
195        assert_eq!(
196            AccessKeyProvider::Openid.capability(),
197            IAM_ACCESS_KEYS_BULK_OPENID_CAPABILITY
198        );
199    }
200}