diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
index 20438ea06893f3abf141cb4a82cc474812f362b9..66d8b3660215477bf3e0ba83b7f115d7637352fe 100644
--- a/doc/CHANGELOG.md
+++ b/doc/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+-  Connection::ListUsers method
+
 ### Changed
 
 - Updated dependency versions.
diff --git a/include/caosdb/connection.h b/include/caosdb/connection.h
index d0414f2e84f81fe9e86a50f2e6411788cf0befeb..e2a17390293f43bf2d6cc8a2af77f5ca23ab13d0 100644
--- a/include/caosdb/connection.h
+++ b/include/caosdb/connection.h
@@ -43,6 +43,9 @@
 #include <map>                             // for map
 #include <memory>                          // for shared_ptr, unique_ptr
 #include <string>                          // for string, basic_string
+#ifdef BUILD_ACM
+#include <vector> // for vector
+#endif
 
 namespace caosdb::connection {
 #ifdef BUILD_ACM
@@ -122,6 +125,11 @@ public:
   // TODO(tf) find a way to deal with this:
   // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
   auto DeleteSingleUser(const std::string &realm, const std::string &name) const -> void;
+
+  /**
+   * List known users.
+   */
+  auto ListUsers() const -> std::vector<User>;
 #endif
 
 private:
diff --git a/requirements.txt b/requirements.txt
index b0410ad5624768205f16743a17bef2d9e34ead66..c3029f4aa360788c3c32abbae097f5a0023a5c42 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,7 +15,7 @@ pluginbase==1.0.1
 Pygments==2.13.0
 PyJWT==2.6.0
 python-dateutil==2.8.2
-PyYAML==6.0
+PyYAML==6.0.1
 requests==2.28.1
 six==1.16.0
 tqdm==4.64.1
diff --git a/src/caosdb/connection.cpp b/src/caosdb/connection.cpp
index 4c77c5254c1300e908622882cd01d29d7b0aab65..550e35aac5c58383b7a8129bc68330769c5260ad 100644
--- a/src/caosdb/connection.cpp
+++ b/src/caosdb/connection.cpp
@@ -36,6 +36,9 @@
 #include <grpcpp/create_channel.h>       // for CreateChannel
 #include <grpcpp/support/status.h>       // for Status
 #include <string>                        // for string, operator+
+#ifdef BUILD_ACM
+#include <vector> // for vector
+#endif
 // IWYU pragma: no_include "net/proto2/public/repeated_field.h"
 
 namespace caosdb::connection {
@@ -46,6 +49,8 @@ using caosdb::acm::v1alpha1::CreateSingleUserRequest;
 using caosdb::acm::v1alpha1::CreateSingleUserResponse;
 using caosdb::acm::v1alpha1::DeleteSingleUserRequest;
 using caosdb::acm::v1alpha1::DeleteSingleUserResponse;
+using caosdb::acm::v1alpha1::ListUsersRequest;
+using caosdb::acm::v1alpha1::ListUsersResponse;
 using caosdb::acm::v1alpha1::RetrieveSingleUserRequest;
 using caosdb::acm::v1alpha1::RetrieveSingleUserResponse;
 #endif
@@ -204,6 +209,37 @@ auto Connection::CreateSingleUser(const User &user) const -> void {
   }
   status.ThrowExceptionIfError();
 }
+
+auto Connection::ListUsers() const -> std::vector<User> {
+  ListUsersRequest request;
+  ListUsersResponse response;
+  grpc::ClientContext context;
+  const grpc::Status grpc_status =
+    this->access_controll_management_service->ListUsers(&context, request, &response);
+  auto status = TransactionStatus::SUCCESS();
+  if (!grpc_status.ok()) {
+    switch (grpc_status.error_code()) {
+    case grpc::StatusCode::UNAUTHENTICATED:
+      status = TransactionStatus::AUTHENTICATION_ERROR(grpc_status.error_message());
+      break;
+    case grpc::StatusCode::UNAVAILABLE:
+      status = TransactionStatus::CONNECTION_ERROR();
+      break;
+    default:
+      auto error_message = grpc_status.error_message();
+      status = TransactionStatus::RPC_ERROR(std::to_string(grpc_status.error_code()) + " - " +
+                                            error_message);
+    }
+  }
+  status.ThrowExceptionIfError();
+
+  std::vector<User> results;
+  for (auto &user : *(response.mutable_users())) {
+    results.push_back(User(std::make_unique<UserImpl>(&user)));
+  }
+
+  return results;
+}
 #endif
 
 auto ConnectionManager::mHasConnection(const std::string &name) const -> bool {