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()); + } +}