AccessControlListCommandsTest.java
package redis.clients.jedis.commands.jedis;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static redis.clients.jedis.util.RedisVersionUtil.getRedisVersion;
import java.util.Arrays;
import java.util.List;
import io.redis.test.annotations.SinceRedisVersion;
import io.redis.test.utils.RedisVersion;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.MethodSource;
import redis.clients.jedis.CommandArguments;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Protocol;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisAccessControlException;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.resps.AccessControlLogEntry;
import redis.clients.jedis.resps.AccessControlUser;
import redis.clients.jedis.util.SafeEncoder;
/**
* TODO: properly define and test exceptions
*/
@ParameterizedClass
@MethodSource("redis.clients.jedis.commands.CommandsTestsParameters#respVersions")
public class AccessControlListCommandsTest extends JedisCommandsTestBase {
public static final String USER_NAME = "newuser";
public static final String USER_PASSWORD = "secret";
public static final String USER_ANTIREZ = "antirez";
@BeforeAll
public static void prepare() throws Exception {
// Use to check if the ACL test should be ran. ACL are available only in 6.0 and later
assumeTrue(getRedisVersion(endpoint).isGreaterThanOrEqualTo(RedisVersion.V6_0_0),
"Not running ACL test on this version of Redis");
}
public AccessControlListCommandsTest(RedisProtocol protocol) {
super(protocol);
}
@AfterEach
@Override
public void tearDown() throws Exception {
try {
jedis.aclDelUser(USER_NAME);
jedis.aclDelUser(USER_ANTIREZ);
} catch (Exception e) {
// Ignore exception
}
super.tearDown();
}
@Test
public void aclWhoAmI() {
String string = jedis.aclWhoAmI();
assertEquals("default", string);
byte[] binary = jedis.aclWhoAmIBinary();
assertArrayEquals(SafeEncoder.encode("default"), binary);
}
@Test
public void aclListDefault() {
assertFalse(jedis.aclList().isEmpty());
assertFalse(jedis.aclListBinary().isEmpty());
}
@Test
public void addAndRemoveUser() {
int existingUsers = jedis.aclList().size();
String status = jedis.aclSetUser(USER_NAME);
assertEquals("OK", status);
assertEquals(existingUsers + 1, jedis.aclList().size());
assertEquals(existingUsers + 1, jedis.aclListBinary().size()); // test binary
long count = jedis.aclDelUser(USER_NAME);
assertEquals(1, count);
assertEquals(existingUsers, jedis.aclList().size());
assertEquals(existingUsers, jedis.aclListBinary().size()); // test binary
}
@Test
public void aclUsers() {
List<String> users = jedis.aclUsers();
assertEquals(3, users.size());
assertThat(users, Matchers.hasItem("default"));
assertThat(users, Matchers.hasItem("deploy"));
assertThat(users, Matchers.hasItem("acljedis"));
assertEquals(3, jedis.aclUsersBinary().size()); // Test binary
}
@Test
@SinceRedisVersion(value = "7.0.0", message = "Redis 6.2.x misses [~]>")
public void aclGetUser() {
// get default user information
AccessControlUser userInfo = jedis.aclGetUser("default");
assertFalse(userInfo.getFlags().isEmpty());
assertEquals(1, userInfo.getPassword().size());
assertEquals("+@all", userInfo.getCommands());
assertEquals("~*", userInfo.getKeys());
// create new user
jedis.aclSetUser(USER_NAME);
userInfo = jedis.aclGetUser(USER_NAME);
assertFalse(userInfo.getFlags().isEmpty());
assertEquals("off", userInfo.getFlags().get(0));
assertTrue(userInfo.getPassword().isEmpty());
assertTrue(userInfo.getKeys().isEmpty());
// reset user
jedis.aclSetUser(USER_NAME, "reset", "+@all", "~*", "-@string", "+incr", "-debug",
"+debug|digest");
userInfo = jedis.aclGetUser(USER_NAME);
assertThat(userInfo.getCommands(), containsString("+@all"));
assertThat(userInfo.getCommands(), containsString("-@string"));
assertThat(userInfo.getCommands(), containsString("+debug|digest"));
}
@Test
public void createUserAndPasswords() {
String status = jedis.aclSetUser(USER_NAME, ">" + USER_PASSWORD);
assertEquals("OK", status);
// create a new client to try to authenticate
Jedis jedis2 = new Jedis();
// the user is just created without any permission the authentication should fail
try {
jedis2.auth(USER_NAME, USER_PASSWORD);
fail("Should throw a WRONGPASS exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("WRONGPASS "));
}
// now activate the user
jedis.aclSetUser(USER_NAME, "on", "+acl");
jedis2.auth(USER_NAME, USER_PASSWORD);
assertEquals(USER_NAME, jedis2.aclWhoAmI());
// test invalid password
jedis2.close();
try {
jedis2.auth(USER_NAME, "wrong-password");
fail("Should throw a WRONGPASS exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("WRONGPASS "));
}
// remove password, and try to authenticate
jedis.aclSetUser(USER_NAME, "<" + USER_PASSWORD);
try {
jedis2.auth(USER_NAME, USER_PASSWORD);
fail("Should throw a WRONGPASS exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("WRONGPASS "));
}
jedis.aclDelUser(USER_NAME); // delete the user
try {
jedis2.auth(USER_NAME, "wrong-password");
fail("Should throw a WRONGPASS exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("WRONGPASS "));
}
jedis2.close();
}
@Test
public void aclSetUserWithAnyPassword() {
String status = jedis.aclSetUser(USER_NAME, "nopass");
assertEquals("OK", status);
status = jedis.aclSetUser(USER_NAME, "on", "+acl");
assertEquals("OK", status);
// connect with this new user and try to get/set keys
Jedis jedis2 = new Jedis();
String authResult = jedis2.auth(USER_NAME, "any password");
assertEquals("OK", authResult);
jedis2.close();
}
@Test
public void aclExcudeSingleCommand() {
String status = jedis.aclSetUser(USER_NAME, "nopass");
assertEquals("OK", status);
status = jedis.aclSetUser(USER_NAME, "on", "+acl");
assertEquals("OK", status);
status = jedis.aclSetUser(USER_NAME, "allcommands", "allkeys");
assertEquals("OK", status);
status = jedis.aclSetUser(USER_NAME, "-ping");
assertEquals("OK", status);
// connect with this new user and try to get/set keys
Jedis jedis2 = new Jedis();
String authResult = jedis2.auth(USER_NAME, "any password");
assertEquals("OK", authResult);
jedis2.incr("mycounter");
try {
jedis2.ping();
fail("Should throw a NOPERM exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("NOPERM "));
assertThat(e.getMessage(), containsString(" has no permissions to run the 'ping' command"));
}
jedis2.close();
}
@Test
@SinceRedisVersion("7.0.0")
public void aclDryRun() {
jedis.aclSetUser(USER_NAME, "nopass", "allkeys", "+set", "-get");
assertEquals("OK", jedis.aclDryRun(USER_NAME, "SET", "key", "value"));
assertThat(jedis.aclDryRun(USER_NAME, "GET", "key"),
endsWith(" has no permissions to run the 'get' command"));
assertEquals("OK", jedis.aclDryRun(USER_NAME,
new CommandArguments(Protocol.Command.SET).key("ca-key").add("value")));
assertThat(jedis.aclDryRun(USER_NAME, new CommandArguments(Protocol.Command.GET).key("ca-key")),
endsWith(" has no permissions to run the 'get' command"));
}
@Test
@SinceRedisVersion("7.0.0")
public void aclDryRunBinary() {
byte[] username = USER_NAME.getBytes();
jedis.aclSetUser(username, "nopass".getBytes(), "allkeys".getBytes(), "+set".getBytes(), "-get".getBytes());
assertArrayEquals("OK".getBytes(), jedis.aclDryRunBinary(username,
"SET".getBytes(), "key".getBytes(), "value".getBytes()));
assertThat(new String(jedis.aclDryRunBinary(username, "GET".getBytes(), "key".getBytes())),
endsWith(" has no permissions to run the 'get' command"));
assertArrayEquals("OK".getBytes(), jedis.aclDryRunBinary(username,
new CommandArguments(Protocol.Command.SET).key("ca-key").add("value")));
assertThat(new String(jedis.aclDryRunBinary(username,
new CommandArguments(Protocol.Command.GET).key("ca-key"))),
endsWith(" has no permissions to run the 'get' command"));
}
@Test
public void aclDelUser() {
String statusSetUser = jedis.aclSetUser(USER_NAME);
assertEquals("OK", statusSetUser);
int before = jedis.aclList().size();
assertEquals(1L, jedis.aclDelUser(USER_NAME));
int after = jedis.aclList().size();
assertEquals(before - 1, after);
}
@Test
public void basicPermissionsTest() {
// create a user with login permissions
jedis.aclSetUser(USER_NAME, ">" + USER_PASSWORD);
// users are not able to access any command
jedis.aclSetUser(USER_NAME, "on", "+acl");
// connect with this new user and try to get/set keys
Jedis jedis2 = new Jedis();
jedis2.auth(USER_NAME, USER_PASSWORD);
try {
jedis2.set("foo", "bar");
fail("Should throw a NOPERM exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), startsWith("NOPERM "));
assertThat(e.getMessage(), containsString(" has no permissions to run the 'set' command"));
}
// change permissions of the user
// by default users are not able to access any key
jedis.aclSetUser(USER_NAME, "+set");
jedis2.close();
jedis2.auth(USER_NAME, USER_PASSWORD);
final List<String> nopermKeys = Arrays.asList("NOPERM No permissions to access a key",
"NOPERM this user has no permissions to access one of the keys used as arguments");
try {
jedis2.set("foo", "bar");
fail("Should throw a NOPERM exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), Matchers.isIn(nopermKeys));
}
// allow user to access a subset of the key
jedis.aclSetUser(USER_NAME, "allcommands", "~foo:*", "~bar:*"); // TODO : a DSL
// create key foo, bar and zap
jedis2.set("foo:1", "a");
jedis2.set("bar:2", "b");
try {
jedis2.set("zap:3", "c");
fail("Should throw a NOPERM exception");
} catch (JedisAccessControlException e) {
assertThat(e.getMessage(), Matchers.isIn(nopermKeys));
}
}
@Test
public void aclCatTest() {
List<String> categories = jedis.aclCat();
assertFalse(categories.isEmpty());
// test binary
List<byte[]> categoriesBinary = jedis.aclCatBinary();
assertFalse(categories.isEmpty());
assertEquals(categories.size(), categoriesBinary.size());
// test commands in a category
assertFalse(jedis.aclCat("scripting").isEmpty());
try {
jedis.aclCat("testcategory");
fail("Should throw a ERR exception");
} catch (Exception e) {
assertEquals("ERR Unknown category 'testcategory'", e.getMessage());
}
}
@Test
public void aclLogTest() {
jedis.aclLogReset();
assertTrue(jedis.aclLog().isEmpty());
// create new user and cconnect
jedis.aclSetUser(USER_ANTIREZ, ">foo", "on", "+set", "~object:1234");
jedis.aclSetUser(USER_ANTIREZ, "+eval", "+multi", "+exec");
jedis.auth(USER_ANTIREZ, "foo");
// generate an error (antirez user does not have the permission to access foo)
try {
jedis.get("foo");
fail("Should have thrown an JedisAccessControlException: user does not have the permission to get(\"foo\")");
} catch (JedisAccessControlException e) {
}
// test the ACL Log
jedis.auth(endpoint.getUsername(), endpoint.getPassword());
List<AccessControlLogEntry> aclEntries = jedis.aclLog();
assertEquals(1, aclEntries.size(), "Number of log messages ");
assertEquals(1, aclEntries.get(0).getCount());
assertEquals(USER_ANTIREZ, aclEntries.get(0).getUsername());
assertEquals("toplevel", aclEntries.get(0).getContext());
assertEquals("command", aclEntries.get(0).getReason());
assertEquals("get", aclEntries.get(0).getObject());
// Capture similar event
jedis.aclLogReset();
assertTrue(jedis.aclLog().isEmpty());
jedis.auth(USER_ANTIREZ, "foo");
for (int i = 0; i < 10; i++) {
// generate an error (antirez user does not have the permission to access foo)
try {
jedis.get("foo");
fail("Should have thrown an JedisAccessControlException: user does not have the permission to get(\"foo\")");
} catch (JedisAccessControlException e) {
}
}
// test the ACL Log
jedis.auth(endpoint.getUsername(), endpoint.getPassword());
assertEquals(1, jedis.aclLog().size(), "Number of log messages ");
assertEquals(10, jedis.aclLog().get(0).getCount());
assertEquals("get", jedis.aclLog().get(0).getObject());
// Generate another type of error
jedis.auth(USER_ANTIREZ, "foo");
try {
jedis.set("somekeynotallowed", "1234");
fail("Should have thrown an JedisAccessControlException: user does not have the permission to set(\"somekeynotallowed\", \"1234\")");
} catch (JedisAccessControlException e) {
}
// test the ACL Log
jedis.auth(endpoint.getUsername(), endpoint.getPassword());
assertEquals( 2, jedis.aclLog().size(), "Number of log messages ");
assertEquals(1, jedis.aclLog().get(0).getCount());
assertEquals("somekeynotallowed", jedis.aclLog().get(0).getObject());
assertEquals("key", jedis.aclLog().get(0).getReason());
jedis.aclLogReset();
assertTrue(jedis.aclLog().isEmpty());
jedis.auth(USER_ANTIREZ, "foo");
Transaction t = jedis.multi();
t.incr("foo");
try {
t.exec();
fail("Should have thrown an JedisAccessControlException: user does not have the permission to incr(\"foo\")");
} catch (Exception e) {
}
t.close();
jedis.auth(endpoint.getUsername(), endpoint.getPassword());
assertEquals( 1, jedis.aclLog().size(), "Number of log messages ");
assertEquals(1, jedis.aclLog().get(0).getCount());
assertEquals("multi", jedis.aclLog().get(0).getContext());
assertEquals("incr", jedis.aclLog().get(0).getObject());
// ACL LOG can accept a numerical argument to show less entries
jedis.auth(USER_ANTIREZ, "foo");
for (int i = 0; i < 5; i++) {
try {
jedis.incr("foo");
fail("Should have thrown an JedisAccessControlException: user does not have the permission to incr(\"foo\")");
} catch (JedisAccessControlException e) {
}
}
try {
jedis.set("foo-2", "bar");
fail("Should have thrown an JedisAccessControlException: user does not have the permission to set(\"foo-2\", \"bar\")");
} catch (JedisAccessControlException e) {
}
jedis.auth(endpoint.getUsername(), endpoint.getPassword());
assertEquals( 3, jedis.aclLog().size(), "Number of log messages ");
assertEquals( 2, jedis.aclLog(2).size(), "Number of log messages ");
// Binary tests
assertEquals( 3, jedis.aclLogBinary().size(), "Number of log messages ");
assertEquals( 2, jedis.aclLogBinary(2).size(), "Number of log messages ");
// RESET
String status = jedis.aclLogReset();
assertEquals(status, "OK");
jedis.aclDelUser(USER_ANTIREZ);
}
@Test
@SinceRedisVersion(value = "7.2.0", message = "Starting with Redis version 7.2.0: Added entry ID, timestamp created, and timestamp last updated.")
public void aclLogWithEntryID() {
try {
jedis.auth("wronguser", "wrongpass");
fail("wrong user should not passed");
} catch (JedisAccessControlException e) {
}
List<AccessControlLogEntry> aclEntries = jedis.aclLog();
assertEquals( 1, aclEntries.size(), "Number of log messages ");
assertEquals(1, aclEntries.get(0).getCount());
assertEquals("wronguser", aclEntries.get(0).getUsername());
assertEquals("toplevel", aclEntries.get(0).getContext());
assertEquals("auth", aclEntries.get(0).getReason());
assertEquals("AUTH", aclEntries.get(0).getObject());
assertTrue(aclEntries.get(0).getEntryId() >= 0);
assertTrue(aclEntries.get(0).getTimestampCreated() > 0);
assertEquals(aclEntries.get(0).getTimestampCreated(), aclEntries.get(0).getTimestampLastUpdated());
// RESET
String status = jedis.aclLogReset();
assertEquals(status, "OK");
}
@Test
public void aclGenPass() {
assertNotNull(jedis.aclGenPass());
// bit length case
assertNotNull(jedis.aclGenPassBinary(16));
assertNotNull(jedis.aclGenPassBinary(32));
}
@Test
public void aclGenPassBinary() {
assertNotNull(jedis.aclGenPassBinary());
// bit length case
assertNotNull(jedis.aclGenPassBinary(16));
assertNotNull(jedis.aclGenPassBinary(32));
}
@Test
@SinceRedisVersion(value = "7.0.0", message = "Redis 6.2.x skips [&]>")
public void aclBinaryCommandsTest() {
jedis.aclSetUser(USER_NAME.getBytes());
assertNotNull(jedis.aclGetUser(USER_NAME));
assertEquals(1L, jedis.aclDelUser(USER_NAME.getBytes()));
jedis.aclSetUser(USER_NAME.getBytes(), "reset".getBytes(), "+@all".getBytes(), "~*".getBytes(),
"-@string".getBytes(), "+incr".getBytes(), "-debug".getBytes(), "+debug|digest".getBytes(),
"resetchannels".getBytes(), "&testchannel:*".getBytes());
AccessControlUser userInfo = jedis.aclGetUser(USER_NAME.getBytes());
assertThat(userInfo.getCommands(), containsString("+@all"));
assertThat(userInfo.getCommands(), containsString("-@string"));
assertThat(userInfo.getCommands(), containsString("+debug|digest"));
assertEquals("&testchannel:*", userInfo.getChannels());
jedis.aclDelUser(USER_NAME.getBytes());
jedis.aclSetUser("TEST_USER".getBytes());
jedis.aclSetUser("ANOTHER_TEST_USER".getBytes());
jedis.aclSetUser("MORE_TEST_USERS".getBytes());
assertEquals(3L, jedis.aclDelUser(
"TEST_USER".getBytes(),
"ANOTHER_TEST_USER".getBytes(),
"MORE_TEST_USERS".getBytes()));
}
@Test
public void aclLoadTest() {
try {
jedis.aclLoad();
fail("Should throw a JedisDataException: ERR This Redis instance is not configured to use an ACL file...");
} catch (JedisDataException e) {
assertThat(e.getMessage(), startsWith("ERR This Redis instance is not configured to use an ACL file."));
}
// TODO test with ACL file
}
@Test
public void aclSaveTest() {
try {
jedis.aclSave();
fail("Should throw a JedisDataException: ERR This Redis instance is not configured to use an ACL file...");
} catch (JedisDataException e) {
assertThat(e.getMessage(), startsWith("ERR This Redis instance is not configured to use an ACL file."));
}
// TODO test with ACL file
}
}