iggy_common/commands/users/
create_user.rs1use super::defaults::*;
20use crate::BytesSerializable;
21use crate::Permissions;
22use crate::UserStatus;
23use crate::Validatable;
24use crate::error::IggyError;
25use crate::{CREATE_USER_CODE, Command};
26use bytes::{BufMut, Bytes, BytesMut};
27use serde::{Deserialize, Serialize};
28use std::fmt::Display;
29use std::str::from_utf8;
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
38pub struct CreateUser {
39 pub username: String,
41 pub password: String,
43 pub status: UserStatus,
45 pub permissions: Option<Permissions>,
47}
48
49impl Command for CreateUser {
50 fn code(&self) -> u32 {
51 CREATE_USER_CODE
52 }
53}
54
55impl Default for CreateUser {
56 fn default() -> Self {
57 CreateUser {
58 username: "user".to_string(),
59 password: "secret".to_string(),
60 status: UserStatus::Active,
61 permissions: None,
62 }
63 }
64}
65
66impl Validatable<IggyError> for CreateUser {
67 fn validate(&self) -> Result<(), IggyError> {
68 if self.username.is_empty()
69 || self.username.len() > MAX_USERNAME_LENGTH
70 || self.username.len() < MIN_USERNAME_LENGTH
71 {
72 return Err(IggyError::InvalidUsername);
73 }
74
75 if self.password.is_empty()
76 || self.password.len() > MAX_PASSWORD_LENGTH
77 || self.password.len() < MIN_PASSWORD_LENGTH
78 {
79 return Err(IggyError::InvalidPassword);
80 }
81
82 Ok(())
83 }
84}
85
86impl BytesSerializable for CreateUser {
87 fn to_bytes(&self) -> Bytes {
88 let mut bytes = BytesMut::with_capacity(2 + self.username.len() + self.password.len());
89 #[allow(clippy::cast_possible_truncation)]
90 bytes.put_u8(self.username.len() as u8);
91 bytes.put_slice(self.username.as_bytes());
92 #[allow(clippy::cast_possible_truncation)]
93 bytes.put_u8(self.password.len() as u8);
94 bytes.put_slice(self.password.as_bytes());
95 bytes.put_u8(self.status.as_code());
96 if let Some(permissions) = &self.permissions {
97 bytes.put_u8(1);
98 let permissions = permissions.to_bytes();
99 #[allow(clippy::cast_possible_truncation)]
100 bytes.put_u32_le(permissions.len() as u32);
101 bytes.put_slice(&permissions);
102 } else {
103 bytes.put_u8(0);
104 }
105 bytes.freeze()
106 }
107
108 fn from_bytes(bytes: Bytes) -> Result<CreateUser, IggyError> {
109 if bytes.len() < 10 {
110 return Err(IggyError::InvalidCommand);
111 }
112
113 let username_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
114 let username = from_utf8(
115 bytes
116 .get(1..1 + username_length)
117 .ok_or(IggyError::InvalidCommand)?,
118 )
119 .map_err(|_| IggyError::InvalidUtf8)?
120 .to_string();
121
122 let mut position = 1 + username_length;
123 let password_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)? as usize;
124 position += 1;
125 let password = from_utf8(
126 bytes
127 .get(position..position + password_length)
128 .ok_or(IggyError::InvalidCommand)?,
129 )
130 .map_err(|_| IggyError::InvalidUtf8)?
131 .to_string();
132
133 position += password_length;
134 let status = UserStatus::from_code(*bytes.get(position).ok_or(IggyError::InvalidCommand)?)?;
135 position += 1;
136 let has_permissions = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
137 if has_permissions > 1 {
138 return Err(IggyError::InvalidCommand);
139 }
140
141 position += 1;
142 let permissions = if has_permissions == 1 {
143 let permissions_length = u32::from_le_bytes(
144 bytes
145 .get(position..position + 4)
146 .ok_or(IggyError::InvalidCommand)?
147 .try_into()
148 .map_err(|_| IggyError::InvalidNumberEncoding)?,
149 );
150 position += 4;
151 let end = position
152 .checked_add(permissions_length as usize)
153 .ok_or(IggyError::InvalidCommand)?;
154 if end > bytes.len() {
155 return Err(IggyError::InvalidCommand);
156 }
157 Some(Permissions::from_bytes(bytes.slice(position..end))?)
158 } else {
159 None
160 };
161
162 let command = CreateUser {
163 username,
164 password,
165 status,
166 permissions,
167 };
168 Ok(command)
169 }
170}
171
172impl Display for CreateUser {
173 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174 let permissions = if let Some(permissions) = &self.permissions {
175 permissions.to_string()
176 } else {
177 "no_permissions".to_string()
178 };
179 write!(
180 f,
181 "{}|******|{}|{}",
182 self.username, self.status, permissions
183 )
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use crate::GlobalPermissions;
191
192 #[test]
193 fn should_be_serialized_as_bytes() {
194 let command = CreateUser {
195 username: "user".to_string(),
196 password: "secret".to_string(),
197 status: UserStatus::Active,
198 permissions: Some(Permissions {
199 global: GlobalPermissions {
200 manage_servers: false,
201 read_servers: true,
202 manage_users: false,
203 read_users: true,
204 manage_streams: false,
205 read_streams: true,
206 manage_topics: false,
207 read_topics: true,
208 poll_messages: true,
209 send_messages: true,
210 },
211 streams: None,
212 }),
213 };
214
215 let bytes = command.to_bytes();
216 let username_length = bytes[0];
217 let username = from_utf8(&bytes[1..1 + username_length as usize]).unwrap();
218 let mut position = 1 + username_length as usize;
219 let password_length = bytes[position];
220 position += 1;
221 let password = from_utf8(&bytes[position..position + password_length as usize]).unwrap();
222 position += password_length as usize;
223 let status = UserStatus::from_code(bytes[position]).unwrap();
224 position += 1;
225 let has_permissions = bytes[3 + username_length as usize + password_length as usize];
226 position += 1;
227
228 let permissions_length =
229 u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
230 position += 4;
231 let permissions =
232 Permissions::from_bytes(bytes.slice(position..position + permissions_length as usize))
233 .unwrap();
234
235 assert!(!bytes.is_empty());
236 assert_eq!(username, command.username);
237 assert_eq!(password, command.password);
238 assert_eq!(status, command.status);
239 assert_eq!(has_permissions, 1);
240 assert_eq!(permissions, command.permissions.unwrap());
241 }
242
243 #[test]
244 fn from_bytes_should_fail_on_empty_input() {
245 assert!(CreateUser::from_bytes(Bytes::new()).is_err());
246 }
247
248 #[test]
249 fn from_bytes_should_fail_on_truncated_input() {
250 let command = CreateUser::default();
251 let bytes = command.to_bytes();
252 for i in 0..bytes.len() - 1 {
253 let truncated = bytes.slice(..i);
254 assert!(
255 CreateUser::from_bytes(truncated).is_err(),
256 "expected error for truncation at byte {i}"
257 );
258 }
259 }
260
261 #[test]
262 fn from_bytes_should_fail_on_corrupted_username_length() {
263 let mut buf = BytesMut::new();
264 buf.put_u8(255);
265 buf.put_slice(b"short");
266 assert!(CreateUser::from_bytes(buf.freeze()).is_err());
267 }
268
269 #[test]
270 fn should_be_deserialized_from_bytes() {
271 let username = "user";
272 let password = "secret";
273 let status = UserStatus::Active;
274 let has_permissions = 1u8;
275 let permissions = Permissions {
276 global: GlobalPermissions {
277 manage_servers: false,
278 read_servers: true,
279 manage_users: false,
280 read_users: true,
281 manage_streams: false,
282 read_streams: true,
283 manage_topics: false,
284 read_topics: true,
285 poll_messages: true,
286 send_messages: true,
287 },
288 streams: None,
289 };
290 let mut bytes = BytesMut::new();
291 #[allow(clippy::cast_possible_truncation)]
292 bytes.put_u8(username.len() as u8);
293 bytes.put_slice(username.as_bytes());
294 #[allow(clippy::cast_possible_truncation)]
295 bytes.put_u8(password.len() as u8);
296 bytes.put_slice(password.as_bytes());
297 bytes.put_u8(status.as_code());
298 bytes.put_u8(has_permissions);
299 let permissions_bytes = permissions.to_bytes();
300 #[allow(clippy::cast_possible_truncation)]
301 bytes.put_u32_le(permissions_bytes.len() as u32);
302 bytes.put_slice(&permissions_bytes);
303
304 let command = CreateUser::from_bytes(bytes.freeze());
305 assert!(command.is_ok());
306
307 let command = command.unwrap();
308 assert_eq!(command.username, username);
309 assert_eq!(command.password, password);
310 assert_eq!(command.status, status);
311 assert!(command.permissions.is_some());
312 assert_eq!(command.permissions.unwrap(), permissions);
313 }
314}