rc_core/admin/
access_keys.rs1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7use crate::{Error, Result};
8
9pub const IAM_ACCESS_KEYS_BULK_CAPABILITY: &str = "admin.iam.access-keys-bulk";
11pub const IAM_ACCESS_KEYS_BULK_LDAP_CAPABILITY: &str = "admin.iam.access-keys-bulk.ldap";
13pub const IAM_ACCESS_KEYS_BULK_OPENID_CAPABILITY: &str = "admin.iam.access-keys-bulk.openid";
15
16pub const MAX_IAM_ACCESS_KEYS_RESPONSE_BYTES: usize = 8 * 1024 * 1024;
18pub const MAX_IAM_ACCESS_KEY_SELECTORS: usize = 1_000;
20pub const MAX_IAM_ACCESS_KEY_SELECTOR_BYTES: usize = 1_024;
22pub const MAX_IAM_ACCESS_KEY_RESULTS: usize = 10_000;
24
25#[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 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#[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#[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#[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#[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#[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}