Skip to content

Commit

Permalink
adding support for ACL GETUSER
Browse files Browse the repository at this point in the history
  • Loading branch information
atakavci committed Feb 3, 2025
1 parent 8fc0309 commit e6de273
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 3 deletions.
119 changes: 119 additions & 0 deletions src/StackExchange.Redis/APITypes/ACLUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Collections.Generic;

namespace StackExchange.Redis;

/// <summary>
/// Represents an Access Control List (ACL) user with various properties such as flags, passwords, commands, keys, channels, and selectors.
/// </summary>
public class ACLUser
{
/// <summary>
/// A dictionary containing user information.
/// </summary>
public readonly Dictionary<string, object>? UserInfo;

/// <summary>
/// An array of flags associated with the user.
/// </summary>
public readonly string[]? Flags;

/// <summary>
/// An array of passwords associated with the user.
/// </summary>
public readonly string[]? Passwords;

/// <summary>
/// A string representing the commands associated with the user.
/// </summary>
public readonly string? Commands;

/// <summary>
/// A string representing the keys associated with the user.
/// </summary>
public readonly string? Keys;

/// <summary>
/// A string representing the channels associated with the user.
/// </summary>
public readonly string? Channels;

/// <summary>
/// An array of selectors associated with the user.
/// </summary>
public readonly ACLSelector[]? Selectors;

/// <summary>
/// Initializes a new instance of the <see cref="ACLUser"/> class with specified parameters.
/// </summary>
/// <param name="userInfo">A dictionary containing user information.</param>
/// <param name="flags">An array oflags associated with the user.</param>
/// <param name="passwords">An array opasswords associated with the user.</param>
/// <param name="commands">A string representing the commands associated with the user.</param>
/// <param name="keys">A string representing the keys associated with the user.</param>
/// <param name="channels">A string representing the channels associated with the user.</param>
/// <param name="selectors">An array oselectors associated with the user.</param>
public ACLUser(Dictionary<string, object>? 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;
}

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return "AccessControlUser{" + "Flags=" + Flags + ", Passwords=" + Passwords
+ ", Commands='" + Commands + "', Keys='" + Keys + "', Channels='" + Channels
+ "', Selectors=" + Selectors + "}";
}
}

/// <summary>
/// Represents an Access Control List (ACL) selector for a Redis user.
/// </summary>
public class ACLSelector
{
/// <summary>
/// Gets the commands associated with the ACL user.
/// </summary>
public readonly string? Commmands;

/// <summary>
/// Gets the keys associated with the ACL user.
/// </summary>
public readonly string? Keys;

/// <summary>
/// Gets the channels associated with the ACL user.
/// </summary>
public readonly string? Channels;

/// <summary>
/// Initializes a new instance of the <see cref="ACLSelector"/> class.
/// </summary>
/// <param name="commands">The commands associated with the ACLSelector.</param>
/// <param name="keys">The keys associated with the ACLSelector.</param>
/// <param name="channels">The channels associated with the ACLSelector.</param>
public ACLSelector(string? commands, string? keys, string? channels)
{
Commmands = commands;
Keys = keys;
Channels = channels;
}

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return "ACLSelector{" + "Commmands='" + Commmands + "', Keys='" + Keys + "', Channels='" + Channels + "'}";
}
}
15 changes: 15 additions & 0 deletions src/StackExchange.Redis/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,5 +342,20 @@ internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan<byte> span)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static TTo[]? ToArray<TTo, TState>(in this RawResult result, Projection<RawResult, TState, TTo> selector, in TState state)
=> result.IsNull ? null : result.GetItems().ToArray(selector, in state);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static List<T>? ToList<T>(this RawResult result, Func<RawResult, T> selector)
{
List<T>? list = null;
if (!result.IsNull)
{
list = new List<T>();
foreach (var item in result.GetItems())
{
list.Add(selector(item));
}
}
return list;
}
}
}
16 changes: 16 additions & 0 deletions src/StackExchange.Redis/Interfaces/IServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,22 @@ public partial interface IServer : IRedis
/// <returns>A task representing the asynchronous operation, with the generated password as a Redis value.</returns>
Task<RedisValue> AccessControlGeneratePasswordAsync(long bits, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Gets the access control user for a specified username.
/// </summary>
/// <param name="username">The username to get the access control user for.</param>
/// <param name="flags">The command flags to use.</param>
/// <returns>The access control user associated with the specified username, or null if not found.</returns>
ACLUser? AccessControlGetUser(RedisValue username, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Gets the access control user for a specified username.
/// </summary>
/// <param name="username">The username to get the access control user for.</param>
/// <param name="flags">The command flags to use.</param>
/// <returns>A task representing the asynchronous operation, with the access control user associated with the specified username, or null if not found.</returns>
Task<ACLUser?> AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Loads access control rules.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -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!
Expand Down Expand Up @@ -76,6 +78,16 @@ 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.Commmands -> 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<string!, object!>?
readonly StackExchange.Redis.ACLUserRules.ClearSelectors -> bool
readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToRemove -> string![]?
readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToSet -> string![]?
Expand Down Expand Up @@ -118,6 +130,8 @@ StackExchange.Redis.ACLRulesBuilder.AppendACLSelectorRules(System.Action<StackEx
StackExchange.Redis.ACLRulesBuilder.Build() -> StackExchange.Redis.ACLRules!
StackExchange.Redis.ACLRulesBuilder.WithACLCommandRules(System.Action<StackExchange.Redis.ACLCommandRulesBuilder!>! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
StackExchange.Redis.ACLRulesBuilder.WithACLUserRules(System.Action<StackExchange.Redis.ACLUserRulesBuilder!>! 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
Expand All @@ -130,6 +144,8 @@ StackExchange.Redis.ACLSelectorRulesBuilder.CommandsDisallowed(params string![]!
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<string!, object!>? 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
Expand Down Expand Up @@ -1135,6 +1151,8 @@ StackExchange.Redis.IServer.AccessControlGetCategories(StackExchange.Redis.Comma
StackExchange.Redis.IServer.AccessControlGetCategoriesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue[]!>!
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.RedisValue[]!>!
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.ACLUser?>!
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<long>!
StackExchange.Redis.IServer.AccessControlGeneratePassword(long bits, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
Expand Down
1 change: 1 addition & 0 deletions src/StackExchange.Redis/RedisLiterals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public static readonly RedisValue
GET = "GET",
GETKEYS = "GETKEYS",
GETNAME = "GETNAME",
GETUSER = "GETUSER",
GT = "GT",
HISTORY = "HISTORY",
ID = "ID",
Expand Down
12 changes: 12 additions & 0 deletions src/StackExchange.Redis/RedisServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ public Task<RedisValue> AccessControlGeneratePasswordAsync(long bits, CommandFla
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<ACLUser?> AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GETUSER, username);
return ExecuteAsync(msg, ResultProcessor.ACLUser);
}

public void AccessControlLoad(CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOAD);
Expand Down
103 changes: 103 additions & 0 deletions src/StackExchange.Redis/ResultProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ 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();

Expand Down Expand Up @@ -2925,6 +2928,106 @@ internal sealed class KeyValuePairProcessor : ValuePairInterleavedProcessorBase<
protected override KeyValuePair<string, RedisValue> Parse(in RawResult first, in RawResult second, object? state) =>
new KeyValuePair<string, RedisValue>(first.GetString()!, second.AsRedisValue());
}

internal sealed class ACLUserProcessor : ResultProcessor<ACLUser?>
{
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
{
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<RawResult> result, out ACLUser? user)
{
var iter = result.GetEnumerator();
bool parseResult = false;
Dictionary<string, object>? 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<string, object>();
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<T> : ResultProcessor
Expand Down
Loading

0 comments on commit e6de273

Please sign in to comment.