Merge branch 'cassandra-5.0' into trunk
diff --git a/CHANGES.txt b/CHANGES.txt
index 87987f8..f05749e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -94,6 +94,7 @@
  * Fix resource cleanup after SAI query timeouts (CASSANDRA-19177)
  * Suppress CVE-2023-6481 (CASSANDRA-19184)
 Merged from 4.1:
+ * Do not create a role if ALTER ROLE IF EXISTS operates on non-existing role (CASSANDRA-19749)
  * Use OpOrder in repairIterator to ensure we don't lose memtables mid-paxos repair (Cassandra-19668)
  * Refresh stale paxos commit (CASSANDRA-19617)
  * Reduce info logging from automatic paxos repair (CASSANDRA-19445)
diff --git a/pylib/cqlshlib/test/test_cqlsh_completion.py b/pylib/cqlshlib/test/test_cqlsh_completion.py
index e8c0473..a787f3f 100644
--- a/pylib/cqlshlib/test/test_cqlsh_completion.py
+++ b/pylib/cqlshlib/test/test_cqlsh_completion.py
@@ -1044,11 +1044,16 @@
 
     def test_complete_in_alter_role(self):
         self.trycompletions('ALTER ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
+        self.trycompletions('ALTER ROLE IF ', immediate='EXISTS ')
         self.trycompletions('ALTER ROLE foo ', immediate='WITH ')
         self.trycompletions('ALTER ROLE foo WITH ', choices=['ACCESS', 'HASHED', 'LOGIN', 'OPTIONS', 'PASSWORD', 'SUPERUSER', 'GENERATED'])
         self.trycompletions('ALTER ROLE foo WITH ACCESS TO ', choices=['ALL', 'DATACENTERS'])
         self.trycompletions('ALTER ROLE foo WITH ACCESS FROM ', choices=['ALL', 'CIDRS'])
 
+    def test_complete_in_create_user(self):
+        self.trycompletions('CREATE USER ', choices=['<username>', 'IF'])
+        self.trycompletions('CREATE USER IF ', immediate='NOT EXISTS ')
+
     def test_complete_in_drop_role(self):
         self.trycompletions('DROP ROLE ', choices=['<identifier>', 'IF', '<quotedName>'])
 
diff --git a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
index d12b020..bb83127 100644
--- a/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/AlterRoleStatement.java
@@ -124,6 +124,9 @@
 
     public ResultMessage execute(ClientState state) throws RequestValidationException, RequestExecutionException
     {
+        if (ifExists && !DatabaseDescriptor.getRoleManager().isExistingRole(role))
+            return null;
+
         if (opts.isGeneratedPassword())
         {
             String generatedPassword = Guardrails.password.generate();
diff --git a/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java
index 1d71025..39e0098 100644
--- a/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java
+++ b/test/unit/org/apache/cassandra/auth/CreateAndAlterRoleTest.java
@@ -18,12 +18,20 @@
 
 package org.apache.cassandra.auth;
 
+import java.util.HashSet;
+import java.util.Set;
+
 import org.junit.BeforeClass;
 import org.junit.Test;
 
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.exceptions.InvalidQueryException;
 import org.apache.cassandra.config.DatabaseDescriptor;
 import org.apache.cassandra.cql3.CQLTester;
 
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mindrot.jbcrypt.BCrypt.gensalt;
 import static org.mindrot.jbcrypt.BCrypt.hashpw;
 
@@ -125,4 +133,35 @@
 
         executeNet("SELECT key FROM system.local");
     }
+
+    @Test
+    public void createAlterRoleIfExists()
+    {
+        useSuperUser();
+
+        executeNet("CREATE ROLE IF NOT EXISTS does_not_exist_yet");
+        assertTrue(getAllRoles().contains("does_not_exist_yet"));
+
+        // execute one more time
+        executeNet("CREATE ROLE IF NOT EXISTS does_not_exist_yet");
+
+        assertThatThrownBy(() -> executeNet("CREATE ROLE does_not_exist_yet"))
+        .isInstanceOf(InvalidQueryException.class)
+        .hasMessageContaining("does_not_exist_yet already exists");
+
+        // alter non-existing is no-op when "if exists" is specified
+        executeNet("ALTER ROLE IF EXISTS also_does_not_exist_yet WITH LOGIN = true");
+        Set<String> roles = getAllRoles();
+        assertTrue(roles.contains("does_not_exist_yet"));
+        // not created - CASSANDRA-19749
+        assertFalse(roles.contains("also_does_not_exist_yet"));
+    }
+
+    private Set<String> getAllRoles()
+    {
+        ResultSet rows = executeNet("SELECT role FROM system_auth.roles");
+        Set<String> roles = new HashSet<>();
+        rows.forEach(row -> roles.add(row.getString(0)));
+        return roles;
+    }
 }