Skip to main content

redis_cloud/
acl.rs

1//! Role-based Access Control (RBAC) operations and models
2//!
3//! This module provides comprehensive access control management for Redis Cloud,
4//! including ACL management for users, roles, Redis rules, and database-level
5//! access controls.
6//!
7//! # Overview
8//!
9//! The ACL module implements Redis Cloud's role-based access control system, allowing
10//! fine-grained control over who can access what resources and perform which operations.
11//! It supports both user-level and database-level access controls.
12//!
13//! # Key Features
14//!
15//! - **User ACLs**: Manage user access control lists and permissions
16//! - **Role Management**: Create and manage roles with specific permissions
17//! - **Redis Rules**: Define Redis command-level access rules
18//! - **Database ACLs**: Control access at the database level
19//! - **Rule Association**: Link users and roles to specific databases
20//!
21//! # Example Usage
22//!
23//! ```no_run
24//! use redis_cloud::{CloudClient, AclHandler};
25//!
26//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
27//! let client = CloudClient::builder()
28//!     .api_key("your-api-key")
29//!     .api_secret("your-api-secret")
30//!     .build()?;
31//!
32//! let handler = AclHandler::new(client);
33//!
34//! // List all ACL users
35//! let users = handler.get_all_acl_users().await?;
36//!
37//! // Get all Redis rules
38//! let rules = handler.get_all_redis_rules().await?;
39//! # Ok(())
40//! # }
41//! ```
42
43use crate::types::{Link, ProcessorResponse};
44use crate::{CloudClient, Result};
45use serde::{Deserialize, Serialize};
46
47// ============================================================================
48// Models
49// ============================================================================
50
51/// ACL role create request
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct AclRoleCreateRequest {
55    /// Database access role name.
56    pub name: String,
57
58    /// A list of Redis ACL rules to assign to this database access role.
59    pub redis_rules: Vec<AclRoleRedisRuleSpec>,
60
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub command_type: Option<String>,
63}
64
65/// ACL user update request
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct AclUserUpdateRequest {
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub user_id: Option<i32>,
71
72    /// Optional. Changes the ACL role assigned to the user. Use GET '/acl/roles' to get a list of database access roles.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub role: Option<String>,
75
76    /// Optional. Changes the user's database password.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub password: Option<String>,
79
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub command_type: Option<String>,
82}
83
84/// ACL users response
85///
86/// Response from GET /acl/users
87#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct AccountACLUsers {
90    /// Account ID
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub account_id: Option<i32>,
93
94    /// List of ACL users
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub users: Option<Vec<ACLUser>>,
97
98    /// HATEOAS links for API navigation
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub links: Option<Vec<Link>>,
101}
102
103/// ACL Redis rules response
104///
105/// Response from GET /acl/redisRules
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct AccountACLRedisRules {
109    /// Account ID
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub account_id: Option<i32>,
112
113    /// List of Redis ACL rules
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub redis_rules: Option<Vec<ACLRedisRule>>,
116
117    /// HATEOAS links for API navigation
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub links: Option<Vec<Link>>,
120}
121
122/// ACL Redis rule
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ACLRedisRule {
126    /// Rule ID
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub id: Option<i32>,
129
130    /// Rule name
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub name: Option<String>,
133
134    /// ACL pattern (e.g., "+@all ~lcm:*")
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub acl: Option<String>,
137
138    /// Whether this is a default rule
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub is_default: Option<bool>,
141
142    /// Rule status (e.g., "active")
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub status: Option<String>,
145}
146
147/// ACL Redis rule create request
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct AclRedisRuleCreateRequest {
151    /// Redis ACL rule name.
152    pub name: String,
153
154    /// Redis ACL rule pattern. See [ACL syntax](https://redis.io/docs/latest/operate/rc/security/access-control/data-access-control/configure-acls/#define-permissions-with-acl-syntax) to learn how to define rules.
155    pub redis_rule: String,
156
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub command_type: Option<String>,
159}
160
161/// ACL roles response
162///
163/// Response from GET /acl/roles
164#[derive(Debug, Clone, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct AccountACLRoles {
167    /// Account ID
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub account_id: Option<i32>,
170
171    /// List of ACL roles
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub roles: Option<Vec<ACLRole>>,
174
175    /// HATEOAS links for API navigation
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub links: Option<Vec<Link>>,
178}
179
180/// ACL role
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct ACLRole {
184    /// Role ID
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub id: Option<i32>,
187
188    /// Role name
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub name: Option<String>,
191
192    /// Redis rules associated with this role
193    ///
194    /// Note: These use different field names (ruleId, ruleName) than standalone redis rules
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub redis_rules: Option<Vec<ACLRoleRedisRule>>,
197
198    /// Users assigned to this role
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub users: Option<Vec<ACLRoleUser>>,
201
202    /// Role status (e.g., "active")
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub status: Option<String>,
205}
206
207/// User reference in an ACL role
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct ACLRoleUser {
211    /// User ID
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub id: Option<i32>,
214
215    /// User name
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub name: Option<String>,
218}
219
220/// Redis rule as embedded in an ACL role response
221///
222/// This has different field names than `ACLRedisRule` because the API
223/// uses different JSON keys when rules are embedded in role responses.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "camelCase")]
226pub struct ACLRoleRedisRule {
227    /// Rule ID
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub rule_id: Option<i32>,
230
231    /// Rule name
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub rule_name: Option<String>,
234
235    /// Databases this rule applies to within the role
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub databases: Option<Vec<ACLRoleDatabase>>,
238}
239
240/// Database reference in an ACL role's redis rule
241#[derive(Debug, Clone, Serialize, Deserialize)]
242#[serde(rename_all = "camelCase")]
243pub struct ACLRoleDatabase {
244    /// Subscription ID
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub subscription_id: Option<i32>,
247
248    /// Database ID
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub database_id: Option<i32>,
251
252    /// Database name
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub database_name: Option<String>,
255
256    /// Regions (for Active-Active databases)
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub regions: Option<Vec<String>>,
259}
260
261/// ACL Redis rule update request
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264pub struct AclRedisRuleUpdateRequest {
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub redis_rule_id: Option<i32>,
267
268    /// Optional. Changes the Redis ACL rule name.
269    pub name: String,
270
271    /// Optional. Changes the Redis ACL rule pattern. See [ACL syntax](https://redis.io/docs/latest/operate/rc/security/access-control/data-access-control/configure-acls/#define-permissions-with-acl-syntax) to learn how to define rules.
272    pub redis_rule: String,
273
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub command_type: Option<String>,
276}
277
278/// Database specification for ACL role assignment
279#[derive(Debug, Clone, Serialize, Deserialize)]
280#[serde(rename_all = "camelCase")]
281pub struct AclRoleDatabaseSpec {
282    /// Subscription ID for the database's subscription. Use 'GET /subscriptions' or 'GET /fixed/subscriptions' to get a list of available subscriptions and their IDs.
283    pub subscription_id: i32,
284
285    /// The database's ID. Use 'GET /subscriptions/{subscriptionId}/databases' or 'GET /fixed/subscriptions/{subscriptionId}/databases' to get a list of databases in a subscription and their IDs.
286    pub database_id: i32,
287
288    /// (Active-Active databases only) Optional. A list of regions where this rule applies for this role.
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub regions: Option<Vec<String>>,
291}
292
293/// ACL user create request
294#[derive(Debug, Clone, Serialize, Deserialize)]
295#[serde(rename_all = "camelCase")]
296pub struct AclUserCreateRequest {
297    /// Access control user name.
298    pub name: String,
299
300    /// Name of the database access role to assign to this user. Use GET '/acl/roles' to get a list of database access roles.
301    pub role: String,
302
303    /// The database password for this user.
304    pub password: String,
305
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub command_type: Option<String>,
308}
309
310/// ACL user information
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct ACLUser {
313    /// User ID
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub id: Option<i32>,
316
317    /// User name
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub name: Option<String>,
320
321    /// Assigned role name
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub role: Option<String>,
324
325    /// User status (e.g., "active", "error")
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub status: Option<String>,
328
329    /// HATEOAS links
330    #[serde(skip_serializing_if = "Option::is_none")]
331    pub links: Option<Vec<Link>>,
332}
333
334/// ACL role update request
335#[derive(Debug, Clone, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct AclRoleUpdateRequest {
338    /// Optional. Changes the database access role name.
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub name: Option<String>,
341
342    /// Optional. Changes the Redis ACL rules to assign to this database access role.
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub redis_rules: Option<Vec<AclRoleRedisRuleSpec>>,
345
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub role_id: Option<i32>,
348
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub command_type: Option<String>,
351}
352
353/// Redis rule specification for role assignment
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct AclRoleRedisRuleSpec {
357    /// The name of a Redis ACL rule to assign to the role. Use 'GET /acl/redisRules' to get a list of available rules for your account.
358    pub rule_name: String,
359
360    /// A list of databases where the specified rule applies for this role.
361    pub databases: Vec<AclRoleDatabaseSpec>,
362}
363
364/// Task state update response
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct TaskStateUpdate {
368    /// Task ID (UUID)
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub task_id: Option<String>,
371
372    /// Command type (e.g., "subscriptionDeleteRequest")
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub command_type: Option<String>,
375
376    /// Task status (e.g., "processing-completed")
377    #[serde(skip_serializing_if = "Option::is_none")]
378    pub status: Option<String>,
379
380    /// Task description
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub description: Option<String>,
383
384    /// Timestamp of the task
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub timestamp: Option<String>,
387
388    /// Task response with resource info
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub response: Option<ProcessorResponse>,
391
392    /// HATEOAS links
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub links: Option<Vec<Link>>,
395}
396
397// ============================================================================
398// Handler
399// ============================================================================
400
401/// Handler for Role-based Access Control (RBAC) operations
402///
403/// Manages ACLs for users, roles, Redis rules, and database-level access controls.
404/// Provides fine-grained permission management for Redis Cloud resources.
405pub struct AclHandler {
406    client: CloudClient,
407}
408
409impl AclHandler {
410    /// Create a new handler
411    #[must_use]
412    pub fn new(client: CloudClient) -> Self {
413        Self { client }
414    }
415
416    /// Get Redis ACL rules
417    /// Gets a list of all Redis ACL rules for this account.
418    ///
419    /// GET /acl/redisRules
420    ///
421    /// # Example
422    ///
423    /// ```no_run
424    /// use redis_cloud::CloudClient;
425    ///
426    /// # async fn example() -> redis_cloud::Result<()> {
427    /// let client = CloudClient::builder()
428    ///     .api_key("your-api-key")
429    ///     .api_secret("your-api-secret")
430    ///     .build()?;
431    ///
432    /// let rules = client.acl().get_all_redis_rules().await?;
433    /// # Ok(())
434    /// # }
435    /// ```
436    pub async fn get_all_redis_rules(&self) -> Result<AccountACLRedisRules> {
437        self.client.get("/acl/redisRules").await
438    }
439
440    /// Create Redis ACL rule
441    /// Creates a new Redis ACL rule.
442    ///
443    /// POST /acl/redisRules
444    pub async fn create_redis_rule(
445        &self,
446        request: &AclRedisRuleCreateRequest,
447    ) -> Result<TaskStateUpdate> {
448        self.client.post("/acl/redisRules", request).await
449    }
450
451    /// Delete Redis ACL rule
452    /// Deletes a Redis ACL rule.
453    ///
454    /// DELETE /acl/redisRules/{aclRedisRuleId}
455    pub async fn delete_redis_rule(&self, acl_redis_rule_id: i32) -> Result<TaskStateUpdate> {
456        let response = self
457            .client
458            .delete_raw(&format!("/acl/redisRules/{acl_redis_rule_id}"))
459            .await?;
460        serde_json::from_value(response).map_err(Into::into)
461    }
462
463    /// Update Redis ACL rule
464    /// Updates a Redis ACL rule.
465    ///
466    /// PUT /acl/redisRules/{aclRedisRuleId}
467    pub async fn update_redis_rule(
468        &self,
469        acl_redis_rule_id: i32,
470        request: &AclRedisRuleUpdateRequest,
471    ) -> Result<TaskStateUpdate> {
472        self.client
473            .put(&format!("/acl/redisRules/{acl_redis_rule_id}"), request)
474            .await
475    }
476
477    /// Get database access roles
478    /// Gets a list of all database access roles for this account.
479    ///
480    /// GET /acl/roles
481    ///
482    /// # Example
483    ///
484    /// ```no_run
485    /// use redis_cloud::CloudClient;
486    ///
487    /// # async fn example() -> redis_cloud::Result<()> {
488    /// let client = CloudClient::builder()
489    ///     .api_key("your-api-key")
490    ///     .api_secret("your-api-secret")
491    ///     .build()?;
492    ///
493    /// let roles = client.acl().get_roles().await?;
494    /// # Ok(())
495    /// # }
496    /// ```
497    pub async fn get_roles(&self) -> Result<AccountACLRoles> {
498        self.client.get("/acl/roles").await
499    }
500
501    /// Create database access role
502    /// Creates a new database access role with the assigned permissions and associates it with the provided databases.
503    ///
504    /// POST /acl/roles
505    pub async fn create_role(&self, request: &AclRoleCreateRequest) -> Result<TaskStateUpdate> {
506        self.client.post("/acl/roles", request).await
507    }
508
509    /// Delete database access role
510    /// Deletes a database access role.
511    ///
512    /// DELETE /acl/roles/{aclRoleId}
513    pub async fn delete_acl_role(&self, acl_role_id: i32) -> Result<TaskStateUpdate> {
514        let response = self
515            .client
516            .delete_raw(&format!("/acl/roles/{acl_role_id}"))
517            .await?;
518        serde_json::from_value(response).map_err(Into::into)
519    }
520
521    /// Update database access role
522    /// Updates a database access role with new assigned permissions or associated databases.
523    ///
524    /// PUT /acl/roles/{aclRoleId}
525    pub async fn update_role(
526        &self,
527        acl_role_id: i32,
528        request: &AclRoleUpdateRequest,
529    ) -> Result<TaskStateUpdate> {
530        self.client
531            .put(&format!("/acl/roles/{acl_role_id}"), request)
532            .await
533    }
534
535    /// Get access control users
536    /// Gets a list of all access control users for this account.
537    ///
538    /// GET /acl/users
539    pub async fn get_all_acl_users(&self) -> Result<AccountACLUsers> {
540        self.client.get("/acl/users").await
541    }
542
543    /// Create access control user
544    /// Creates a new access control user with the assigned database access role.
545    ///
546    /// POST /acl/users
547    pub async fn create_user(&self, request: &AclUserCreateRequest) -> Result<TaskStateUpdate> {
548        self.client.post("/acl/users", request).await
549    }
550
551    /// Delete access control user
552    /// Deletes a access control user.
553    ///
554    /// DELETE /acl/users/{aclUserId}
555    pub async fn delete_user(&self, acl_user_id: i32) -> Result<TaskStateUpdate> {
556        let response = self
557            .client
558            .delete_raw(&format!("/acl/users/{acl_user_id}"))
559            .await?;
560        serde_json::from_value(response).map_err(Into::into)
561    }
562
563    /// Get a single access control user
564    /// Gets details and settings for single access control user.
565    ///
566    /// GET /acl/users/{aclUserId}
567    pub async fn get_user_by_id(&self, acl_user_id: i32) -> Result<ACLUser> {
568        self.client.get(&format!("/acl/users/{acl_user_id}")).await
569    }
570
571    /// Update access control user
572    /// Updates a access control user with a different role or database password.
573    ///
574    /// PUT /acl/users/{aclUserId}
575    pub async fn update_acl_user(
576        &self,
577        acl_user_id: i32,
578        request: &AclUserUpdateRequest,
579    ) -> Result<TaskStateUpdate> {
580        self.client
581            .put(&format!("/acl/users/{acl_user_id}"), request)
582            .await
583    }
584}