Skip to content

Commit e6de273

Browse files
committed
adding support for ACL GETUSER
1 parent 8fc0309 commit e6de273

File tree

8 files changed

+321
-3
lines changed

8 files changed

+321
-3
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System.Collections.Generic;
2+
3+
namespace StackExchange.Redis;
4+
5+
/// <summary>
6+
/// Represents an Access Control List (ACL) user with various properties such as flags, passwords, commands, keys, channels, and selectors.
7+
/// </summary>
8+
public class ACLUser
9+
{
10+
/// <summary>
11+
/// A dictionary containing user information.
12+
/// </summary>
13+
public readonly Dictionary<string, object>? UserInfo;
14+
15+
/// <summary>
16+
/// An array of flags associated with the user.
17+
/// </summary>
18+
public readonly string[]? Flags;
19+
20+
/// <summary>
21+
/// An array of passwords associated with the user.
22+
/// </summary>
23+
public readonly string[]? Passwords;
24+
25+
/// <summary>
26+
/// A string representing the commands associated with the user.
27+
/// </summary>
28+
public readonly string? Commands;
29+
30+
/// <summary>
31+
/// A string representing the keys associated with the user.
32+
/// </summary>
33+
public readonly string? Keys;
34+
35+
/// <summary>
36+
/// A string representing the channels associated with the user.
37+
/// </summary>
38+
public readonly string? Channels;
39+
40+
/// <summary>
41+
/// An array of selectors associated with the user.
42+
/// </summary>
43+
public readonly ACLSelector[]? Selectors;
44+
45+
/// <summary>
46+
/// Initializes a new instance of the <see cref="ACLUser"/> class with specified parameters.
47+
/// </summary>
48+
/// <param name="userInfo">A dictionary containing user information.</param>
49+
/// <param name="flags">An array oflags associated with the user.</param>
50+
/// <param name="passwords">An array opasswords associated with the user.</param>
51+
/// <param name="commands">A string representing the commands associated with the user.</param>
52+
/// <param name="keys">A string representing the keys associated with the user.</param>
53+
/// <param name="channels">A string representing the channels associated with the user.</param>
54+
/// <param name="selectors">An array oselectors associated with the user.</param>
55+
public ACLUser(Dictionary<string, object>? userInfo, string[]? flags, string[]? passwords, string? commands, string? keys, string? channels, ACLSelector[]? selectors)
56+
{
57+
UserInfo = userInfo;
58+
Flags = flags;
59+
Passwords = passwords;
60+
Commands = commands;
61+
Keys = keys;
62+
Channels = channels;
63+
Selectors = selectors;
64+
}
65+
66+
/// <summary>
67+
/// Returns a string that represents the current object.
68+
/// </summary>
69+
/// <returns>A string that represents the current object.</returns>
70+
public override string ToString()
71+
{
72+
return "AccessControlUser{" + "Flags=" + Flags + ", Passwords=" + Passwords
73+
+ ", Commands='" + Commands + "', Keys='" + Keys + "', Channels='" + Channels
74+
+ "', Selectors=" + Selectors + "}";
75+
}
76+
}
77+
78+
/// <summary>
79+
/// Represents an Access Control List (ACL) selector for a Redis user.
80+
/// </summary>
81+
public class ACLSelector
82+
{
83+
/// <summary>
84+
/// Gets the commands associated with the ACL user.
85+
/// </summary>
86+
public readonly string? Commmands;
87+
88+
/// <summary>
89+
/// Gets the keys associated with the ACL user.
90+
/// </summary>
91+
public readonly string? Keys;
92+
93+
/// <summary>
94+
/// Gets the channels associated with the ACL user.
95+
/// </summary>
96+
public readonly string? Channels;
97+
98+
/// <summary>
99+
/// Initializes a new instance of the <see cref="ACLSelector"/> class.
100+
/// </summary>
101+
/// <param name="commands">The commands associated with the ACLSelector.</param>
102+
/// <param name="keys">The keys associated with the ACLSelector.</param>
103+
/// <param name="channels">The channels associated with the ACLSelector.</param>
104+
public ACLSelector(string? commands, string? keys, string? channels)
105+
{
106+
Commmands = commands;
107+
Keys = keys;
108+
Channels = channels;
109+
}
110+
111+
/// <summary>
112+
/// Returns a string that represents the current object.
113+
/// </summary>
114+
/// <returns>A string that represents the current object.</returns>
115+
public override string ToString()
116+
{
117+
return "ACLSelector{" + "Commmands='" + Commmands + "', Keys='" + Keys + "', Channels='" + Channels + "'}";
118+
}
119+
}

src/StackExchange.Redis/ExtensionMethods.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,20 @@ internal static int VectorSafeIndexOfCRLF(this ReadOnlySpan<byte> span)
342342
[MethodImpl(MethodImplOptions.AggressiveInlining)]
343343
internal static TTo[]? ToArray<TTo, TState>(in this RawResult result, Projection<RawResult, TState, TTo> selector, in TState state)
344344
=> result.IsNull ? null : result.GetItems().ToArray(selector, in state);
345+
346+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
347+
internal static List<T>? ToList<T>(this RawResult result, Func<RawResult, T> selector)
348+
{
349+
List<T>? list = null;
350+
if (!result.IsNull)
351+
{
352+
list = new List<T>();
353+
foreach (var item in result.GetItems())
354+
{
355+
list.Add(selector(item));
356+
}
357+
}
358+
return list;
359+
}
345360
}
346361
}

src/StackExchange.Redis/Interfaces/IServer.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,22 @@ public partial interface IServer : IRedis
139139
/// <returns>A task representing the asynchronous operation, with the generated password as a Redis value.</returns>
140140
Task<RedisValue> AccessControlGeneratePasswordAsync(long bits, CommandFlags flags = CommandFlags.None);
141141

142+
/// <summary>
143+
/// Gets the access control user for a specified username.
144+
/// </summary>
145+
/// <param name="username">The username to get the access control user for.</param>
146+
/// <param name="flags">The command flags to use.</param>
147+
/// <returns>The access control user associated with the specified username, or null if not found.</returns>
148+
ACLUser? AccessControlGetUser(RedisValue username, CommandFlags flags = CommandFlags.None);
149+
150+
/// <summary>
151+
/// Gets the access control user for a specified username.
152+
/// </summary>
153+
/// <param name="username">The username to get the access control user for.</param>
154+
/// <param name="flags">The command flags to use.</param>
155+
/// <returns>A task representing the asynchronous operation, with the access control user associated with the specified username, or null if not found.</returns>
156+
Task<ACLUser?> AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None);
157+
142158
/// <summary>
143159
/// Loads access control rules.
144160
/// </summary>

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#nullable enable
22
abstract StackExchange.Redis.RedisResult.IsNull.get -> bool
3+
override StackExchange.Redis.ACLSelector.ToString() -> string!
4+
override StackExchange.Redis.ACLUser.ToString() -> string!
35
override StackExchange.Redis.ChannelMessage.Equals(object? obj) -> bool
46
override StackExchange.Redis.ChannelMessage.GetHashCode() -> int
57
override StackExchange.Redis.ChannelMessage.ToString() -> string!
@@ -76,6 +78,16 @@ readonly StackExchange.Redis.ACLSelectorRules.CommandsDisallowed -> string![]?
7678
readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedPatterns -> string![]?
7779
readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedReadForPatterns -> string![]?
7880
readonly StackExchange.Redis.ACLSelectorRules.KeysAllowedWriteForPatterns -> string![]?
81+
readonly StackExchange.Redis.ACLSelector.Channels -> string?
82+
readonly StackExchange.Redis.ACLSelector.Commmands -> string?
83+
readonly StackExchange.Redis.ACLSelector.Keys -> string?
84+
readonly StackExchange.Redis.ACLUser.Channels -> string?
85+
readonly StackExchange.Redis.ACLUser.Commands -> string?
86+
readonly StackExchange.Redis.ACLUser.Flags -> string![]?
87+
readonly StackExchange.Redis.ACLUser.Keys -> string?
88+
readonly StackExchange.Redis.ACLUser.Passwords -> string![]?
89+
readonly StackExchange.Redis.ACLUser.Selectors -> StackExchange.Redis.ACLSelector![]?
90+
readonly StackExchange.Redis.ACLUser.UserInfo -> System.Collections.Generic.Dictionary<string!, object!>?
7991
readonly StackExchange.Redis.ACLUserRules.ClearSelectors -> bool
8092
readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToRemove -> string![]?
8193
readonly StackExchange.Redis.ACLUserRules.HashedPasswordsToSet -> string![]?
@@ -118,6 +130,8 @@ StackExchange.Redis.ACLRulesBuilder.AppendACLSelectorRules(System.Action<StackEx
118130
StackExchange.Redis.ACLRulesBuilder.Build() -> StackExchange.Redis.ACLRules!
119131
StackExchange.Redis.ACLRulesBuilder.WithACLCommandRules(System.Action<StackExchange.Redis.ACLCommandRulesBuilder!>! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
120132
StackExchange.Redis.ACLRulesBuilder.WithACLUserRules(System.Action<StackExchange.Redis.ACLUserRulesBuilder!>! buildAction) -> StackExchange.Redis.ACLRulesBuilder!
133+
StackExchange.Redis.ACLSelector
134+
StackExchange.Redis.ACLSelector.ACLSelector(string? commands, string? keys, string? channels) -> void
121135
StackExchange.Redis.ACLSelectorRules
122136
StackExchange.Redis.ACLSelectorRules.ACLSelectorRules(string![]? commandsAllowed, string![]? commandsDisallowed, string![]? categoriesAllowed, string![]? categoriesDisallowed, string![]? keysAllowedPatterns, string![]? keysAllowedReadForPatterns, string![]? keysAllowedWriteForPatterns) -> void
123137
StackExchange.Redis.ACLSelectorRulesBuilder
@@ -130,6 +144,8 @@ StackExchange.Redis.ACLSelectorRulesBuilder.CommandsDisallowed(params string![]!
130144
StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
131145
StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedReadForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
132146
StackExchange.Redis.ACLSelectorRulesBuilder.KeysAllowedWriteForPatterns(params string![]! patterns) -> StackExchange.Redis.ACLSelectorRulesBuilder!
147+
StackExchange.Redis.ACLUser
148+
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
133149
StackExchange.Redis.ACLUserRules
134150
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
135151
StackExchange.Redis.ACLUserRulesBuilder
@@ -1135,6 +1151,8 @@ StackExchange.Redis.IServer.AccessControlGetCategories(StackExchange.Redis.Comma
11351151
StackExchange.Redis.IServer.AccessControlGetCategoriesAsync(StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue[]!>!
11361152
StackExchange.Redis.IServer.AccessControlGetCommands(StackExchange.Redis.RedisValue category, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue[]!
11371153
StackExchange.Redis.IServer.AccessControlGetCommandsAsync(StackExchange.Redis.RedisValue category, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue[]!>!
1154+
StackExchange.Redis.IServer.AccessControlGetUser(StackExchange.Redis.RedisValue username, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.ACLUser?
1155+
StackExchange.Redis.IServer.AccessControlGetUserAsync(StackExchange.Redis.RedisValue username, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.ACLUser?>!
11381156
StackExchange.Redis.IServer.AccessControlDeleteUsers(StackExchange.Redis.RedisValue[]! usernames, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> long
11391157
StackExchange.Redis.IServer.AccessControlDeleteUsersAsync(StackExchange.Redis.RedisValue[]! usernames, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<long>!
11401158
StackExchange.Redis.IServer.AccessControlGeneratePassword(long bits, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue

src/StackExchange.Redis/RedisLiterals.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public static readonly RedisValue
8989
GET = "GET",
9090
GETKEYS = "GETKEYS",
9191
GETNAME = "GETNAME",
92+
GETUSER = "GETUSER",
9293
GT = "GT",
9394
HISTORY = "HISTORY",
9495
ID = "ID",

src/StackExchange.Redis/RedisServer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ public Task<RedisValue> AccessControlGeneratePasswordAsync(long bits, CommandFla
9999
return ExecuteAsync(msg, ResultProcessor.RedisValue);
100100
}
101101

102+
public ACLUser? AccessControlGetUser(RedisValue username, CommandFlags flags = CommandFlags.None)
103+
{
104+
var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GETUSER, username);
105+
return ExecuteSync(msg, ResultProcessor.ACLUser);
106+
}
107+
108+
public Task<ACLUser?> AccessControlGetUserAsync(RedisValue username, CommandFlags flags = CommandFlags.None)
109+
{
110+
var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.GETUSER, username);
111+
return ExecuteAsync(msg, ResultProcessor.ACLUser);
112+
}
113+
102114
public void AccessControlLoad(CommandFlags flags = CommandFlags.None)
103115
{
104116
var msg = Message.Create(-1, flags, RedisCommand.ACL, RedisLiterals.LOAD);

src/StackExchange.Redis/ResultProcessor.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ public static readonly TimeSpanProcessor
187187
public static readonly HashEntryArrayProcessor
188188
HashEntryArray = new HashEntryArrayProcessor();
189189

190+
public static readonly ACLUserProcessor
191+
ACLUser = new ACLUserProcessor();
192+
190193
public static readonly KeyValuePairProcessor KeyValuePair = new KeyValuePairProcessor();
191194
public static readonly ArrayOfKeyValueArrayProcessor ArrayOfKeyValueArray = new ArrayOfKeyValueArrayProcessor();
192195

@@ -2925,6 +2928,106 @@ internal sealed class KeyValuePairProcessor : ValuePairInterleavedProcessorBase<
29252928
protected override KeyValuePair<string, RedisValue> Parse(in RawResult first, in RawResult second, object? state) =>
29262929
new KeyValuePair<string, RedisValue>(first.GetString()!, second.AsRedisValue());
29272930
}
2931+
2932+
internal sealed class ACLUserProcessor : ResultProcessor<ACLUser?>
2933+
{
2934+
protected override bool SetResultCore(PhysicalConnection connection, Message message, in RawResult result)
2935+
{
2936+
ACLUser? user = null;
2937+
if (result.Resp2TypeArray == ResultType.Array)
2938+
{
2939+
var items = result.GetItems();
2940+
if (TryParseACLUser(items, out user))
2941+
{
2942+
SetResult(message, user);
2943+
return true;
2944+
}
2945+
}
2946+
return false;
2947+
}
2948+
2949+
private static bool TryParseACLUser(Sequence<RawResult> result, out ACLUser? user)
2950+
{
2951+
var iter = result.GetEnumerator();
2952+
bool parseResult = false;
2953+
Dictionary<string, object>? info = null;
2954+
string[]? flags = null;
2955+
string[]? passwords = null;
2956+
string? commands = null;
2957+
string? keys = null;
2958+
string? channels = null;
2959+
ACLSelector[]? selectors = null;
2960+
user = null;
2961+
2962+
while (iter.MoveNext())
2963+
{
2964+
switch (iter.Current.GetString())
2965+
{
2966+
case "flags":
2967+
flags = iter.GetNext().ToArray((in RawResult item) => item.GetString()!)!;
2968+
parseResult = true;
2969+
break;
2970+
case "passwords":
2971+
passwords = iter.GetNext().ToArray((in RawResult item) => item.GetString()!);
2972+
parseResult = true;
2973+
break;
2974+
case "commands":
2975+
commands = iter.GetNext().GetString()!;
2976+
parseResult = true;
2977+
break;
2978+
case "keys":
2979+
keys = iter.GetNext().GetString()!;
2980+
parseResult = true;
2981+
break;
2982+
case "channels":
2983+
channels = iter.GetNext().GetString()!;
2984+
parseResult = true;
2985+
break;
2986+
case "selectors":
2987+
selectors = iter.GetNext().ToArray((in RawResult item) => ToACLSelector(item));
2988+
parseResult = true;
2989+
break;
2990+
default:
2991+
info = info ?? new Dictionary<string, object>();
2992+
info.Add(iter.Current.GetString()!, iter.GetNext());
2993+
parseResult = false;
2994+
break;
2995+
}
2996+
}
2997+
if (parseResult)
2998+
{
2999+
user = new ACLUser(info, flags, passwords, commands, keys, channels, selectors);
3000+
}
3001+
return parseResult;
3002+
}
3003+
3004+
private static ACLSelector ToACLSelector(RawResult result)
3005+
{
3006+
var iter = result.GetItems().GetEnumerator();
3007+
string? commands = null;
3008+
string? keys = null;
3009+
string? channels = null;
3010+
3011+
while (iter.MoveNext())
3012+
{
3013+
switch (iter.Current.GetString())
3014+
{
3015+
case "commands":
3016+
commands = iter.GetNext().GetString()!;
3017+
break;
3018+
case "keys":
3019+
keys = iter.GetNext().GetString()!;
3020+
break;
3021+
case "channels":
3022+
channels = iter.GetNext().GetString()!;
3023+
break;
3024+
default:
3025+
break;
3026+
}
3027+
}
3028+
return new ACLSelector(commands, keys, channels);
3029+
}
3030+
}
29283031
}
29293032

29303033
internal abstract class ResultProcessor<T> : ResultProcessor

0 commit comments

Comments
 (0)