Skip to content

Commit

Permalink
add support for ACL LIST
Browse files Browse the repository at this point in the history
  • Loading branch information
atakavci committed Feb 4, 2025
1 parent d95cf2e commit b2655bb
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 27 deletions.
6 changes: 3 additions & 3 deletions src/StackExchange.Redis/APITypes/ACLUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public class ACLSelector
/// <summary>
/// Gets the commands associated with the ACL user.
/// </summary>
public readonly string? Commmands;
public readonly string? Commands;

/// <summary>
/// Gets the keys associated with the ACL user.
Expand All @@ -103,7 +103,7 @@ public class ACLSelector
/// <param name="channels">The channels associated with the ACLSelector.</param>
public ACLSelector(string? commands, string? keys, string? channels)
{
Commmands = commands;
Commands = commands;
Keys = keys;
Channels = channels;
}
Expand All @@ -114,6 +114,6 @@ public ACLSelector(string? commands, string? keys, string? channels)
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return "ACLSelector{" + "Commmands='" + Commmands + "', Keys='" + Keys + "', Channels='" + Channels + "'}";
return "ACLSelector{" + "Commands='" + Commands + "', Keys='" + Keys + "', Channels='" + Channels + "'}";
}
}
14 changes: 14 additions & 0 deletions src/StackExchange.Redis/Interfaces/IServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ public partial interface IServer : IRedis
/// <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>
/// Lists all access control rules.
/// </summary>
/// <param name="flags">The command flags to use.</param>
/// <returns>An array of Redis values representing the access control rules.</returns>
RedisValue[]? AccessControlList(CommandFlags flags = CommandFlags.None);

/// <summary>
/// Asynchronously lists all access control rules.
/// </summary>
/// <param name="flags">The command flags to use.</param>
/// <returns>A task representing the asynchronous operation, with an array of Redis values representing the access control rules.</returns>
Task<RedisValue[]?> AccessControlListAsync(CommandFlags flags = CommandFlags.None);

/// <summary>
/// Loads access control rules.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ 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.Commands -> string?
readonly StackExchange.Redis.ACLSelector.Keys -> string?
readonly StackExchange.Redis.ACLUser.Channels -> string?
readonly StackExchange.Redis.ACLUser.Commands -> string?
Expand Down Expand Up @@ -1157,6 +1157,8 @@ StackExchange.Redis.IServer.AccessControlDeleteUsers(StackExchange.Redis.RedisVa
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
StackExchange.Redis.IServer.AccessControlGeneratePasswordAsync(long bits, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
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.RedisValue[]?>!
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
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 @@ -111,6 +111,18 @@ public Task<RedisValue> AccessControlGeneratePasswordAsync(long bits, CommandFla
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<RedisValue[]?> 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);
Expand Down
109 changes: 86 additions & 23 deletions tests/StackExchange.Redis.Tests/ACLIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ACLIntegrationTests : TestBase

public ACLIntegrationTests(ITestOutputHelper output, SharedConnectionFixture fixture) : base(output, fixture)
{
_conn = Create();
_conn = Create(require: RedisFeatures.v7_4_0_rc1);
_redisServer = GetAnyPrimary(_conn);
}

Expand Down Expand Up @@ -47,8 +47,8 @@ public void AccessControlGetUser_ShouldReturnUserDetails()
{
Action<ACLSelectorRulesBuilder> act = rules => rules.CommandsAllowed("GET", "SET");
// Arrange
var username = new RedisValue("testuser");
_redisServer.AccessControlSetUser("testuser", new ACLRulesBuilder()
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"))
Expand All @@ -58,7 +58,7 @@ public void AccessControlGetUser_ShouldReturnUserDetails()
.Build());

// Act
var user = _redisServer.AccessControlGetUser(username);
var user = _redisServer.AccessControlGetUser(userName);

// Assert
Assert.NotNull(user);
Expand All @@ -72,7 +72,7 @@ public void AccessControlGetUser_ShouldReturnUserDetails()
public void AccessControlGetUser_ShouldReturnNullForNonExistentUser()
{
// Act
var user = _redisServer.AccessControlGetUser("nonexistentuser");
var user = _redisServer.AccessControlGetUser(Me());

// Assert
Assert.Null(user);
Expand All @@ -82,10 +82,11 @@ public void AccessControlGetUser_ShouldReturnNullForNonExistentUser()
public void AccessControlDeleteUsers_ShouldReturnCorrectCount()
{
// Arrange
_redisServer.AccessControlSetUser(new RedisValue("user1"), new ACLRulesBuilder().Build());
string userName = Me();
_redisServer.AccessControlSetUser(new RedisValue(userName), new ACLRulesBuilder().Build());

// Act
var count = _redisServer.AccessControlDeleteUsers(new RedisValue[] { "user1", "user2" });
var count = _redisServer.AccessControlDeleteUsers(new RedisValue[] { userName, "user2" });

// Assert
Assert.Equal(1, count);
Expand Down Expand Up @@ -116,14 +117,15 @@ public void AccessControlLogReset_ShouldExecuteSuccessfully()
public void AccessControlLog_ShouldReturnLogs()
{
// Arrange
string userName = Me();
_redisServer.AccessControlSetUser(
"user1",
userName,
new ACLRulesBuilder()
.WithACLUserRules(rules => rules.PasswordsToSet(new[] { "pass1" })
.UserState(ACLUserState.ON))
.Build());

Assert.Throws<RedisServerException>(() => _conn.GetDatabase().Execute("AUTH", "user1", "pass2"));
Assert.Throws<RedisServerException>(() => _conn.GetDatabase().Execute("AUTH", userName, "pass2"));

// Act
var logs = _redisServer.AccessControlLog(10);
Expand All @@ -137,17 +139,6 @@ public void AccessControlLog_ShouldReturnLogs()
[Fact]
public void AccessControlWhoAmI_ShouldReturnCurrentUser()
{
// // Arrange
// var conn = Create(require: RedisFeatures.v7_0_0_rc1);
// var redisServer = (RedisServer)GetAnyPrimary(conn);

// redisServer.AccessControlSetUser(
// "user1",
// new ACLRulesBuilder()
// .WithACLUserRules(rules => rules.PasswordsToSet(new[] { "pass1" })
// .UserState(UserState.ON))
// .Build());

// Act
var user = _redisServer.AccessControlWhoAmI();

Expand All @@ -156,14 +147,86 @@ public void AccessControlWhoAmI_ShouldReturnCurrentUser()
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(new RedisValue("testuser"), new ACLRules(null, null, null));
_redisServer.AccessControlSetUser(userName, updatedRules);

// Assert
// In this case, we're asserting that no exceptions are thrown and the user is successfully set.
// To validate this, you might want to verify if the user exists in your Redis instance or use a similar check.
var user = _redisServer.AccessControlGetUser(userName);
Assert.NotNull(user);
Assert.DoesNotContain(hashedPassword1, user.Passwords!);
Assert.Contains(hashedPassword2, user.Passwords!);
Assert.Contains(updatedPassword, user.Passwords!);
}
}

0 comments on commit b2655bb

Please sign in to comment.