Skip to main content

iggy_common/commands/users/
create_user.rs

1/* Licensed to the Apache Software Foundation (ASF) under one
2 * or more contributor license agreements.  See the NOTICE file
3 * distributed with this work for additional information
4 * regarding copyright ownership.  The ASF licenses this file
5 * to you under the Apache License, Version 2.0 (the
6 * "License"); you may not use this file except in compliance
7 * with the License.  You may obtain a copy of the License at
8 *
9 *   http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing,
12 * software distributed under the License is distributed on an
13 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 * KIND, either express or implied.  See the License for the
15 * specific language governing permissions and limitations
16 * under the License.
17 */
18
19use 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/// `CreateUser` command is used to create a new user.
32/// It has additional payload:
33/// - `username` - unique name of the user, must be between 3 and 50 characters long.
34/// - `password` - password of the user, must be between 3 and 100 characters long.
35/// - `status` - status of the user, can be either `active` or `inactive`.
36/// - `permissions` - optional permissions of the user. If not provided, user will have no permissions.
37#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
38pub struct CreateUser {
39    /// Unique name of the user, must be between 3 and 50 characters long.
40    pub username: String,
41    /// Password of the user, must be between 3 and 100 characters long.
42    pub password: String,
43    /// Status of the user, can be either `active` or `inactive`.
44    pub status: UserStatus,
45    /// Optional permissions of the user. If not provided, user will have no permissions.
46    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}