diff --git a/src/StackExchange.Redis/APITypes/ACLRules.cs b/src/StackExchange.Redis/APITypes/ACLRules.cs
new file mode 100644
index 000000000..cee0ccaee
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/ACLRules.cs
@@ -0,0 +1,572 @@
+using System.Collections.Generic;
+
+namespace StackExchange.Redis;
+
+///
+/// To control Access Control List config of individual users with ACL SETUSER command.
+///
+///
+///
+public class ACLRules
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ACL user rules.
+ /// The ACL command rules.
+ /// The ACL selector rules.
+ public ACLRules(
+ ACLUserRules? aclUserRules,
+ ACLCommandRules? aclCommandRules,
+ ACLSelectorRules[]? aclSelectorRules)
+ {
+ AclUserRules = aclUserRules;
+ AclCommandRules = aclCommandRules;
+ AclSelectorRules = aclSelectorRules;
+ }
+
+ ///
+ /// Gets the ACL user rules.
+ ///
+ public readonly ACLUserRules? AclUserRules;
+
+ ///
+ /// Gets the ACL command rules.
+ ///
+ public readonly ACLCommandRules? AclCommandRules;
+
+ ///
+ /// Gets the ACL selector rules.
+ ///
+ public readonly ACLSelectorRules[]? AclSelectorRules;
+
+ ///
+ /// Converts the ACL rules to Redis values.
+ ///
+ /// An array of Redis values representing the ACL rules.
+ internal RedisValue[] ToRedisValues()
+ {
+ var redisValues = new List();
+ AclUserRules?.AppendTo(redisValues);
+ AclCommandRules?.AppendTo(redisValues);
+
+ if (AclSelectorRules is not null)
+ {
+ foreach (var rules in AclSelectorRules)
+ {
+ rules.AppendTo(redisValues);
+ }
+ }
+ return redisValues.ToArray();
+ }
+}
+
+///
+/// Represents the ACL user rules.
+///
+public class ACLUserRules
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// If set to true, resets the user.
+ /// If set to true, no password is required.
+ /// If set to true, resets the password.
+ /// The state of the user.
+ /// The passwords to set.
+ /// The passwords to remove.
+ /// The hashed passwords to set.
+ /// The hashed passwords to remove.
+ /// If set to true, clears the selectors.
+ public ACLUserRules(
+ bool resetUser,
+ bool noPass,
+ bool resetPass,
+ ACLUserState? userState,
+ string[]? passwordsToSet,
+ string[]? passwordsToRemove,
+ string[]? hashedPasswordsToSet,
+ string[]? hashedPasswordsToRemove,
+ bool clearSelectors)
+ {
+ ResetUser = resetUser;
+ NoPass = noPass;
+ ResetPass = resetPass;
+ UserState = userState;
+ PasswordsToSet = passwordsToSet;
+ PasswordsToRemove = passwordsToRemove;
+ HashedPasswordsToSet = hashedPasswordsToSet;
+ HashedPasswordsToRemove = hashedPasswordsToRemove;
+ ClearSelectors = clearSelectors;
+ }
+
+ ///
+ /// Gets a value indicating whether the user is reset.
+ ///
+ public readonly bool ResetUser;
+
+ ///
+ /// Gets a value indicating whether no password is required.
+ ///
+ public readonly bool NoPass;
+
+ ///
+ /// Gets a value indicating whether the password is reset.
+ ///
+ public readonly bool ResetPass;
+
+ ///
+ /// Gets the state of the user.
+ ///
+ public readonly ACLUserState? UserState;
+
+ ///
+ /// Gets the passwords to set.
+ ///
+ public readonly string[]? PasswordsToSet;
+
+ ///
+ /// Gets the passwords to remove.
+ ///
+ public readonly string[]? PasswordsToRemove;
+
+ ///
+ /// Gets the hashed passwords to set.
+ ///
+ public readonly string[]? HashedPasswordsToSet;
+
+ ///
+ /// Gets the hashed passwords to remove.
+ ///
+ public readonly string[]? HashedPasswordsToRemove;
+
+ ///
+ /// Gets a value indicating whether the selectors are cleared.
+ ///
+ public readonly bool ClearSelectors;
+
+ ///
+ /// Appends the ACL user rules to the specified list of Redis values.
+ ///
+ /// The list of Redis values.
+ internal void AppendTo(List redisValues)
+ {
+ if (ResetUser)
+ {
+ redisValues.Add(RedisLiterals.RESET);
+ }
+ if (NoPass)
+ {
+ redisValues.Add(RedisLiterals.NOPASS);
+ }
+ if (ResetPass)
+ {
+ redisValues.Add(RedisLiterals.RESETPASS);
+ }
+ if (UserState.HasValue)
+ {
+ redisValues.Add(UserState.ToString());
+ }
+ if (PasswordsToSet is not null)
+ {
+ foreach (var password in PasswordsToSet)
+ {
+ redisValues.Add(">" + password);
+ }
+ }
+ if (PasswordsToRemove is not null)
+ {
+ foreach (var password in PasswordsToRemove)
+ {
+ redisValues.Add("<" + password);
+ }
+ }
+ if (HashedPasswordsToSet is not null)
+ {
+ foreach (var password in HashedPasswordsToSet)
+ {
+ redisValues.Add("#" + password);
+ }
+ }
+ if (HashedPasswordsToRemove is not null)
+ {
+ foreach (var password in HashedPasswordsToRemove)
+ {
+ redisValues.Add("!" + password);
+ }
+ }
+ if (ClearSelectors)
+ {
+ redisValues.Add(RedisLiterals.CLEARSELECTORS);
+ }
+ }
+}
+
+///
+/// Represents the ACL command rules.
+///
+public class ACLCommandRules
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The commands rule.
+ /// The commands allowed.
+ /// The commands disallowed.
+ /// The categories allowed.
+ /// The categories disallowed.
+ /// The keys rule.
+ /// The keys allowed patterns.
+ /// The keys allowed read-for patterns.
+ /// The keys allowed write-for patterns.
+ /// The pub/sub rule.
+ /// The pub/sub allow channels.
+ public ACLCommandRules(
+ ACLCommandsRule? commandsRule,
+ string[]? commandsAllowed,
+ string[]? commandsDisallowed,
+ string[]? categoriesAllowed,
+ string[]? categoriesDisallowed,
+ ACLKeysRule? keysRule,
+ string[]? keysAllowedPatterns,
+ string[]? keysAllowedReadForPatterns,
+ string[]? keysAllowedWriteForPatterns,
+ ACLPubSubRule? pubSubRule,
+ string[]? pubSubAllowChannels)
+ {
+ CommandsRule = commandsRule;
+ CommandsAllowed = commandsAllowed;
+ CommandsDisallowed = commandsDisallowed;
+ CategoriesAllowed = categoriesAllowed;
+ CategoriesDisallowed = categoriesDisallowed;
+ KeysRule = keysRule;
+ KeysAllowedPatterns = keysAllowedPatterns;
+ KeysAllowedReadForPatterns = keysAllowedReadForPatterns;
+ KeysAllowedWriteForPatterns = keysAllowedWriteForPatterns;
+ PubSubRule = pubSubRule;
+ PubSubAllowChannels = pubSubAllowChannels;
+ }
+
+ ///
+ /// Gets the commands rule.
+ ///
+ public readonly ACLCommandsRule? CommandsRule;
+
+ ///
+ /// Gets the commands allowed.
+ ///
+ public readonly string[]? CommandsAllowed;
+
+ ///
+ /// Gets the commands disallowed.
+ ///
+ public readonly string[]? CommandsDisallowed;
+
+ ///
+ /// Gets the categories allowed.
+ ///
+ public readonly string[]? CategoriesAllowed;
+
+ ///
+ /// Gets the categories disallowed.
+ ///
+ public readonly string[]? CategoriesDisallowed;
+
+ ///
+ /// Gets the keys rule.
+ ///
+ public readonly ACLKeysRule? KeysRule;
+
+ ///
+ /// Gets the keys allowed patterns.
+ ///
+ public readonly string[]? KeysAllowedPatterns;
+
+ ///
+ /// Gets the keys allowed read-for patterns.
+ ///
+ public readonly string[]? KeysAllowedReadForPatterns;
+
+ ///
+ /// Gets the keys allowed write-for patterns.
+ ///
+ public readonly string[]? KeysAllowedWriteForPatterns;
+
+ ///
+ /// Gets the pub/sub rule.
+ ///
+ public readonly ACLPubSubRule? PubSubRule;
+
+ ///
+ /// Gets the pub/sub allow channels.
+ ///
+ public readonly string[]? PubSubAllowChannels;
+
+ ///
+ /// Appends the ACL command rules to the specified list of Redis values.
+ ///
+ /// The list of Redis values.
+ internal void AppendTo(List redisValues)
+ {
+ if (CommandsRule.HasValue)
+ {
+ redisValues.Add(CommandsRule.ToString());
+ }
+ if (CommandsAllowed is not null)
+ {
+ foreach (var command in CommandsAllowed)
+ {
+ redisValues.Add(RedisLiterals.PlusSymbol + command);
+ }
+ }
+ if (CommandsDisallowed is not null)
+ {
+ foreach (var command in CommandsDisallowed)
+ {
+ redisValues.Add(RedisLiterals.MinusSymbol + command);
+ }
+ }
+ if (CategoriesAllowed is not null)
+ {
+ foreach (var category in CategoriesAllowed)
+ {
+ redisValues.Add("+@" + category);
+ }
+ }
+ if (CategoriesDisallowed is not null)
+ {
+ foreach (var category in CategoriesDisallowed)
+ {
+ redisValues.Add("-@" + category);
+ }
+ }
+ if (KeysRule.HasValue)
+ {
+ redisValues.Add(KeysRule.ToString());
+ }
+ if (KeysAllowedPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedPatterns)
+ {
+ redisValues.Add("~" + pattern);
+ }
+ }
+ if (KeysAllowedReadForPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedReadForPatterns)
+ {
+ redisValues.Add("%R~" + pattern);
+ }
+ }
+ if (KeysAllowedWriteForPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedWriteForPatterns)
+ {
+ redisValues.Add("%W~" + pattern);
+ }
+ }
+ if (PubSubRule.HasValue)
+ {
+ redisValues.Add(PubSubRule.ToString());
+ }
+ if (PubSubAllowChannels is not null)
+ {
+ foreach (var channel in PubSubAllowChannels)
+ {
+ redisValues.Add("&" + channel);
+ }
+ }
+ }
+}
+
+///
+/// Represents the ACL selector rules.
+///
+public class ACLSelectorRules
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The commands allowed.
+ /// The commands disallowed.
+ /// The categories allowed.
+ /// The categories disallowed.
+ /// The keys allowed patterns.
+ /// The keys allowed read-for patterns.
+ /// The keys allowed write-for patterns.
+ public ACLSelectorRules(
+ string[]? commandsAllowed,
+ string[]? commandsDisallowed,
+ string[]? categoriesAllowed,
+ string[]? categoriesDisallowed,
+ string[]? keysAllowedPatterns,
+ string[]? keysAllowedReadForPatterns,
+ string[]? keysAllowedWriteForPatterns)
+ {
+ CommandsAllowed = commandsAllowed;
+ CommandsDisallowed = commandsDisallowed;
+ CategoriesAllowed = categoriesAllowed;
+ CategoriesDisallowed = categoriesDisallowed;
+ KeysAllowedPatterns = keysAllowedPatterns;
+ KeysAllowedReadForPatterns = keysAllowedReadForPatterns;
+ KeysAllowedWriteForPatterns = keysAllowedWriteForPatterns;
+ }
+
+ ///
+ /// Gets the commands allowed.
+ ///
+ public readonly string[]? CommandsAllowed;
+
+ ///
+ /// Gets the commands disallowed.
+ ///
+ public readonly string[]? CommandsDisallowed;
+
+ ///
+ /// Gets the categories allowed.
+ ///
+ public readonly string[]? CategoriesAllowed;
+
+ ///
+ /// Gets the categories disallowed.
+ ///
+ public readonly string[]? CategoriesDisallowed;
+
+ ///
+ /// Gets the keys allowed patterns.
+ ///
+ public readonly string[]? KeysAllowedPatterns;
+
+ ///
+ /// Gets the keys allowed read-for patterns.
+ ///
+ public readonly string[]? KeysAllowedReadForPatterns;
+
+ ///
+ /// Gets the keys allowed write-for patterns.
+ ///
+ public readonly string[]? KeysAllowedWriteForPatterns;
+
+ ///
+ /// Appends the ACL selector rules to the specified list of Redis values.
+ ///
+ /// The list of Redis values.
+ internal void AppendTo(List redisValues)
+ {
+ redisValues.Add("(");
+ if (CommandsAllowed is not null)
+ {
+ foreach (var command in CommandsAllowed)
+ {
+ redisValues.Add("+" + command);
+ }
+ }
+ if (CommandsDisallowed is not null)
+ {
+ foreach (var command in CommandsDisallowed)
+ {
+ redisValues.Add("-" + command);
+ }
+ }
+ if (CategoriesAllowed is not null)
+ {
+ foreach (var category in CategoriesAllowed)
+ {
+ redisValues.Add("+@" + category);
+ }
+ }
+ if (CategoriesDisallowed is not null)
+ {
+ foreach (var category in CategoriesDisallowed)
+ {
+ redisValues.Add("-@" + category);
+ }
+ }
+ if (KeysAllowedPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedPatterns)
+ {
+ redisValues.Add("~" + pattern);
+ }
+ }
+ if (KeysAllowedReadForPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedReadForPatterns)
+ {
+ redisValues.Add("%R~" + pattern);
+ }
+ }
+ if (KeysAllowedWriteForPatterns is not null)
+ {
+ foreach (var pattern in KeysAllowedWriteForPatterns)
+ {
+ redisValues.Add("%W~" + pattern);
+ }
+ }
+ redisValues.Add(")");
+ }
+}
+
+///
+/// Represents the state of an ACL user.
+///
+public enum ACLUserState
+{
+ ///
+ /// The user is on.
+ ///
+ ON,
+
+ ///
+ /// The user is off.
+ ///
+ OFF,
+}
+
+///
+/// Represents the ACL commands rule.
+///
+public enum ACLCommandsRule
+{
+ ///
+ /// All commands are allowed.
+ ///
+ ALLCOMMANDS,
+
+ ///
+ /// No commands are allowed.
+ ///
+ NOCOMMANDS,
+}
+
+///
+/// Represents the ACL keys rule.
+///
+public enum ACLKeysRule
+{
+ ///
+ /// All keys are allowed.
+ ///
+ ALLKEYS,
+
+ ///
+ /// Keys are reset.
+ ///
+ RESETKEYS,
+}
+
+///
+/// Represents the ACL pub/sub rule.
+///
+public enum ACLPubSubRule
+{
+ ///
+ /// All channels are allowed.
+ ///
+ ALLCHANNELS,
+
+ ///
+ /// Channels are reset.
+ ///
+ RESETCHANNELS,
+}
diff --git a/src/StackExchange.Redis/APITypes/ACLRulesBuilder.cs b/src/StackExchange.Redis/APITypes/ACLRulesBuilder.cs
new file mode 100644
index 000000000..69c2b601f
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/ACLRulesBuilder.cs
@@ -0,0 +1,462 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace StackExchange.Redis;
+
+///
+/// Main Builder for ACLRules.
+///
+public class ACLRulesBuilder
+{
+ private ACLUserRulesBuilder? _aCLUserRulesBuilder;
+ private ACLCommandRulesBuilder? _aCLCommandRulesBuilder;
+ private List? _aCLSelectorRulesBuilderList;
+
+ ///
+ /// Adds ACL user rules.
+ ///
+ /// The action to build ACL user rules.
+ /// The current instance of .
+ public ACLRulesBuilder WithACLUserRules(Action buildAction)
+ {
+ buildAction(_aCLUserRulesBuilder ??= new ACLUserRulesBuilder());
+ return this;
+ }
+
+ ///
+ /// Adds ACL command rules.
+ ///
+ /// The action to build ACL command rules.
+ /// The current instance of .
+ public ACLRulesBuilder WithACLCommandRules(Action buildAction)
+ {
+ buildAction(_aCLCommandRulesBuilder ??= new ACLCommandRulesBuilder());
+ return this;
+ }
+
+ ///
+ /// Appends ACL selector rules.
+ ///
+ /// The action to build ACL selector rules.
+ /// The current instance of .
+ public ACLRulesBuilder AppendACLSelectorRules(Action buildAction)
+ {
+ _aCLSelectorRulesBuilderList ??= new List();
+ var newSelectorRule = new ACLSelectorRulesBuilder();
+ buildAction(newSelectorRule);
+ _aCLSelectorRulesBuilderList.Add(newSelectorRule);
+ return this;
+ }
+
+ ///
+ /// Builds the ACL rules.
+ ///
+ /// The built .
+ public ACLRules Build()
+ {
+ return new ACLRules(
+ _aCLUserRulesBuilder?.Build(),
+ _aCLCommandRulesBuilder?.Build(),
+ _aCLSelectorRulesBuilderList?.Select(item => item.Build()).ToArray());
+ }
+}
+
+///
+/// Builder for ACLUserRules.
+///
+public class ACLUserRulesBuilder
+{
+ private bool _resetUser = false;
+ private bool _noPass = false;
+ private bool _resetPass = false;
+ private ACLUserState? _userState;
+ private string[]? _passwordsToSet;
+ private string[]? _passwordsToRemove;
+ private string[]? _hashedPasswordsToSet;
+ private string[]? _hashedPasswordsToRemove;
+ private bool _clearSelectors = false;
+
+ ///
+ /// Resets the user.
+ ///
+ /// If set to true, resets the user.
+ /// The current instance of .
+ public ACLUserRulesBuilder ResetUser(bool resetUser)
+ {
+ _resetUser = resetUser;
+ return this;
+ }
+
+ ///
+ /// Sets the no pass flag.
+ ///
+ /// If set to true, sets the no pass flag.
+ /// The current instance of .
+ public ACLUserRulesBuilder NoPass(bool noPass)
+ {
+ _noPass = noPass;
+ return this;
+ }
+
+ ///
+ /// Resets the password.
+ ///
+ /// If set to true, resets the password.
+ /// The current instance of .
+ public ACLUserRulesBuilder ResetPass(bool resetPass)
+ {
+ _resetPass = resetPass;
+ return this;
+ }
+
+ ///
+ /// Sets the user state.
+ ///
+ /// The user state.
+ /// The current instance of .
+ public ACLUserRulesBuilder UserState(ACLUserState? userState)
+ {
+ _userState = userState;
+ return this;
+ }
+
+ ///
+ /// Sets the passwords to set.
+ ///
+ /// The passwords to set.
+ /// The current instance of .
+ public ACLUserRulesBuilder PasswordsToSet(params string[] passwords)
+ {
+ _passwordsToSet = passwords;
+ return this;
+ }
+
+ ///
+ /// Sets the passwords to remove.
+ ///
+ /// The passwords to remove.
+ /// The current instance of .
+ public ACLUserRulesBuilder PasswordsToRemove(params string[] passwords)
+ {
+ _passwordsToRemove = passwords;
+ return this;
+ }
+
+ ///
+ /// Sets the hashed passwords to set.
+ ///
+ /// The hashed passwords to set.
+ /// The current instance of .
+ public ACLUserRulesBuilder HashedPasswordsToSet(params string[] hashedPasswords)
+ {
+ _hashedPasswordsToSet = hashedPasswords;
+ return this;
+ }
+
+ ///
+ /// Sets the hashed passwords to remove.
+ ///
+ /// The hashed passwords to remove.
+ /// The current instance of .
+ public ACLUserRulesBuilder HashedPasswordsToRemove(params string[] hashedPasswords)
+ {
+ _hashedPasswordsToRemove = hashedPasswords;
+ return this;
+ }
+
+ ///
+ /// Clears the selectors.
+ ///
+ /// If set to true, clears the selectors.
+ /// The current instance of .
+ public ACLUserRulesBuilder ClearSelectors(bool clearSelectors)
+ {
+ _clearSelectors = clearSelectors;
+ return this;
+ }
+
+ ///
+ /// Builds the ACL user rules.
+ ///
+ /// The built .
+ public ACLUserRules Build()
+ {
+ return new ACLUserRules(
+ _resetUser,
+ _noPass,
+ _resetPass,
+ _userState,
+ _passwordsToSet,
+ _passwordsToRemove,
+ _hashedPasswordsToSet,
+ _hashedPasswordsToRemove,
+ _clearSelectors);
+ }
+}
+
+///
+/// Builder for ACLCommandRules.
+///
+public class ACLCommandRulesBuilder
+{
+ private ACLCommandsRule? _commandsRule;
+ private string[]? _commandsAllowed;
+ private string[]? _commandsDisallowed;
+ private string[]? _categoriesAllowed;
+ private string[]? _categoriesDisallowed;
+ private ACLKeysRule? _keysRule;
+ private string[]? _keysAllowedPatterns;
+ private string[]? _keysAllowedReadForPatterns;
+ private string[]? _keysAllowedWriteForPatterns;
+ private ACLPubSubRule? _pubSubRule;
+ private string[]? _pubSubAllowChannels;
+
+ ///
+ /// Sets the commands rule.
+ ///
+ /// The commands rule.
+ /// The current instance of .
+ public ACLCommandRulesBuilder CommandsRule(ACLCommandsRule? commandsRule)
+ {
+ _commandsRule = commandsRule;
+ return this;
+ }
+
+ ///
+ /// Sets the commands allowed.
+ ///
+ /// The commands allowed.
+ /// The current instance of .
+ public ACLCommandRulesBuilder CommandsAllowed(params string[] commands)
+ {
+ _commandsAllowed = commands;
+ return this;
+ }
+
+ ///
+ /// Sets the commands disallowed.
+ ///
+ /// The commands disallowed.
+ /// The current instance of .
+ public ACLCommandRulesBuilder CommandsDisallowed(params string[] commands)
+ {
+ _commandsDisallowed = commands;
+ return this;
+ }
+
+ ///
+ /// Sets the categories allowed.
+ ///
+ /// The categories allowed.
+ /// The current instance of .
+ public ACLCommandRulesBuilder CategoriesAllowed(params string[] categories)
+ {
+ _categoriesAllowed = categories;
+ return this;
+ }
+
+ ///
+ /// Sets the categories disallowed.
+ ///
+ /// The categories disallowed.
+ /// The current instance of .
+ public ACLCommandRulesBuilder CategoriesDisallowed(params string[] categories)
+ {
+ _categoriesDisallowed = categories;
+ return this;
+ }
+
+ ///
+ /// Sets the keys rule.
+ ///
+ /// The keys rule.
+ /// The current instance of .
+ public ACLCommandRulesBuilder KeysRule(ACLKeysRule? keysRule)
+ {
+ _keysRule = keysRule;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed patterns.
+ ///
+ /// The keys allowed patterns.
+ /// The current instance of .
+ public ACLCommandRulesBuilder KeysAllowedPatterns(params string[] patterns)
+ {
+ _keysAllowedPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed read for patterns.
+ ///
+ /// The keys allowed read for patterns.
+ /// The current instance of .
+ public ACLCommandRulesBuilder KeysAllowedReadForPatterns(params string[] patterns)
+ {
+ _keysAllowedReadForPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed write for patterns.
+ ///
+ /// The keys allowed write for patterns.
+ /// The current instance of .
+ public ACLCommandRulesBuilder KeysAllowedWriteForPatterns(params string[] patterns)
+ {
+ _keysAllowedWriteForPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Sets the pub/sub rule.
+ ///
+ /// The pub/sub rule.
+ /// The current instance of .
+ public ACLCommandRulesBuilder PubSubRule(ACLPubSubRule? pubSubRule)
+ {
+ _pubSubRule = pubSubRule;
+ return this;
+ }
+
+ ///
+ /// Sets the pub/sub allow channels.
+ ///
+ /// The pub/sub allow channels.
+ /// The current instance of .
+ public ACLCommandRulesBuilder PubSubAllowChannels(params string[] channels)
+ {
+ _pubSubAllowChannels = channels;
+ return this;
+ }
+
+ ///
+ /// Builds the ACL command rules.
+ ///
+ /// The built .
+ public ACLCommandRules Build()
+ {
+ return new ACLCommandRules(
+ _commandsRule,
+ _commandsAllowed,
+ _commandsDisallowed,
+ _categoriesAllowed,
+ _categoriesDisallowed,
+ _keysRule,
+ _keysAllowedPatterns,
+ _keysAllowedReadForPatterns,
+ _keysAllowedWriteForPatterns,
+ _pubSubRule,
+ _pubSubAllowChannels);
+ }
+}
+
+///
+/// Builder for ACLSelectorRules.
+///
+public class ACLSelectorRulesBuilder
+{
+ private string[]? _commandsAllowed;
+ private string[]? _commandsDisallowed;
+ private string[]? _categoriesAllowed;
+ private string[]? _categoriesDisallowed;
+ private string[]? _keysAllowedPatterns;
+ private string[]? _keysAllowedReadForPatterns;
+ private string[]? _keysAllowedWriteForPatterns;
+
+ ///
+ /// Sets the commands allowed.
+ ///
+ /// The commands allowed.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder CommandsAllowed(params string[] commands)
+ {
+ _commandsAllowed = commands;
+ return this;
+ }
+
+ ///
+ /// Sets the commands disallowed.
+ ///
+ /// The commands disallowed.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder CommandsDisallowed(params string[] commands)
+ {
+ _commandsDisallowed = commands;
+ return this;
+ }
+
+ ///
+ /// Sets the categories allowed.
+ ///
+ /// The categories allowed.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder CategoriesAllowed(params string[] categories)
+ {
+ _categoriesAllowed = categories;
+ return this;
+ }
+
+ ///
+ /// Sets the categories disallowed.
+ ///
+ /// The categories disallowed.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder CategoriesDisallowed(params string[] categories)
+ {
+ _categoriesDisallowed = categories;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed patterns.
+ ///
+ /// The keys allowed patterns.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder KeysAllowedPatterns(params string[] patterns)
+ {
+ _keysAllowedPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed read for patterns.
+ ///
+ /// The keys allowed read for patterns.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder KeysAllowedReadForPatterns(params string[] patterns)
+ {
+ _keysAllowedReadForPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Sets the keys allowed write for patterns.
+ ///
+ /// The keys allowed write for patterns.
+ /// The current instance of .
+ public ACLSelectorRulesBuilder KeysAllowedWriteForPatterns(params string[] patterns)
+ {
+ _keysAllowedWriteForPatterns = patterns;
+ return this;
+ }
+
+ ///
+ /// Builds the ACL selector rules.
+ ///
+ /// The built .
+ public ACLSelectorRules Build()
+ {
+ return new ACLSelectorRules(
+ _commandsAllowed,
+ _commandsDisallowed,
+ _categoriesAllowed,
+ _categoriesDisallowed,
+ _keysAllowedPatterns,
+ _keysAllowedReadForPatterns,
+ _keysAllowedWriteForPatterns);
+ }
+}
diff --git a/src/StackExchange.Redis/APITypes/ACLUser.cs b/src/StackExchange.Redis/APITypes/ACLUser.cs
new file mode 100644
index 000000000..778c6cca2
--- /dev/null
+++ b/src/StackExchange.Redis/APITypes/ACLUser.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+
+namespace StackExchange.Redis;
+
+///
+/// Represents an Access Control List (ACL) user with various properties such as flags, passwords, commands, keys, channels, and selectors.
+///
+public class ACLUser
+{
+ ///
+ /// A dictionary containing user information.
+ ///
+ public readonly Dictionary? UserInfo;
+
+ ///
+ /// An array of flags associated with the user.
+ ///
+ public readonly string[]? Flags;
+
+ ///
+ /// An array of passwords associated with the user.
+ ///
+ public readonly string[]? Passwords;
+
+ ///
+ /// A string representing the commands associated with the user.
+ ///
+ public readonly string? Commands;
+
+ ///
+ /// A string representing the keys associated with the user.
+ ///
+ public readonly string? Keys;
+
+ ///
+ /// A string representing the channels associated with the user.
+ ///
+ public readonly string? Channels;
+
+ ///
+ /// An array of selectors associated with the user.
+ ///
+ public readonly ACLSelector[]? Selectors;
+
+ ///
+ /// Initializes a new instance of the class with specified parameters.
+ ///
+ /// A dictionary containing user information.
+ /// An array oflags associated with the user.
+ /// An array opasswords associated with the user.
+ /// A string representing the commands associated with the user.
+ /// A string representing the keys associated with the user.
+ /// A string representing the channels associated with the user.
+ /// An array oselectors associated with the user.
+ public ACLUser(Dictionary? userInfo, string[]? flags, string[]? passwords, string? commands, string? keys, string? channels, ACLSelector[]? selectors)
+ {
+ UserInfo = userInfo;
+ Flags = flags;
+ Passwords = passwords;
+ Commands = commands;
+ Keys = keys;
+ Channels = channels;
+ Selectors = selectors;
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override string ToString()
+ {
+ return "AccessControlUser{" + "Flags=" + Flags + ", Passwords=" + Passwords
+ + ", Commands='" + Commands + "', Keys='" + Keys + "', Channels='" + Channels
+ + "', Selectors=" + Selectors + "}";
+ }
+}
+
+///
+/// Represents an Access Control List (ACL) selector for a Redis user.
+///
+public class ACLSelector
+{
+ ///
+ /// Gets the commands associated with the ACL user.
+ ///
+ public readonly string? Commands;
+
+ ///
+ /// Gets the keys associated with the ACL user.
+ ///
+ public readonly string? Keys;
+
+ ///
+ /// Gets the channels associated with the ACL user.
+ ///
+ public readonly string? Channels;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The commands associated with the ACLSelector.
+ /// The keys associated with the ACLSelector.
+ /// The channels associated with the ACLSelector.
+ public ACLSelector(string? commands, string? keys, string? channels)
+ {
+ Commands = commands;
+ Keys = keys;
+ Channels = channels;
+ }
+
+ ///
+ /// Returns a string that represents the current object.
+ ///
+ /// A string that represents the current object.
+ public override string ToString()
+ {
+ return "ACLSelector{" + "Commands='" + Commands + "', Keys='" + Keys + "', Channels='" + Channels + "'}";
+ }
+}
diff --git a/src/StackExchange.Redis/Enums/RedisCommand.cs b/src/StackExchange.Redis/Enums/RedisCommand.cs
index a4647d7eb..e3c022dda 100644
--- a/src/StackExchange.Redis/Enums/RedisCommand.cs
+++ b/src/StackExchange.Redis/Enums/RedisCommand.cs
@@ -5,7 +5,7 @@ namespace StackExchange.Redis;
internal enum RedisCommand
{
NONE, // must be first for "zero reasons"
-
+ ACL,
APPEND,
ASKING,
AUTH,
@@ -358,6 +358,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
return true;
// Commands that can be issued anywhere
case RedisCommand.NONE:
+ case RedisCommand.ACL:
case RedisCommand.ASKING:
case RedisCommand.AUTH:
case RedisCommand.BGREWRITEAOF:
diff --git a/src/StackExchange.Redis/ExtensionMethods.cs b/src/StackExchange.Redis/ExtensionMethods.cs
index 87904aa9c..8c3ad4065 100644
--- a/src/StackExchange.Redis/ExtensionMethods.cs
+++ b/src/StackExchange.Redis/ExtensionMethods.cs
@@ -342,5 +342,20 @@ internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan span)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static TTo[]? ToArray(in this RawResult result, Projection selector, in TState state)
=> result.IsNull ? null : result.GetItems().ToArray(selector, in state);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static List? ToList(this RawResult result, Func selector)
+ {
+ List? list = null;
+ if (!result.IsNull)
+ {
+ list = new List();
+ foreach (var item in result.GetItems())
+ {
+ list.Add(selector(item));
+ }
+ }
+ return list;
+ }
}
}
diff --git a/src/StackExchange.Redis/Interfaces/IServer.cs b/src/StackExchange.Redis/Interfaces/IServer.cs
index fad2d4232..579d5cd7e 100644
--- a/src/StackExchange.Redis/Interfaces/IServer.cs
+++ b/src/StackExchange.Redis/Interfaces/IServer.cs
@@ -77,6 +77,184 @@ public partial interface IServer : IRedis
///
int DatabaseCount { get; }
+ ///
+ /// Gets the categories of access control commands.
+ ///
+ /// The command flags to use.
+ /// An array of Redis values representing the categories.
+ RedisValue[] AccessControlGetCategories(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously gets the categories of access control commands.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with an array of Redis values representing the categories.
+ Task AccessControlGetCategoriesAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the access control commands for a specified category.
+ ///
+ /// The category to get commands for.
+ /// The command flags to use.
+ /// An array of Redis values representing the commands.
+ RedisValue[] AccessControlGetCommands(RedisValue category, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously gets the access control commands for a specified category.
+ ///
+ /// The category to get commands for.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with an array of Redis values representing the commands.
+ Task AccessControlGetCommandsAsync(RedisValue category, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Deletes specified access control users.
+ ///
+ /// The usernames of the users to delete.
+ /// The command flags to use.
+ /// The number of users deleted.
+ long AccessControlDeleteUsers(RedisValue[] usernames, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously deletes specified access control users.
+ ///
+ /// The usernames of the users to delete.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with the number of users deleted.
+ Task AccessControlDeleteUsersAsync(RedisValue[] usernames, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Generates a password for access control.
+ ///
+ /// The number of bits for the password.
+ /// The command flags to use.
+ /// The generated password as a Redis value.
+ RedisValue AccessControlGeneratePassword(long bits, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously generates a password for access control.
+ ///
+ /// The number of bits for the password.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with the generated password as a Redis value.
+ Task AccessControlGeneratePasswordAsync(long bits, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the access control user for a specified username.
+ ///
+ /// The username to get the access control user for.
+ /// The command flags to use.
+ /// The access control user associated with the specified username, or null if not found.
+ ACLUser? AccessControlGetUser(RedisValue username, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the access control user for a specified username.
+ ///
+ /// The username to get the access control user for.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with the access control user associated with the specified username, or null if not found.
+ Task AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Lists all access control rules.
+ ///
+ /// The command flags to use.
+ /// An array of Redis values representing the access control rules.
+ RedisValue[]? AccessControlList(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously lists all access control rules.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with an array of Redis values representing the access control rules.
+ Task AccessControlListAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Loads access control rules.
+ ///
+ /// The command flags to use.
+ void AccessControlLoad(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously loads access control rules.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation.
+ Task AccessControlLoadAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Resets the access control log.
+ ///
+ /// The command flags to use.
+ void AccessControlLogReset(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously resets the access control log.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation.
+ Task AccessControlLogResetAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the access control log.
+ ///
+ /// The number of log entries to retrieve.
+ /// The command flags to use.
+ /// An array of key-value pairs representing the log entries.
+ KeyValuePair[][] AccessControlLog(long count, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously gets the access control log.
+ ///
+ /// The number of log entries to retrieve.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with an array of key-value pairs representing the log entries.
+ Task[][]> AccessControlLogAsync(long count, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Saves access control rules.
+ ///
+ /// The command flags to use.
+ void AccessControlSave(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously saves access control rules.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation.
+ Task AccessControlSaveAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Gets the current access control user.
+ ///
+ /// The command flags to use.
+ /// The current access control user as a Redis value.
+ RedisValue AccessControlWhoAmI(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously gets the current access control user.
+ ///
+ /// The command flags to use.
+ /// A task representing the asynchronous operation, with the current access control user as a Redis value.
+ Task AccessControlWhoAmIAsync(CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Sets access control rules for a user.
+ ///
+ /// The username to set rules for.
+ /// The access control rules to set.
+ /// The command flags to use.
+ void AccessControlSetUser(RedisValue userName, ACLRules rules, CommandFlags flags = CommandFlags.None);
+
+ ///
+ /// Asynchronously sets access control rules for a user.
+ ///
+ /// The username to set rules for.
+ /// The access control rules to set.
+ /// The command flags to use.
+ /// A task representing the asynchronous operation.
+ Task AccessControlSetUserAsync(RedisValue userName, ACLRules rules, CommandFlags flags = CommandFlags.None);
+
///
/// The CLIENT KILL command closes a given client connection identified by ip:port.
/// The ip:port should match a line returned by the CLIENT LIST command.
diff --git a/src/StackExchange.Redis/Message.cs b/src/StackExchange.Redis/Message.cs
index b89a6b946..98a7f7b1a 100644
--- a/src/StackExchange.Redis/Message.cs
+++ b/src/StackExchange.Redis/Message.cs
@@ -174,6 +174,7 @@ public bool IsAdmin
{
switch (Command)
{
+ case RedisCommand.ACL:
case RedisCommand.BGREWRITEAOF:
case RedisCommand.BGSAVE:
case RedisCommand.CLIENT:
@@ -525,6 +526,26 @@ internal static Message Create(int db, CommandFlags flags, RedisCommand command,
return new CommandKeyValuesKeyMessage(db, flags, command, key0, values, key1);
}
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, RedisValue value, RedisValue[] values)
+ {
+#if NET6_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(values);
+#else
+ if (values == null) throw new ArgumentNullException(nameof(values));
+#endif
+ return new CommandValueValuesMessage(db, flags, command, value, values);
+ }
+
+ internal static Message Create(int db, CommandFlags flags, RedisCommand command, RedisValue value0, RedisValue value1, RedisValue[] values)
+ {
+#if NET6_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(values);
+#else
+ if (values == null) throw new ArgumentNullException(nameof(values));
+#endif
+ return new CommandValueValueValuesMessage(db, flags, command, value0, value1, values);
+ }
+
internal static CommandFlags GetPrimaryReplicaFlags(CommandFlags flags)
{
// for the purposes of the switch, we only care about two bits
@@ -535,6 +556,7 @@ internal static bool RequiresDatabase(RedisCommand command)
{
switch (command)
{
+ case RedisCommand.ACL:
case RedisCommand.ASKING:
case RedisCommand.AUTH:
case RedisCommand.BGREWRITEAOF:
@@ -1048,6 +1070,63 @@ protected override void WriteImpl(PhysicalConnection physical)
public override int ArgCount => values.Length;
}
+ private sealed class CommandValueValuesMessage : Message
+ {
+ private readonly RedisValue value;
+ private readonly RedisValue[] values;
+
+ public CommandValueValuesMessage(int db, CommandFlags flags, RedisCommand command, RedisValue value, RedisValue[] values) : base(db, flags, command)
+ {
+ this.value = value;
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i].AssertNotNull();
+ }
+ this.values = values;
+ }
+
+ protected override void WriteImpl(PhysicalConnection physical)
+ {
+ physical.WriteHeader(command, values.Length + 1);
+ physical.WriteBulkString(value);
+ for (int i = 0; i < values.Length; i++)
+ {
+ physical.WriteBulkString(values[i]);
+ }
+ }
+ public override int ArgCount => values.Length + 1;
+ }
+
+ private sealed class CommandValueValueValuesMessage : Message
+ {
+ private readonly RedisValue value0;
+ private readonly RedisValue value1;
+ private readonly RedisValue[] values;
+
+ public CommandValueValueValuesMessage(int db, CommandFlags flags, RedisCommand command, RedisValue value0, RedisValue value1, RedisValue[] values) : base(db, flags, command)
+ {
+ this.value0 = value0;
+ this.value1 = value1;
+ for (int i = 0; i < values.Length; i++)
+ {
+ values[i].AssertNotNull();
+ }
+ this.values = values;
+ }
+
+ protected override void WriteImpl(PhysicalConnection physical)
+ {
+ physical.WriteHeader(command, values.Length + 2);
+ physical.WriteBulkString(value0);
+ physical.WriteBulkString(value1);
+ for (int i = 0; i < values.Length; i++)
+ {
+ physical.WriteBulkString(values[i]);
+ }
+ }
+ public override int ArgCount => values.Length + 2;
+ }
+
private sealed class CommandKeysMessage : Message
{
private readonly RedisKey[] keys;
diff --git a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
index a24333c8e..7cbd43432 100644
--- a/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
+++ b/src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
@@ -1,5 +1,7 @@
#nullable enable
abstract StackExchange.Redis.RedisResult.IsNull.get -> bool
+override StackExchange.Redis.ACLSelector.ToString() -> string!
+override StackExchange.Redis.ACLUser.ToString() -> string!
override StackExchange.Redis.ChannelMessage.Equals(object? obj) -> bool
override StackExchange.Redis.ChannelMessage.GetHashCode() -> int
override StackExchange.Redis.ChannelMessage.ToString() -> string!
@@ -55,6 +57,112 @@ override StackExchange.Redis.SocketManager.ToString() -> string!
override StackExchange.Redis.SortedSetEntry.Equals(object? obj) -> bool
override StackExchange.Redis.SortedSetEntry.GetHashCode() -> int
override StackExchange.Redis.SortedSetEntry.ToString() -> string!
+readonly StackExchange.Redis.ACLCommandRules.CategoriesAllowed -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.CategoriesDisallowed -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.CommandsAllowed -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.CommandsDisallowed -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.CommandsRule -> StackExchange.Redis.ACLCommandsRule?
+readonly StackExchange.Redis.ACLCommandRules.KeysAllowedPatterns -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.KeysAllowedReadForPatterns -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.KeysAllowedWriteForPatterns -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.KeysRule -> StackExchange.Redis.ACLKeysRule?
+readonly StackExchange.Redis.ACLCommandRules.PubSubAllowChannels -> string![]?
+readonly StackExchange.Redis.ACLCommandRules.PubSubRule -> StackExchange.Redis.ACLPubSubRule?
+readonly StackExchange.Redis.ACLRules.AclCommandRules -> StackExchange.Redis.ACLCommandRules?
+readonly StackExchange.Redis.ACLRules.AclSelectorRules -> StackExchange.Redis.ACLSelectorRules![]?
+readonly StackExchange.Redis.ACLRules.AclUserRules -> StackExchange.Redis.ACLUserRules?
+readonly StackExchange.Redis.ACLSelectorRules.CategoriesAllowed -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.CategoriesDisallowed -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.CommandsAllowed -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.CommandsDisallowed -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedPatterns -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedReadForPatterns -> string![]?
+readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedWriteForPatterns -> string![]?
+readonly StackExchange.Redis.ACLSelector.Channels -> string?
+readonly StackExchange.Redis.ACLSelector.Commands -> string?
+readonly StackExchange.Redis.ACLSelector.Keys -> string?
+readonly StackExchange.Redis.ACLUser.Channels -> string?
+readonly StackExchange.Redis.ACLUser.Commands -> string?
+readonly StackExchange.Redis.ACLUser.Flags -> string![]?
+readonly StackExchange.Redis.ACLUser.Keys -> string?
+readonly StackExchange.Redis.ACLUser.Passwords -> string![]?
+readonly StackExchange.Redis.ACLUser.Selectors -> StackExchange.Redis.ACLSelector![]?
+readonly StackExchange.Redis.ACLUser.UserInfo -> System.Collections.Generic.Dictionary?
+readonly StackExchange.Redis.ACLUserRules.ClearSelectors -> bool
+readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToRemove -> string![]?
+readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToSet -> string![]?
+readonly StackExchange.Redis.ACLUserRules.NoPass -> bool
+readonly StackExchange.Redis.ACLUserRules.PasswordsToRemove -> string![]?
+readonly StackExchange.Redis.ACLUserRules.PasswordsToSet -> string![]?
+readonly StackExchange.Redis.ACLUserRules.ResetPass -> bool
+readonly StackExchange.Redis.ACLUserRules.ResetUser -> bool
+readonly StackExchange.Redis.ACLUserRules.UserState -> StackExchange.Redis.ACLUserState?
+StackExchange.Redis.ACLCommandRules
+StackExchange.Redis.ACLCommandRules.ACLCommandRules(StackExchange.Redis.ACLCommandsRule? commandsRule, string![]? commandsAllowed, string![]? commandsDisallowed, string![]? categoriesAllowed, string![]? categoriesDisallowed, StackExchange.Redis.ACLKeysRule? keysRule, string![]? keysAllowedPatterns, string![]? keysAllowedReadForPatterns, string![]? keysAllowedWriteForPatterns, StackExchange.Redis.ACLPubSubRule? pubSubRule, string![]? pubSubAllowChannels) -> void
+StackExchange.Redis.ACLCommandRulesBuilder
+StackExchange.Redis.ACLCommandRulesBuilder.ACLCommandRulesBuilder() -> void
+StackExchange.Redis.ACLCommandRulesBuilder.Build() -> StackExchange.Redis.ACLCommandRules!
+StackExchange.Redis.ACLCommandRulesBuilder.CategoriesAllowed(params string![]! categories) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.CategoriesDisallowed(params string![]! categories) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.CommandsAllowed(params string![]! commands) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.CommandsDisallowed(params string![]! commands) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.CommandsRule(StackExchange.Redis.ACLCommandsRule? commandsRule) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.KeysAllowedPatterns(params string![]! patterns) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.KeysAllowedReadForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.KeysAllowedWriteForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.KeysRule(StackExchange.Redis.ACLKeysRule? keysRule) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.PubSubAllowChannels(params string![]! channels) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandRulesBuilder.PubSubRule(StackExchange.Redis.ACLPubSubRule? pubSubRule) -> StackExchange.Redis.ACLCommandRulesBuilder!
+StackExchange.Redis.ACLCommandsRule
+StackExchange.Redis.ACLCommandsRule.ALLCOMMANDS = 0 -> StackExchange.Redis.ACLCommandsRule
+StackExchange.Redis.ACLCommandsRule.NOCOMMANDS = 1 -> StackExchange.Redis.ACLCommandsRule
+StackExchange.Redis.ACLKeysRule
+StackExchange.Redis.ACLKeysRule.ALLKEYS = 0 -> StackExchange.Redis.ACLKeysRule
+StackExchange.Redis.ACLKeysRule.RESETKEYS = 1 -> StackExchange.Redis.ACLKeysRule
+StackExchange.Redis.ACLPubSubRule
+StackExchange.Redis.ACLPubSubRule.ALLCHANNELS = 0 -> StackExchange.Redis.ACLPubSubRule
+StackExchange.Redis.ACLPubSubRule.RESETCHANNELS = 1 -> StackExchange.Redis.ACLPubSubRule
+StackExchange.Redis.ACLRules
+StackExchange.Redis.ACLRules.ACLRules(StackExchange.Redis.ACLUserRules? aclUserRules, StackExchange.Redis.ACLCommandRules? aclCommandRules, StackExchange.Redis.ACLSelectorRules![]? aclSelectorRules) -> void
+StackExchange.Redis.ACLRulesBuilder
+StackExchange.Redis.ACLRulesBuilder.ACLRulesBuilder() -> void
+StackExchange.Redis.ACLRulesBuilder.AppendACLSelectorRules(System.Action! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
+StackExchange.Redis.ACLRulesBuilder.Build() -> StackExchange.Redis.ACLRules!
+StackExchange.Redis.ACLRulesBuilder.WithACLCommandRules(System.Action! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
+StackExchange.Redis.ACLRulesBuilder.WithACLUserRules(System.Action! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
+StackExchange.Redis.ACLSelector
+StackExchange.Redis.ACLSelector.ACLSelector(string? commands, string? keys, string? channels) -> void
+StackExchange.Redis.ACLSelectorRules
+StackExchange.Redis.ACLSelectorRules.ACLSelectorRules(string![]? commandsAllowed, string![]? commandsDisallowed, string![]? categoriesAllowed, string![]? categoriesDisallowed, string![]? keysAllowedPatterns, string![]? keysAllowedReadForPatterns, string![]? keysAllowedWriteForPatterns) -> void
+StackExchange.Redis.ACLSelectorRulesBuilder
+StackExchange.Redis.ACLSelectorRulesBuilder.ACLSelectorRulesBuilder() -> void
+StackExchange.Redis.ACLSelectorRulesBuilder.Build() -> StackExchange.Redis.ACLSelectorRules!
+StackExchange.Redis.ACLSelectorRulesBuilder.CategoriesAllowed(params string![]! categories) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.CategoriesDisallowed(params string![]! categories) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.CommandsAllowed(params string![]! commands) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.CommandsDisallowed(params string![]! commands) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedReadForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedWriteForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
+StackExchange.Redis.ACLUser
+StackExchange.Redis.ACLUser.ACLUser(System.Collections.Generic.Dictionary? userInfo, string![]? flags, string![]? passwords, string? commands, string? keys, string? channels, StackExchange.Redis.ACLSelector![]? selectors) -> void
+StackExchange.Redis.ACLUserRules
+StackExchange.Redis.ACLUserRules.ACLUserRules(bool resetUser, bool noPass, bool resetPass, StackExchange.Redis.ACLUserState? userState, string![]? passwordsToSet, string![]? passwordsToRemove, string![]? hashedPasswordsToSet, string![]? hashedPasswordsToRemove, bool clearSelectors) -> void
+StackExchange.Redis.ACLUserRulesBuilder
+StackExchange.Redis.ACLUserRulesBuilder.ACLUserRulesBuilder() -> void
+StackExchange.Redis.ACLUserRulesBuilder.Build() -> StackExchange.Redis.ACLUserRules!
+StackExchange.Redis.ACLUserRulesBuilder.ClearSelectors(bool clearSelectors) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.HashedPasswordsToRemove(params string![]! hashedPasswords) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.HashedPasswordsToSet(params string![]! hashedPasswords) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.NoPass(bool noPass) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.PasswordsToRemove(params string![]! passwords) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.PasswordsToSet(params string![]! passwords) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.ResetPass(bool resetPass) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.ResetUser(bool resetUser) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserRulesBuilder.UserState(StackExchange.Redis.ACLUserState? userState) -> StackExchange.Redis.ACLUserRulesBuilder!
+StackExchange.Redis.ACLUserState
+StackExchange.Redis.ACLUserState.OFF = 1 -> StackExchange.Redis.ACLUserState
+StackExchange.Redis.ACLUserState.ON = 0 -> StackExchange.Redis.ACLUserState
StackExchange.Redis.Aggregate
StackExchange.Redis.Aggregate.Max = 2 -> StackExchange.Redis.Aggregate
StackExchange.Redis.Aggregate.Min = 1 -> StackExchange.Redis.Aggregate
@@ -1039,6 +1147,30 @@ StackExchange.Redis.IServer.AllowReplicaWrites.get -> bool
StackExchange.Redis.IServer.AllowReplicaWrites.set -> void
StackExchange.Redis.IServer.AllowSlaveWrites.get -> bool
StackExchange.Redis.IServer.AllowSlaveWrites.set -> void
+StackExchange.Redis.IServer.AccessControlGetCategories(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+StackExchange.Redis.IServer.AccessControlGetCategoriesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlGetCommands(StackExchange.Redis.RedisValue category, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
+StackExchange.Redis.IServer.AccessControlGetCommandsAsync(StackExchange.Redis.RedisValue category, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlGetUser(StackExchange.Redis.RedisValue username, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ACLUser?
+StackExchange.Redis.IServer.AccessControlGetUserAsync(StackExchange.Redis.RedisValue username, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlDeleteUsers(StackExchange.Redis.RedisValue[]! usernames, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
+StackExchange.Redis.IServer.AccessControlDeleteUsersAsync(StackExchange.Redis.RedisValue[]! usernames, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlGeneratePassword(long bits, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IServer.AccessControlGeneratePasswordAsync(long bits, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlList(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]?
+StackExchange.Redis.IServer.AccessControlListAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlLoad(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
+StackExchange.Redis.IServer.AccessControlLoadAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlLogReset(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
+StackExchange.Redis.IServer.AccessControlLogResetAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlLog(long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Collections.Generic.KeyValuePair[]![]!
+StackExchange.Redis.IServer.AccessControlLogAsync(long count, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task[]![]!>!
+StackExchange.Redis.IServer.AccessControlSave(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
+StackExchange.Redis.IServer.AccessControlSaveAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlWhoAmI(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
+StackExchange.Redis.IServer.AccessControlWhoAmIAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
+StackExchange.Redis.IServer.AccessControlSetUser(StackExchange.Redis.RedisValue userName, StackExchange.Redis.ACLRules! rules, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
+StackExchange.Redis.IServer.AccessControlSetUserAsync(StackExchange.Redis.RedisValue userName, StackExchange.Redis.ACLRules! rules, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task!
StackExchange.Redis.IServer.ClientKill(long? id = null, StackExchange.Redis.ClientType? clientType = null, System.Net.EndPoint? endpoint = null, bool skipMe = true, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
StackExchange.Redis.IServer.ClientKill(System.Net.EndPoint! endpoint, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> void
StackExchange.Redis.IServer.ClientKill(StackExchange.Redis.ClientKillFilter! filter, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
diff --git a/src/StackExchange.Redis/RedisLiterals.cs b/src/StackExchange.Redis/RedisLiterals.cs
index 549691fd2..b7daca8c4 100644
--- a/src/StackExchange.Redis/RedisLiterals.cs
+++ b/src/StackExchange.Redis/RedisLiterals.cs
@@ -66,12 +66,15 @@ public static readonly RedisValue
BYLEX = "BYLEX",
BYSCORE = "BYSCORE",
BYTE = "BYTE",
+ CAT = "CAT",
CH = "CH",
CHANNELS = "CHANNELS",
+ CLEARSELECTORS = "CLEARSELECTORS",
COPY = "COPY",
COUNT = "COUNT",
DB = "DB",
@default = "default",
+ DELUSER = "DELUSER",
DESC = "DESC",
DOCTOR = "DOCTOR",
ENCODING = "ENCODING",
@@ -82,9 +85,11 @@ public static readonly RedisValue
FILTERBY = "FILTERBY",
FLUSH = "FLUSH",
FREQ = "FREQ",
+ GENPASS = "GENPASS",
GET = "GET",
GETKEYS = "GETKEYS",
GETNAME = "GETNAME",
+ GETUSER = "GETUSER",
GT = "GT",
HISTORY = "HISTORY",
ID = "ID",
@@ -101,6 +106,7 @@ public static readonly RedisValue
LIMIT = "LIMIT",
LIST = "LIST",
LOAD = "LOAD",
+ LOG = "LOG",
LT = "LT",
MATCH = "MATCH",
MALLOC_STATS = "MALLOC-STATS",
@@ -111,6 +117,7 @@ public static readonly RedisValue
MINMATCHLEN = "MINMATCHLEN",
MODULE = "MODULE",
NODES = "NODES",
+ NOPASS = "NOPASS",
NOSAVE = "NOSAVE",
NOT = "NOT",
NOVALUES = "NOVALUES",
@@ -130,6 +137,7 @@ public static readonly RedisValue
REFCOUNT = "REFCOUNT",
REPLACE = "REPLACE",
RESET = "RESET",
+ RESETPASS = "RESETPASS",
RESETSTAT = "RESETSTAT",
REV = "REV",
REWRITE = "REWRITE",
@@ -139,12 +147,14 @@ public static readonly RedisValue
SET = "SET",
SETINFO = "SETINFO",
SETNAME = "SETNAME",
+ SETUSER = "SETUSER",
SKIPME = "SKIPME",
STATS = "STATS",
STORE = "STORE",
TYPE = "TYPE",
USERNAME = "USERNAME",
WEIGHTS = "WEIGHTS",
+ WHOAMI = "WHOAMI",
WITHMATCHLEN = "WITHMATCHLEN",
WITHSCORES = "WITHSCORES",
WITHVALUES = "WITHVALUES",
diff --git a/src/StackExchange.Redis/RedisServer.cs b/src/StackExchange.Redis/RedisServer.cs
index 8810e1e2b..340d84485 100644
--- a/src/StackExchange.Redis/RedisServer.cs
+++ b/src/StackExchange.Redis/RedisServer.cs
@@ -51,6 +51,150 @@ public bool AllowReplicaWrites
public Version Version => server.Version;
+ public RedisValue[] AccessControlGetCategories(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.CAT);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task AccessControlGetCategoriesAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.CAT);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public RedisValue[] AccessControlGetCommands(RedisValue category, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.CAT, category);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public Task AccessControlGetCommandsAsync(RedisValue category, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.CAT, category);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray, defaultValue: Array.Empty());
+ }
+
+ public long AccessControlDeleteUsers(RedisValue[] usernames, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.DELUSER, usernames);
+ return ExecuteSync(msg, ResultProcessor.Int64);
+ }
+
+ public Task AccessControlDeleteUsersAsync(RedisValue[] usernames, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.DELUSER, usernames);
+ return ExecuteAsync(msg, ResultProcessor.Int64);
+ }
+
+ public RedisValue AccessControlGeneratePassword(long bits, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GENPASS, bits);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task AccessControlGeneratePasswordAsync(long bits, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GENPASS, bits);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public ACLUser? AccessControlGetUser(RedisValue username, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GETUSER, username);
+ return ExecuteSync(msg, ResultProcessor.ACLUser);
+ }
+
+ public Task AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GETUSER, username);
+ return ExecuteAsync(msg, ResultProcessor.ACLUser);
+ }
+
+ public RedisValue[]? AccessControlList(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LIST);
+ return ExecuteSync(msg, ResultProcessor.RedisValueArray);
+ }
+
+ public Task AccessControlListAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LIST);
+ return ExecuteAsync(msg, ResultProcessor.RedisValueArray);
+ }
+
+ public void AccessControlLoad(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOAD);
+ ExecuteSync(msg, ResultProcessor.DemandOK);
+ }
+
+ public Task AccessControlLoadAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOAD);
+ return ExecuteAsync(msg, ResultProcessor.DemandOK);
+ }
+
+ public void AccessControlLogReset(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOG, RedisLiterals.RESET);
+ ExecuteSync(msg, ResultProcessor.DemandOK);
+ }
+
+ public Task AccessControlLogResetAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOG, RedisLiterals.RESET);
+ return ExecuteAsync(msg, ResultProcessor.DemandOK);
+ }
+
+ public KeyValuePair[][] AccessControlLog(long count, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOG, count);
+ return ExecuteSync(msg, ResultProcessor.ArrayOfKeyValueArray, defaultValue: Array.Empty[]>());
+ }
+
+ public Task[][]> AccessControlLogAsync(long count, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOG, count);
+ return ExecuteAsync(msg, ResultProcessor.ArrayOfKeyValueArray, defaultValue: Array.Empty[]>());
+ }
+
+ public void AccessControlSave(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.SAVE);
+ ExecuteSync(msg, ResultProcessor.DemandOK);
+ }
+
+ public Task AccessControlSaveAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.SAVE);
+ return ExecuteAsync(msg, ResultProcessor.DemandOK);
+ }
+
+ public RedisValue AccessControlWhoAmI(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.WHOAMI);
+ return ExecuteSync(msg, ResultProcessor.RedisValue);
+ }
+
+ public Task AccessControlWhoAmIAsync(CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.WHOAMI);
+ return ExecuteAsync(msg, ResultProcessor.RedisValue);
+ }
+
+ public void AccessControlSetUser(RedisValue userName, ACLRules rules, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.SETUSER, userName, rules.ToRedisValues());
+ ExecuteSync(msg, ResultProcessor.DemandOK);
+ }
+
+ public Task AccessControlSetUserAsync(RedisValue userName, ACLRules rules, CommandFlags flags = CommandFlags.None)
+ {
+ var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.SETUSER, userName, rules.ToRedisValues());
+ return ExecuteAsync(msg, ResultProcessor.DemandOK);
+ }
+
public void ClientKill(EndPoint endpoint, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.CLIENT, RedisLiterals.KILL, (RedisValue)Format.ToString(endpoint));
diff --git a/src/StackExchange.Redis/ResultProcessor.cs b/src/StackExchange.Redis/ResultProcessor.cs
index 648387b87..d20798e25 100644
--- a/src/StackExchange.Redis/ResultProcessor.cs
+++ b/src/StackExchange.Redis/ResultProcessor.cs
@@ -187,6 +187,12 @@ public static readonly TimeSpanProcessor
public static readonly HashEntryArrayProcessor
HashEntryArray = new HashEntryArrayProcessor();
+ public static readonly ACLUserProcessor
+ ACLUser = new ACLUserProcessor();
+
+ public static readonly KeyValuePairProcessor KeyValuePair = new KeyValuePairProcessor();
+ public static readonly ArrayOfKeyValueArrayProcessor ArrayOfKeyValueArray = new ArrayOfKeyValueArrayProcessor();
+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Conditionally run on instance")]
public void ConnectionFail(Message message, ConnectionFailureType fail, Exception? innerException, string? annotation, ConnectionMultiplexer? muxer)
{
@@ -2887,6 +2893,146 @@ protected override bool SetResultCore(PhysicalConnection connection, Message mes
return false;
}
}
+
+ internal sealed class ArrayOfKeyValueArrayProcessor : ResultProcessor[][]>
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ switch (result.Resp2TypeArray)
+ {
+ case ResultType.Array:
+ var arrayOfArrays = result.GetItems();
+
+ var returnArray = result.ToArray[], KeyValuePairProcessor>(
+ (in RawResult rawInnerArray, in KeyValuePairProcessor proc) =>
+ {
+ if (proc.TryParse(rawInnerArray, out KeyValuePair[]? kvpArray))
+ {
+ return kvpArray!;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(rawInnerArray), $"Error processing {message.CommandAndKey}, could not decode array '{rawInnerArray}'");
+ }
+ },
+ KeyValuePair)!;
+
+ SetResult(message, returnArray);
+ return true;
+ }
+ return false;
+ }
+ }
+ internal sealed class KeyValuePairProcessor : ValuePairInterleavedProcessorBase>
+ {
+ protected override KeyValuePair Parse(in RawResult first, in RawResult second, object? state) =>
+ new KeyValuePair(first.GetString()!, second.AsRedisValue());
+ }
+
+ internal sealed class ACLUserProcessor : ResultProcessor
+ {
+ protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
+ {
+ if (result.IsNull)
+ {
+ SetResult(message, null);
+ return true;
+ }
+ ACLUser? user = null;
+ if (result.Resp2TypeArray == ResultType.Array)
+ {
+ var items = result.GetItems();
+ if (TryParseACLUser(items, out user))
+ {
+ SetResult(message, user);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static bool TryParseACLUser(Sequence result, out ACLUser? user)
+ {
+ var iter = result.GetEnumerator();
+ bool parseResult = false;
+ Dictionary? info = null;
+ string[]? flags = null;
+ string[]? passwords = null;
+ string? commands = null;
+ string? keys = null;
+ string? channels = null;
+ ACLSelector[]? selectors = null;
+ user = null;
+
+ while (iter.MoveNext())
+ {
+ switch (iter.Current.GetString())
+ {
+ case "flags":
+ flags = iter.GetNext().ToArray((in RawResult item) => item.GetString()!)!;
+ parseResult = true;
+ break;
+ case "passwords":
+ passwords = iter.GetNext().ToArray((in RawResult item) => item.GetString()!);
+ parseResult = true;
+ break;
+ case "commands":
+ commands = iter.GetNext().GetString()!;
+ parseResult = true;
+ break;
+ case "keys":
+ keys = iter.GetNext().GetString()!;
+ parseResult = true;
+ break;
+ case "channels":
+ channels = iter.GetNext().GetString()!;
+ parseResult = true;
+ break;
+ case "selectors":
+ selectors = iter.GetNext().ToArray((in RawResult item) => ToACLSelector(item));
+ parseResult = true;
+ break;
+ default:
+ info = info ?? new Dictionary();
+ info.Add(iter.Current.GetString()!, iter.GetNext());
+ parseResult = false;
+ break;
+ }
+ }
+ if (parseResult)
+ {
+ user = new ACLUser(info, flags, passwords, commands, keys, channels, selectors);
+ }
+ return parseResult;
+ }
+
+ private static ACLSelector ToACLSelector(RawResult result)
+ {
+ var iter = result.GetItems().GetEnumerator();
+ string? commands = null;
+ string? keys = null;
+ string? channels = null;
+
+ while (iter.MoveNext())
+ {
+ switch (iter.Current.GetString())
+ {
+ case "commands":
+ commands = iter.GetNext().GetString()!;
+ break;
+ case "keys":
+ keys = iter.GetNext().GetString()!;
+ break;
+ case "channels":
+ channels = iter.GetNext().GetString()!;
+ break;
+ default:
+ break;
+ }
+ }
+ return new ACLSelector(commands, keys, channels);
+ }
+ }
}
internal abstract class ResultProcessor : ResultProcessor
diff --git a/tests/StackExchange.Redis.Tests/ACLIntegrationTests.cs b/tests/StackExchange.Redis.Tests/ACLIntegrationTests.cs
new file mode 100644
index 000000000..c783dab76
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ACLIntegrationTests.cs
@@ -0,0 +1,232 @@
+using System;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace StackExchange.Redis.Tests;
+
+[RunPerProtocol]
+[Collection(SharedConnectionFixture.Key)]
+public class ACLIntegrationTests : TestBase
+{
+ private readonly IConnectionMultiplexer _conn;
+ private readonly IServer _redisServer;
+
+ public ACLIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture)
+ {
+ _conn = Create(require: RedisFeatures.v7_4_0_rc1);
+ _redisServer = GetAnyPrimary(_conn);
+ }
+
+ [Fact]
+ public void AccessControlGetCategories_ShouldReturnCategories()
+ {
+ // Act
+ var categories = _redisServer.AccessControlGetCategories();
+
+ // Assert
+ Assert.NotNull(categories);
+ Assert.Contains("write", categories);
+ Assert.Contains("set", categories);
+ Assert.Contains("list", categories);
+ }
+
+ [Fact]
+ public void AccessControlGetCommands_ShouldReturnCommands()
+ {
+ // Act
+ var commands = _redisServer.AccessControlGetCommands("set");
+
+ // Assert
+ Assert.NotNull(commands);
+ Assert.Contains("sort", commands);
+ Assert.Contains("spop", commands);
+ }
+
+ [Fact]
+ public void AccessControlGetUser_ShouldReturnUserDetails()
+ {
+ Action act = rules => rules.CommandsAllowed("GET", "SET");
+ // Arrange
+ var userName = new RedisValue(Me());
+ _redisServer.AccessControlSetUser(userName, new ACLRulesBuilder()
+ .AppendACLSelectorRules(rules => rules.CommandsAllowed("GET", "SET"))
+ .AppendACLSelectorRules(rules => rules.KeysAllowedReadForPatterns("key*"))
+ .WithACLUserRules(rules => rules.PasswordsToSet("psw1", "psw2"))
+ .WithACLCommandRules(rules => rules.CommandsAllowed("HGET", "HSET")
+ .KeysAllowedPatterns("key1", "key*")
+ .PubSubAllowChannels("chan1", "chan*"))
+ .Build());
+
+ // Act
+ var user = _redisServer.AccessControlGetUser(userName);
+
+ // Assert
+ Assert.NotNull(user);
+ Assert.NotNull(user.Passwords);
+ Assert.True(user.Passwords.Length > 1);
+ Assert.NotNull(user.Selectors);
+ Assert.True(user.Selectors.Length > 1);
+ }
+
+ [Fact]
+ public void AccessControlGetUser_ShouldReturnNullForNonExistentUser()
+ {
+ // Act
+ var user = _redisServer.AccessControlGetUser(Me());
+
+ // Assert
+ Assert.Null(user);
+ }
+
+ [Fact]
+ public void AccessControlDeleteUsers_ShouldReturnCorrectCount()
+ {
+ // Arrange
+ string userName = Me();
+ _redisServer.AccessControlSetUser(new RedisValue(userName), new ACLRulesBuilder().Build());
+
+ // Act
+ var count = _redisServer.AccessControlDeleteUsers(new RedisValue[] { userName, "user2" });
+
+ // Assert
+ Assert.Equal(1, count);
+ }
+
+ [Fact]
+ public void AccessControlGeneratePassword_ShouldReturnGeneratedPassword()
+ {
+ // Act
+ var password = _redisServer.AccessControlGeneratePassword(256);
+
+ // Assert
+ Assert.True(password.HasValue);
+ Assert.True(password.ToString().Length > 0); // Ensure a password is generated
+ }
+
+ [Fact]
+ public void AccessControlLogReset_ShouldExecuteSuccessfully()
+ {
+ // Act
+ _redisServer.AccessControlLogReset();
+
+ // Assert
+ // The action is successful if no exceptions are thrown
+ }
+
+ [Fact]
+ public void AccessControlLog_ShouldReturnLogs()
+ {
+ // Arrange
+ string userName = Me();
+ _redisServer.AccessControlSetUser(
+ userName,
+ new ACLRulesBuilder()
+ .WithACLUserRules(rules => rules.PasswordsToSet(new[] { "pass1" })
+ .UserState(ACLUserState.ON))
+ .Build());
+
+ Assert.Throws(() => _conn.GetDatabase().Execute("AUTH", userName, "pass2"));
+
+ // Act
+ var logs = _redisServer.AccessControlLog(10);
+
+ // Assert
+ Assert.NotNull(logs);
+ Assert.NotEmpty(logs);
+ Assert.Contains(logs[0], x => x.Key == "reason");
+ }
+
+ [Fact]
+ public void AccessControlWhoAmI_ShouldReturnCurrentUser()
+ {
+ // Act
+ var user = _redisServer.AccessControlWhoAmI();
+
+ // Assert
+ Assert.True(user.HasValue);
+ Assert.True(user.ToString().Length > 0); // Ensure there's a valid user returned
+ }
+
+ [Fact]
+ public void AccessControlList_ShouldReturnAllUsers()
+ {
+ // Arrange
+ var userName1 = new RedisValue(Me() + "1");
+ var userName2 = new RedisValue(Me() + "2");
+ _redisServer.AccessControlSetUser(userName1, new ACLRulesBuilder().Build());
+ _redisServer.AccessControlSetUser(userName2, new ACLRulesBuilder().Build());
+
+ // Act
+ var users = _redisServer.AccessControlList();
+
+ // Assert
+ Assert.NotNull(users);
+ Assert.Contains(users, user => user.ToString().Contains(userName1!));
+ Assert.Contains(users, user => user.ToString().Contains(userName2!));
+ }
+
+ [Fact]
+ public void AccessControlSetUser_ShouldSetUserWithGivenRules()
+ {
+ string userName = Me();
+
+ // Act
+ _redisServer.AccessControlSetUser(new RedisValue(userName), new ACLRules(null, null, null));
+
+ // Assert
+ var users = _redisServer.AccessControlList();
+ Assert.NotNull(users);
+ Assert.Contains(users!, user => user.ToString().Contains(userName));
+ }
+
+ [Fact]
+ public void AccessControlSetUser_ShouldSetUserWithMultipleRules()
+ {
+ // Arrange
+ var userName = new RedisValue(Me());
+ var rules = new ACLRulesBuilder()
+ .AppendACLSelectorRules(r => r.CommandsAllowed("HMGET", "HMSET").KeysAllowedReadForPatterns("key*"))
+ .WithACLUserRules(r => r.PasswordsToSet("password1", "password2"))
+ .WithACLCommandRules(r => r.CommandsAllowed("HGET", "HSET")
+ .KeysAllowedPatterns("key1", "key*")
+ .PubSubAllowChannels("chan1", "chan*"))
+ .Build();
+
+ // Act
+ _redisServer.AccessControlSetUser(userName, rules);
+
+ // Assert
+ var user = _redisServer.AccessControlGetUser(userName);
+ Assert.NotNull(user);
+ Assert.Contains(user.Selectors!, s => s.Commands!.Contains("hmget") && s.Commands!.Contains("hmset"));
+ Assert.Contains(user.Selectors!, s => s.Keys!.Contains("key*"));
+ }
+
+ [Fact]
+ public void AccessControlSetUser_ShouldUpdateExistingUser()
+ {
+ // Arrange
+ var userName = new RedisValue(Me());
+ var hashedPassword1 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+ var hashedPassword2 = "1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+ var updatedPassword = "2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
+ var initialRules = new ACLRulesBuilder()
+ .WithACLUserRules(r => r.HashedPasswordsToSet(hashedPassword1, hashedPassword2))
+ .Build();
+ _redisServer.AccessControlSetUser(userName, initialRules);
+
+ var updatedRules = new ACLRulesBuilder()
+ .WithACLUserRules(r => r.HashedPasswordsToSet(updatedPassword).HashedPasswordsToRemove(hashedPassword1))
+ .Build();
+
+ // Act
+ _redisServer.AccessControlSetUser(userName, updatedRules);
+
+ // Assert
+ var user = _redisServer.AccessControlGetUser(userName);
+ Assert.NotNull(user);
+ Assert.DoesNotContain(hashedPassword1, user.Passwords!);
+ Assert.Contains(hashedPassword2, user.Passwords!);
+ Assert.Contains(updatedPassword, user.Passwords!);
+ }
+}
diff --git a/tests/StackExchange.Redis.Tests/ACLTests.cs b/tests/StackExchange.Redis.Tests/ACLTests.cs
new file mode 100644
index 000000000..68cc0663f
--- /dev/null
+++ b/tests/StackExchange.Redis.Tests/ACLTests.cs
@@ -0,0 +1,407 @@
+using System.Collections.Generic;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace StackExchange.Redis.Tests;
+
+[Collection(SharedConnectionFixture.Key)]
+public class ACLTests : TestBase
+{
+ public ACLTests(ITestOutputHelper output) : base(output) { }
+
+ protected override string GetConfiguration() => TestConfig.Current.PrimaryServerAndPort;
+
+ [Fact]
+ public void ToRedisValues_ShouldReturnCorrectValues_WhenAllFieldsAreSet()
+ {
+ // Arrange
+ var aclUserRules = new ACLUserRules(
+ resetUser: true,
+ noPass: true,
+ resetPass: false,
+ userState: ACLUserState.ON,
+ passwordsToSet: new[] { "password1", "password2" },
+ passwordsToRemove: new[] { "password3" },
+ hashedPasswordsToSet: new[] { "hashed1", "hashed2" },
+ hashedPasswordsToRemove: new[] { "hashed3" },
+ clearSelectors: true);
+
+ var aclCommandRules = new ACLCommandRules(
+ commandsRule: ACLCommandsRule.NOCOMMANDS,
+ commandsAllowed: new[] { "GET", "SET" },
+ commandsDisallowed: new[] { "DEL" },
+ categoriesAllowed: new[] { "string", "list" },
+ categoriesDisallowed: new[] { "set", "hash" },
+ keysRule: ACLKeysRule.ALLKEYS,
+ keysAllowedPatterns: new[] { "user:*", "session:*" },
+ keysAllowedReadForPatterns: new[] { "user:*" },
+ keysAllowedWriteForPatterns: new[] { "session:*" },
+ pubSubRule: ACLPubSubRule.ALLCHANNELS,
+ pubSubAllowChannels: new[] { "channel1", "channel2" });
+
+ var aclSelectorRules = new[]
+ {
+ new ACLSelectorRules(
+ commandsAllowed: new[] { "GET", },
+ commandsDisallowed: new[] { "SET" },
+ categoriesAllowed: new[] { "string" },
+ categoriesDisallowed: new[] { "list" },
+ keysAllowedPatterns: new[] { "user:*" },
+ keysAllowedReadForPatterns: new[] { "session:*" },
+ keysAllowedWriteForPatterns: new[] { "user:*" }),
+ };
+
+ var aclRules = new ACLRules(aclUserRules, aclCommandRules, aclSelectorRules);
+
+ // Act
+ var redisValues = aclRules.ToRedisValues();
+
+ // Assert
+ var expectedValues = new List
+ {
+ RedisLiterals.RESET,
+ RedisLiterals.NOPASS,
+ "ON",
+ ">password1",
+ ">password2",
+ "
+ {
+ "ALLCOMMANDS",
+ "ALLKEYS",
+ "ALLCHANNELS",
+ };
+
+ Assert.Equal(expectedValues, redisValues);
+ }
+
+ [Fact]
+ public void ToRedisValues_NoCommandsNoKeysResetChannels()
+ {
+ // Arrange
+ var aclUserRules = new ACLUserRules(
+ resetUser: false,
+ noPass: false,
+ resetPass: false,
+ userState: null,
+ passwordsToSet: null,
+ passwordsToRemove: null,
+ hashedPasswordsToSet: null,
+ hashedPasswordsToRemove: null,
+ clearSelectors: false);
+
+ var aclCommandRules = new ACLCommandRules(
+ commandsRule: ACLCommandsRule.NOCOMMANDS,
+ commandsAllowed: null,
+ commandsDisallowed: null,
+ categoriesAllowed: null,
+ categoriesDisallowed: null,
+ keysRule: ACLKeysRule.RESETKEYS,
+ keysAllowedPatterns: null,
+ keysAllowedReadForPatterns: null,
+ keysAllowedWriteForPatterns: null,
+ pubSubRule: ACLPubSubRule.RESETCHANNELS,
+ pubSubAllowChannels: null);
+
+ // Act
+ var aclRules = new ACLRules(aclUserRules, aclCommandRules, null);
+ var redisValues = aclRules.ToRedisValues();
+
+ // Assert
+ var expectedValues = new List
+ {
+ "NOCOMMANDS",
+ "RESETKEYS",
+ "RESETCHANNELS",
+ };
+
+ Assert.Equal(expectedValues, redisValues);
+ }
+
+ [Fact]
+ public void Build_ShouldCreateACLRulesWithUserRules_WhenUserRulesAreSet()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ builder.WithACLUserRules(userBuilder => userBuilder
+ .ResetUser(true)
+ .NoPass(true)
+ .UserState(ACLUserState.ON)
+ .PasswordsToSet("password123")
+ .ClearSelectors(true));
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ Assert.NotNull(aclRules.AclUserRules);
+ Assert.True(aclRules.AclUserRules?.ResetUser);
+ Assert.True(aclRules.AclUserRules?.NoPass);
+ Assert.Equal(ACLUserState.ON, aclRules.AclUserRules?.UserState);
+ Assert.Contains(">password123", aclRules.ToRedisValues());
+ Assert.Contains(RedisLiterals.CLEARSELECTORS, aclRules.ToRedisValues());
+ }
+
+ [Fact]
+ public void Build_ShouldCreateACLRulesWithCommandRules_WhenCommandRulesAreSet()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ builder.WithACLCommandRules(commandBuilder => commandBuilder
+ .CommandsRule(ACLCommandsRule.ALLCOMMANDS)
+ .CommandsAllowed("GET", "SET")
+ .CommandsDisallowed("DEL")
+ .CategoriesAllowed("string")
+ .KeysRule(ACLKeysRule.ALLKEYS)
+ .KeysAllowedPatterns("user:*", "session:*"));
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ Assert.NotNull(aclRules.AclCommandRules);
+ Assert.Equal(ACLCommandsRule.ALLCOMMANDS, aclRules.AclCommandRules?.CommandsRule);
+ Assert.Contains("+GET", aclRules.ToRedisValues());
+ Assert.Contains("-DEL", aclRules.ToRedisValues());
+ Assert.Contains("+@string", aclRules.ToRedisValues());
+ Assert.Contains("~user:*", aclRules.ToRedisValues());
+ }
+
+ [Fact]
+ public void Build_ShouldCreateACLRulesWithSelectorRules_WhenSelectorRulesAreSet()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ builder.AppendACLSelectorRules(selectorBuilder => selectorBuilder
+ .CommandsAllowed("GET")
+ .CommandsDisallowed("SET")
+ .CategoriesAllowed("list")
+ .KeysAllowedPatterns("session:*"));
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ Assert.NotNull(aclRules.AclSelectorRules);
+ Assert.Single(aclRules.AclSelectorRules);
+ Assert.Contains("+GET", aclRules.ToRedisValues());
+ Assert.Contains("-SET", aclRules.ToRedisValues());
+ Assert.Contains("+@list", aclRules.ToRedisValues());
+ Assert.Contains("~session:*", aclRules.ToRedisValues());
+ }
+
+ [Fact]
+ public void Build_ShouldCreateACLRulesWithAllComponents_WhenAllRulesAreSet()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ builder.WithACLUserRules(userBuilder => userBuilder
+ .ResetUser(true)
+ .NoPass(true)
+ .UserState(ACLUserState.OFF)
+ .PasswordsToSet("newpassword")
+ .ClearSelectors(true));
+
+ builder.WithACLCommandRules(commandBuilder => commandBuilder
+ .CommandsRule(ACLCommandsRule.NOCOMMANDS)
+ .CommandsAllowed("GET", "SET")
+ .CategoriesAllowed("list")
+ .KeysRule(ACLKeysRule.RESETKEYS)
+ .KeysAllowedPatterns("user:*"));
+
+ builder.AppendACLSelectorRules(selectorBuilder => selectorBuilder
+ .CommandsDisallowed("DEL")
+ .CategoriesDisallowed("hash")
+ .KeysAllowedPatterns("session:*"));
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ // Verify ACLUserRules
+ Assert.True(aclRules.AclUserRules?.ResetUser);
+ Assert.True(aclRules.AclUserRules?.NoPass);
+ Assert.Equal(ACLUserState.OFF, aclRules.AclUserRules?.UserState);
+ Assert.Contains(">newpassword", aclRules.ToRedisValues());
+ Assert.Contains(RedisLiterals.CLEARSELECTORS, aclRules.ToRedisValues());
+
+ // Verify ACLCommandRules
+ Assert.Equal(ACLCommandsRule.NOCOMMANDS, aclRules.AclCommandRules?.CommandsRule);
+ Assert.Contains("+GET", aclRules.ToRedisValues());
+ Assert.Contains("+SET", aclRules.ToRedisValues());
+ Assert.Contains("+@list", aclRules.ToRedisValues());
+ Assert.Contains("~user:*", aclRules.ToRedisValues());
+
+ // Verify ACLSelectorRules
+ Assert.NotNull(aclRules.AclSelectorRules);
+ Assert.Single(aclRules.AclSelectorRules);
+ Assert.Contains("-DEL", aclRules.ToRedisValues());
+ Assert.Contains("-@hash", aclRules.ToRedisValues());
+ Assert.Contains("~session:*", aclRules.ToRedisValues());
+ }
+
+ [Fact]
+ public void Build_ShouldHandleEmptyInput_WhenNoRulesAreSet()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ Assert.Null(aclRules.AclUserRules);
+ Assert.Null(aclRules.AclCommandRules);
+ Assert.Null(aclRules.AclSelectorRules);
+ Assert.Empty(aclRules.ToRedisValues());
+ }
+
+ [Fact]
+ public void Build_ShouldCreateACLRulesWithMultipleSelectorRules_WhenMultipleAreAppended()
+ {
+ // Arrange
+ var builder = new ACLRulesBuilder();
+
+ builder.AppendACLSelectorRules(selectorBuilder => selectorBuilder
+ .CommandsAllowed("GET")
+ .CategoriesAllowed("string"));
+
+ builder.AppendACLSelectorRules(selectorBuilder => selectorBuilder
+ .CommandsDisallowed("SET")
+ .CategoriesDisallowed("list"));
+
+ // Act
+ var aclRules = builder.Build();
+
+ // Assert
+ Assert.NotNull(aclRules.AclSelectorRules);
+ Assert.Equal(2, aclRules.AclSelectorRules.Length);
+
+ Assert.Contains("+GET", aclRules.ToRedisValues());
+ Assert.Contains("+@string", aclRules.ToRedisValues());
+
+ Assert.Contains("-SET", aclRules.ToRedisValues());
+ Assert.Contains("-@list", aclRules.ToRedisValues());
+ }
+}