diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt
index 29c27eaf536434d2f870fc111ca00f5febec1327..a913c6906d61b4a171fe9955c9c2f81e17f60433 100644
--- a/include/CMakeLists.txt
+++ b/include/CMakeLists.txt
@@ -52,7 +52,9 @@ set(libcaosdb_INCL
 
 IF(BUILD_ACM)
     list(APPEND libcaosdb_INCL
+         ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/role.h
          ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/user.h
+         ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/permission_rule.h
          )
 ENDIF()
 
diff --git a/include/caosdb/acm/permission_rule.h b/include/caosdb/acm/permission_rule.h
new file mode 100644
index 0000000000000000000000000000000000000000..017a33039b1dee350855f399187c871e01b074e7
--- /dev/null
+++ b/include/caosdb/acm/permission_rule.h
@@ -0,0 +1,69 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * @brief PermissionRules grant or deny permissions to roles.
+ *
+ * @file caosdb/acm/permission_rule.h
+ * @author Timm Fitchen
+ * @date 2022-02-11
+ */
+#ifdef BUILD_ACM
+#ifndef CAOSDB_ACM_PERMISSION_RULE_H
+#define CAOSDB_ACM_PERMISSION_RULE_H
+
+#include <stddef.h>      // for size_t
+#include <unordered_set> // for unordered_set
+#include <string>        // for string
+
+namespace caosdb::acm {
+
+class PermissionRuleImpl;
+
+class PermissionRule {
+public:
+  PermissionRule(std::string permission, bool isGrant, bool isPriority);
+  explicit PermissionRule(PermissionRuleImpl *wrapped);
+  PermissionRule(const PermissionRule &rule);
+  auto operator=(const PermissionRule &rule) -> PermissionRule &;
+  ~PermissionRule();
+  auto ToString() const -> std::string;
+  [[nodiscard]] auto IsGrant() const -> bool;
+  auto SetGrant(bool isGrant) -> void;
+  [[nodiscard]] auto IsPriority() const -> bool;
+  auto SetPriority(bool isPriority) -> void;
+  [[nodiscard]] auto GetPermission() const -> const std::string &;
+
+  auto operator==(const PermissionRule &other) const -> bool;
+
+private:
+  PermissionRuleImpl *impl;
+};
+
+struct HashPermissionRule {
+  auto operator()(const PermissionRule &rule) const -> size_t;
+};
+
+using PermissionRules = std::unordered_set<PermissionRule, HashPermissionRule>;
+
+} // namespace caosdb::acm
+#endif
+#endif
diff --git a/include/caosdb/acm/role.h b/include/caosdb/acm/role.h
new file mode 100644
index 0000000000000000000000000000000000000000..560449b12762206680d9445cbc7bbe6942d5b896
--- /dev/null
+++ b/include/caosdb/acm/role.h
@@ -0,0 +1,77 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+/**
+ * @brief Roles, together with users, and permissions are a fundamental concept
+ * of the access controll management of CaosDB.
+ *
+ * @file caosdb/acm/role.h
+ * @author Timm Fitchen
+ * @date 2022-02-11
+ */
+#ifdef BUILD_ACM
+#ifndef CAOSDB_ACM_ROLES_H
+#define CAOSDB_ACM_ROLES_H
+
+#include "caosdb/acm/permission_rule.h" // for PermissionRule
+#include <memory>                       // for unique_ptr
+#include <string>                       // for string
+
+namespace caosdb::connection {
+class Connection;
+}
+
+namespace caosdb::acm {
+
+class RoleImpl;
+
+class Role {
+public:
+  explicit Role(const std::string &name);
+  explicit Role(std::unique_ptr<RoleImpl> wrapped);
+  Role(const std::string &name, const std::string &description);
+  Role(const Role &role);
+  auto operator=(const Role &role) -> Role &;
+  ~Role();
+  auto ToString() const -> std::string;
+  [[nodiscard]] auto GetName() const -> const std::string &;
+  auto SetName(const std::string &name) -> void;
+  [[nodiscard]] auto GetDescription() const -> const std::string &;
+  auto SetDescription(const std::string &description) -> void;
+  [[nodiscard]] auto GetPermissionRules() const -> const PermissionRules &;
+  // auto SetPermissionRules(PermissionRules rules) -> void;
+  auto ClearPermissionRules() -> void;
+  auto Grant(const std::string &permission, bool isPriority) -> void;
+  auto Deny(const std::string &permission, bool isPriority) -> void;
+  auto RevokeGrant(const std::string &permission, bool isPriority) -> void;
+  auto RevokeDenial(const std::string &permission, bool isPriority) -> void;
+  auto AddPermissionRule(const std::string &permission, bool isGrant, bool isPriority) -> void;
+  auto RemovePermissionRule(const std::string &permission, bool isGrant, bool isPriority) -> void;
+
+  friend class caosdb::connection::Connection;
+
+private:
+  std::unique_ptr<RoleImpl> wrapped;
+};
+
+} // namespace caosdb::acm
+#endif
+#endif
diff --git a/include/caosdb/connection.h b/include/caosdb/connection.h
index d0414f2e84f81fe9e86a50f2e6411788cf0befeb..1f47cc045baf3f0217c315e5d9e9b4e826007d54 100644
--- a/include/caosdb/connection.h
+++ b/include/caosdb/connection.h
@@ -28,6 +28,7 @@
  * @brief Configuration and setup of the connection.
  */
 #ifdef BUILD_ACM
+#include "caosdb/acm/role.h"                  // for Role
 #include "caosdb/acm/user.h"                  // for User
 #include "caosdb/acm/v1alpha1/main.grpc.pb.h" // for AccessControlMan...
 #endif
@@ -43,9 +44,11 @@
 #include <map>                             // for map
 #include <memory>                          // for shared_ptr, unique_ptr
 #include <string>                          // for string, basic_string
+#include <vector>                          // for vector
 
 namespace caosdb::connection {
 #ifdef BUILD_ACM
+using caosdb::acm::Role;
 using caosdb::acm::User;
 using caosdb::acm::v1alpha1::AccessControlManagementService;
 #endif
@@ -103,6 +106,17 @@ public:
   [[nodiscard]] auto CreateTransaction() const -> std::unique_ptr<Transaction>;
 
 #ifdef BUILD_ACM
+  /**
+   * List all known roles.
+   */
+  [[nodiscard]] auto ListRoles() const -> std::vector<Role>;
+
+  [[nodiscard]] auto RetrieveSingleRole(const std::string &name) const -> Role;
+
+  auto CreateSingleRole(const Role &role) const -> void;
+
+  auto DeleteSingleRole(const std::string &name) const -> void;
+
   /**
    * Retrieve a single user.
    */
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f64bc7671cbe142390ca5439916dfbddd3660294..f2917635ea619755e50bd2291b436d66820e226f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -41,6 +41,9 @@ set(libcaosdb_SRC
 
 IF(BUILD_ACM)
     list(APPEND libcaosdb_SRC
+         ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/permission_rule.cpp
+         ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/role.cpp
+         ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/role_impl.h
          ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/user.cpp
          ${CMAKE_CURRENT_SOURCE_DIR}/caosdb/acm/user_impl.h
          )
diff --git a/src/caosdb/acm/permission_rule.cpp b/src/caosdb/acm/permission_rule.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..bc4cb2f09478051165002d26abb60c23683dc1b3
--- /dev/null
+++ b/src/caosdb/acm/permission_rule.cpp
@@ -0,0 +1,91 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#include "caosdb/acm/permission_rule.h"
+#include "caosdb/acm/permission_rule_impl.h" // for PermissionRuleImpl
+#include "caosdb/acm/v1alpha1/main.pb.h"     // for ListPermissionRulesRequest
+#include "caosdb/protobuf_helper.h"          // for ProtoMessageWrapper
+#include <type_traits>                       // for remove_reference<>::type
+#include <utility>                           // for move
+
+namespace caosdb::acm {
+using caosdb::utility::ScalarProtoMessageWrapper;
+using ProtoPermissionRule = caosdb::acm::v1alpha1::PermissionRule;
+
+PermissionRuleImpl::PermissionRuleImpl() = default;
+
+PermissionRuleImpl::PermissionRuleImpl(ProtoPermissionRule *rule)
+  : ScalarProtoMessageWrapper<ProtoPermissionRule>(rule) {}
+
+PermissionRuleImpl::PermissionRuleImpl(std::string permission, bool isGrant, bool isPriority) {
+  this->wrapped->set_permission(std::move(permission));
+  this->wrapped->set_grant(isGrant);
+  this->wrapped->set_priority(isPriority);
+}
+
+PermissionRule::PermissionRule(std::string permission, bool isGrant, bool isPriority)
+  : impl(new PermissionRuleImpl(std::move(permission), isGrant, isPriority)) {}
+
+PermissionRule::PermissionRule(const PermissionRule &rule) : impl(new PermissionRuleImpl()) {
+  this->impl->wrapped->CopyFrom(*(rule.impl->wrapped));
+}
+
+auto PermissionRule::operator=(const PermissionRule &rule) -> PermissionRule & {
+  if (this == &rule) {
+    return *this;
+  }
+  this->impl->wrapped->CopyFrom(*(rule.impl->wrapped));
+
+  return *this;
+}
+
+PermissionRule::~PermissionRule() { delete this->impl; }
+
+auto PermissionRule::ToString() const -> std::string { return this->impl->ToString(); }
+
+[[nodiscard]] auto PermissionRule::IsGrant() const -> bool { return this->impl->wrapped->grant(); }
+
+auto PermissionRule::SetGrant(bool isGrant) -> void { this->impl->wrapped->set_grant(isGrant); }
+
+[[nodiscard]] auto PermissionRule::IsPriority() const -> bool {
+  return this->impl->wrapped->priority();
+}
+
+auto PermissionRule::SetPriority(bool isPriority) -> void {
+  this->impl->wrapped->set_priority(isPriority);
+}
+
+auto PermissionRule::operator==(const PermissionRule &other) const -> bool {
+  if (this == &other) {
+    return true;
+  }
+  return this->GetPermission() == other.GetPermission() &&
+         this->IsPriority() == other.IsPriority() && this->IsGrant() == other.IsGrant();
+}
+
+auto HashPermissionRule::operator()(const PermissionRule &rule) const -> size_t {
+  return std::hash<std::string>()(rule.GetPermission());
+}
+
+auto PermissionRule::GetPermission() const -> const std::string & {
+  return this->impl->wrapped->permission();
+}
+
+} // namespace caosdb::acm
diff --git a/src/caosdb/acm/permission_rule_impl.h b/src/caosdb/acm/permission_rule_impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..b8e99f5efdce5c68081ec196ab2afbe67ad970a4
--- /dev/null
+++ b/src/caosdb/acm/permission_rule_impl.h
@@ -0,0 +1,51 @@
+
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#ifndef CAOSDB_ACM_PERMISSION_RULE_IMPL_H
+#define CAOSDB_ACM_PERMISSION_RULE_IMPL_H
+
+#include "caosdb/acm/role.h"
+#include "caosdb/acm/v1alpha1/main.pb.h" // for ListRolesRequest
+#include "caosdb/protobuf_helper.h"      // for ProtoMessageWrapper
+#include <utility>                       // for move
+
+namespace caosdb::acm {
+using caosdb::utility::ScalarProtoMessageWrapper;
+using ProtoPermissionRule = caosdb::acm::v1alpha1::PermissionRule;
+
+/**
+ * PermissionRuleImpl class is designed to hide the implementation which makes direct use
+ * of the protobuf objects underneath from the clients of the caosdb library.
+ */
+class PermissionRuleImpl : public ScalarProtoMessageWrapper<ProtoPermissionRule> {
+public:
+  /**
+   * Constructor. Instanciate a role from the server's responces.
+   */
+  PermissionRuleImpl(ProtoPermissionRule *rule);
+  PermissionRuleImpl();
+  PermissionRuleImpl(std::string permission, bool isGrant, bool isPriority);
+
+  friend class PermissionRule;
+};
+
+} // namespace caosdb::acm
+#endif
diff --git a/src/caosdb/acm/role.cpp b/src/caosdb/acm/role.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..23b919f6249feaa5066f28f85a52d0e937b69d90
--- /dev/null
+++ b/src/caosdb/acm/role.cpp
@@ -0,0 +1,156 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#include "caosdb/acm/role.h"
+#include "caosdb/acm/permission_rule.h"  // for PermissionRule
+#include "caosdb/acm/role_impl.h"        // for RoleImpl
+#include "caosdb/acm/v1alpha1/main.pb.h" // for ListRolesRequest
+#include "caosdb/protobuf_helper.h"      // for ProtoMessageWrapper
+#include <unordered_set>                 // for _Node_iterator, operator!=
+#include <utility>                       // for move
+// IWYU pragma: no_include "net/proto2/public/repeated_field.h"
+
+namespace caosdb::acm {
+using caosdb::utility::ScalarProtoMessageWrapper;
+using ProtoRole = caosdb::acm::v1alpha1::Role;
+using ProtoRolePermissions = caosdb::acm::v1alpha1::RolePermissions;
+using ProtoRoleCapabilities = caosdb::acm::v1alpha1::RoleCapabilities;
+using ProtoListRoleItem = caosdb::acm::v1alpha1::ListRoleItem;
+
+RoleImpl::RoleImpl(const std::string &name, const std::string &description) {
+  if (!name.empty()) {
+    this->wrapped->set_name(name);
+  }
+  if (!description.empty()) {
+    this->wrapped->set_description(description);
+  }
+}
+
+RoleImpl::RoleImpl(ProtoRole *role) : ScalarProtoMessageWrapper<ProtoRole>(role) {}
+
+RoleImpl::RoleImpl(ProtoListRoleItem &role_item)
+  : ScalarProtoMessageWrapper<ProtoRole>(role_item.release_role()) {}
+
+RoleImpl::RoleImpl(const std::string &name) : RoleImpl(name, "") {}
+
+[[nodiscard]] auto RoleImpl::GeneratePermissionRulesSet() -> std::unique_ptr<PermissionRules> {
+  auto result = std::make_unique<PermissionRules>();
+  for (auto const &rule : this->wrapped->permission_rules()) {
+    result->emplace(rule.permission(), rule.grant(), rule.priority());
+  }
+  return result;
+}
+
+Role::Role(std::unique_ptr<RoleImpl> wrapped) : wrapped(std::move(wrapped)) {}
+
+Role::Role(const std::string &name, const std::string &description)
+  : wrapped(std::make_unique<RoleImpl>(name, description)) {}
+
+Role::Role(const std::string &name) : Role(name, {""}) {}
+
+Role::Role(const Role &role) : Role(role.GetName(), role.GetDescription()) {}
+
+auto Role::operator=(const Role &role) -> Role & {
+  if (this == &role) {
+    return *this;
+  }
+  this->wrapped->wrapped->CopyFrom(*(role.wrapped->wrapped));
+
+  return *this;
+}
+
+Role::~Role() = default;
+
+auto Role::GetName() const -> const std::string & { return this->wrapped->wrapped->name(); }
+
+auto Role::SetName(const std::string &name) -> void {
+  if (!name.empty()) {
+    this->wrapped->wrapped->set_name(name);
+  } else {
+    this->wrapped->wrapped->clear_name();
+  }
+}
+
+auto Role::GetDescription() const -> const std::string & {
+  return this->wrapped->wrapped->description();
+}
+
+auto Role::SetDescription(const std::string &description) -> void {
+  if (!description.empty()) {
+    this->wrapped->wrapped->set_description(description);
+  } else {
+    this->wrapped->wrapped->clear_description();
+  }
+}
+
+auto Role::ToString() const -> std::string { return this->wrapped->ToString(); }
+
+[[nodiscard]] auto Role::GetPermissionRules() const -> const PermissionRules & {
+  if (this->wrapped->rules == nullptr) {
+    this->wrapped->rules = this->wrapped->GeneratePermissionRulesSet();
+  }
+  return *(this->wrapped->rules);
+}
+
+auto Role::Grant(const std::string &permission, bool isPriority) -> void {
+  this->AddPermissionRule(permission, true, isPriority);
+}
+
+auto Role::RevokeGrant(const std::string &permission, bool isPriority) -> void {
+  this->RemovePermissionRule(permission, true, isPriority);
+}
+
+auto Role::Deny(const std::string &permission, bool isPriority) -> void {
+  this->AddPermissionRule(permission, false, isPriority);
+}
+
+auto Role::RevokeDenial(const std::string &permission, bool isPriority) -> void {
+  this->RemovePermissionRule(permission, false, isPriority);
+}
+
+auto Role::AddPermissionRule(const std::string &permission, bool isGrant, bool isPriority) -> void {
+  this->RemovePermissionRule(permission, isGrant, isPriority);
+  auto *new_rule = this->wrapped->wrapped->add_permission_rules();
+  new_rule->set_permission(permission);
+  new_rule->set_priority(isPriority);
+  new_rule->set_grant(isGrant);
+}
+
+auto Role::RemovePermissionRule(const std::string &permission, bool isGrant, bool isPriority)
+  -> void {
+  this->wrapped->rules.reset();
+  auto rule = this->wrapped->wrapped->mutable_permission_rules()->begin();
+  while (rule != this->wrapped->wrapped->mutable_permission_rules()->end()) {
+    if (rule->grant() == isGrant && rule->priority() == isPriority &&
+        rule->permission() == permission) {
+      this->wrapped->wrapped->mutable_permission_rules()->erase(rule);
+      rule = this->wrapped->wrapped->mutable_permission_rules()->begin();
+    } else {
+      rule++;
+    }
+  }
+}
+
+auto Role::ClearPermissionRules() -> void {
+  this->wrapped->rules.reset();
+  this->wrapped->wrapped->clear_permission_rules();
+}
+
+} // namespace caosdb::acm
diff --git a/src/caosdb/acm/role_impl.h b/src/caosdb/acm/role_impl.h
new file mode 100644
index 0000000000000000000000000000000000000000..090de2255599c1cba2463e860a94e75cb54e5875
--- /dev/null
+++ b/src/caosdb/acm/role_impl.h
@@ -0,0 +1,66 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+#include "caosdb/acm/role.h"
+#include "caosdb/acm/v1alpha1/main.pb.h" // for ListRolesRequest
+#include "caosdb/protobuf_helper.h"      // for ProtoMessageWrapper
+#include <memory>                        // for unique_ptr
+#include <utility>                       // for move
+
+namespace caosdb::acm {
+using caosdb::utility::ScalarProtoMessageWrapper;
+using ProtoRole = caosdb::acm::v1alpha1::Role;
+using ProtoRolePermissions = caosdb::acm::v1alpha1::RolePermissions;
+using ProtoRoleCapabilities = caosdb::acm::v1alpha1::RoleCapabilities;
+using ProtoListRoleItem = caosdb::acm::v1alpha1::ListRoleItem;
+
+/**
+ * RoleImpl class is designed to hide the implementation which makes direct use
+ * of the protobuf objects underneath from the clients of the caosdb library.
+ */
+class RoleImpl : public ScalarProtoMessageWrapper<ProtoRole> {
+public:
+  /**
+   * Constructor. Instanciate a role with the given name.
+   */
+  explicit RoleImpl(const std::string &name);
+  /**
+   * Constructor. Instanciate a role with the given name and description.
+   */
+  RoleImpl(const std::string &name, const std::string &description);
+  /**
+   * Constructor. Instanciate a role from the server's responces.
+   */
+  RoleImpl(ProtoRole *role);
+
+  /**
+   * Constructor. Instanciate a role from a listRoles RPC.
+   */
+  RoleImpl(ProtoListRoleItem &role);
+
+  friend class Role;
+  friend class caosdb::connection::Connection;
+
+private:
+  std::unique_ptr<PermissionRules> rules;
+  [[nodiscard]] auto GeneratePermissionRulesSet() -> std::unique_ptr<PermissionRules>;
+};
+
+} // namespace caosdb::acm
diff --git a/src/caosdb/connection.cpp b/src/caosdb/connection.cpp
index abc87dacb054a570540eff7e896eb1058b7b267b..b81eff3017ab53c1c13c3245f54a64a14ef6d843 100644
--- a/src/caosdb/connection.cpp
+++ b/src/caosdb/connection.cpp
@@ -21,9 +21,10 @@
  */
 #include "caosdb/connection.h"
 #ifdef BUILD_ACM
+#include "caosdb/acm/role_impl.h"             // for RoleImpl
 #include "caosdb/acm/user_impl.h"             // for UserImpl
 #include "caosdb/acm/v1alpha1/main.grpc.pb.h" // for AccessControlMan...
-#include "caosdb/acm/v1alpha1/main.pb.h"      // for CreateSingleUser...
+#include "caosdb/acm/v1alpha1/main.pb.h"      // for ListRolesRequest
 #endif
 #include "caosdb/configuration.h"            // for ConnectionConfigur...
 #include "caosdb/exceptions.h"               // for ConfigurationError
@@ -41,12 +42,21 @@
 
 namespace caosdb::connection {
 #ifdef BUILD_ACM
+using caosdb::acm::RoleImpl;
 using caosdb::acm::UserImpl;
 using caosdb::acm::v1alpha1::AccessControlManagementService;
+using caosdb::acm::v1alpha1::CreateSingleRoleRequest;
+using caosdb::acm::v1alpha1::CreateSingleRoleResponse;
 using caosdb::acm::v1alpha1::CreateSingleUserRequest;
 using caosdb::acm::v1alpha1::CreateSingleUserResponse;
+using caosdb::acm::v1alpha1::DeleteSingleRoleRequest;
+using caosdb::acm::v1alpha1::DeleteSingleRoleResponse;
 using caosdb::acm::v1alpha1::DeleteSingleUserRequest;
 using caosdb::acm::v1alpha1::DeleteSingleUserResponse;
+using caosdb::acm::v1alpha1::ListRolesRequest;
+using caosdb::acm::v1alpha1::ListRolesResponse;
+using caosdb::acm::v1alpha1::RetrieveSingleRoleRequest;
+using caosdb::acm::v1alpha1::RetrieveSingleRoleResponse;
 using caosdb::acm::v1alpha1::RetrieveSingleUserRequest;
 using caosdb::acm::v1alpha1::RetrieveSingleUserResponse;
 #endif
@@ -118,6 +128,87 @@ auto Connection::RetrieveVersionInfo() const -> const VersionInfo & {
 }
 
 #ifdef BUILD_ACM
+[[nodiscard]] auto Connection::RetrieveSingleRole(const std::string &name) const -> Role {
+  RetrieveSingleRoleRequest request;
+  request.set_name(name);
+  RetrieveSingleRoleResponse response;
+  grpc::ClientContext context;
+  const grpc::Status grpc_status =
+    this->access_controll_management_service->RetrieveSingleRole(&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();
+  auto *role = response.release_role();
+  return Role(std::make_unique<RoleImpl>(role));
+}
+
+auto Connection::DeleteSingleRole(const std::string &name) const -> void {
+  DeleteSingleRoleRequest request;
+  request.set_name(name);
+  DeleteSingleRoleResponse response;
+  grpc::ClientContext context;
+  const grpc::Status grpc_status =
+    this->access_controll_management_service->DeleteSingleRole(&context, request, &response);
+
+  auto status = TransactionStatus::SUCCESS();
+  std::vector<Role> result;
+  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();
+}
+
+auto Connection::CreateSingleRole(const Role &role) const -> void {
+  CreateSingleRoleRequest request;
+  request.set_allocated_role(role.wrapped->wrapped);
+  CreateSingleRoleResponse response;
+  grpc::ClientContext context;
+  const grpc::Status grpc_status =
+    this->access_controll_management_service->CreateSingleRole(&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();
+}
+
 // TODO(tf) find a way to deal with this:
 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
 [[nodiscard]] auto Connection::RetrieveSingleUser(const std::string &realm,
@@ -189,6 +280,33 @@ auto Connection::CreateSingleUser(const User &user) const -> void {
     this->access_controll_management_service->CreateSingleUser(&context, request, &response);
 
   auto status = TransactionStatus::SUCCESS();
+  std::vector<Role> result;
+  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();
+}
+
+[[nodiscard]] auto Connection::ListRoles() const -> std::vector<Role> {
+  const ListRolesRequest request;
+  ListRolesResponse response;
+  grpc::ClientContext context;
+  const grpc::Status grpc_status =
+    this->access_controll_management_service->ListRoles(&context, request, &response);
+
+  auto status = TransactionStatus::SUCCESS();
+  std::vector<Role> result;
   if (!grpc_status.ok()) {
     switch (grpc_status.error_code()) {
     case grpc::StatusCode::UNAUTHENTICATED:
@@ -204,7 +322,14 @@ auto Connection::CreateSingleUser(const User &user) const -> void {
     }
   }
   status.ThrowExceptionIfError();
+
+  auto *roles = response.mutable_roles();
+  for (auto &role_item : *roles) {
+    result.emplace_back(std::make_unique<RoleImpl>(role_item));
+  }
+  return result;
 }
+
 #endif
 
 auto ConnectionManager::mHasConnection(const std::string &name) const -> bool {
diff --git a/src/cxxcaosdbcli.cpp b/src/cxxcaosdbcli.cpp
index e6e5cee66e6c90ab7719434d7667cd6ad20ccbb8..8047050bc2480344db39cf5961228a221562542a 100644
--- a/src/cxxcaosdbcli.cpp
+++ b/src/cxxcaosdbcli.cpp
@@ -21,6 +21,9 @@
  */
 
 // A simple caosdb client
+#ifdef BUILD_ACM
+#include "caosdb/acm/role.h" // for Role
+#endif
 #include "caosdb/connection.h"         // for Connection, ConnectionManager
 #include "caosdb/constants.h"          // for LIBCAOSDB_VERSION_MINOR, LIBCAOSDB_V...
 #include "caosdb/entity.h"             // for Entity
@@ -29,59 +32,153 @@
 #include "caosdb/logging.h"            // for CAOSDB_LOG_TRACE
 #include "caosdb/transaction.h"        // for Transaction, ResultSet
 #include "caosdb/transaction_status.h" // for TransactionSt...
-#include <boost/log/core/record.hpp>   // for record
-#include <boost/log/detail/attachable_sstream_buf.hpp> // for basic_ostring...
-#include <boost/log/sources/record_ostream.hpp>        // for operator<<
-#include <boost/preprocessor/seq/limits/enum_256.hpp>  // for BOOST_PP_SEQ_...
-#include <boost/preprocessor/seq/limits/size_256.hpp>  // for BOOST_PP_SEQ_...
-#include <exception>                                   // for exception
-#include <iostream> // for operator<<, basic_ostream, basic_ost...
-#include <memory>   // for unique_ptr, allocator, __shared_ptr_...
-#include <string>   // for operator<<, char_traits
-
+#include <boost/program_options.hpp>   // for options_description
+#include <exception>                   // for exception
+#include <iostream>                    // for operator<<, basic_ostream, basic_ost...
+#include <memory>                      // for unique_ptr, allocator, __shared_ptr_...
+#include <string>                      // for operator<<, char_traits
+#include <vector>                      // for vector
 const auto logger_name = "libcaosdb";
 
-auto main() -> int {
+using boost::program_options::bool_switch;
+using boost::program_options::command_line_parser;
+using boost::program_options::notify;
+using boost::program_options::options_description;
+using boost::program_options::store;
+using boost::program_options::value;
+using boost::program_options::variables_map;
+#ifdef BUILD_ACM
+using caosdb::acm::Role;
+#endif
 
-  std::cout << "CaosDB C++ client (libcaosdb " << caosdb::LIBCAOSDB_VERSION_MINOR << "."
-            << caosdb::LIBCAOSDB_VERSION_MINOR << "." << caosdb::LIBCAOSDB_VERSION_PATCH << ")\n"
-            << "We don't miss the H of caos.\n"
+auto print_version(bool print) -> void {
+  if (print) {
+    std::cout << "CaosDB C++ client (libcaosdb " << caosdb::LIBCAOSDB_VERSION_MAJOR << "."
+              << caosdb::LIBCAOSDB_VERSION_MINOR << "." << caosdb::LIBCAOSDB_VERSION_PATCH << ")\n"
+              << "We don't miss the H of caos.\n"
+              << std::endl;
+  }
+}
+
+auto test_connection() -> void {
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+
+  connection->RetrieveVersionInfoNoExceptions();
+  // get version info of the server
+  const auto &v_info = connection->GetVersionInfo();
+  std::cout << "Server Version: " << v_info->GetMajor() << "." << v_info->GetMinor() << "."
+            << v_info->GetPatch() << "-" << v_info->GetPreRelease() << "-" << v_info->GetBuild()
             << std::endl;
+}
+
+auto retrieve_entity_by_id(const std::string &id) -> void {
+  std::cout << "Retrieve entity " << id << std::endl;
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  auto transaction(connection->CreateTransaction());
+  transaction->RetrieveById(id);
+  transaction->ExecuteAsynchronously();
+  auto t_stat = transaction->WaitForIt();
+  CAOSDB_LOG_INFO(logger_name) << "status: " << t_stat.GetCode() << " // "
+                               << t_stat.GetDescription();
+  const auto &result_set = transaction->GetResultSet();
+  for (const auto &entity : result_set) {
+    std::cout << entity.ToString() << std::endl;
+  }
+}
+
+auto execute_query(const std::string &query) -> void {
+  std::cout << "Execute query:\n" << query << std::endl;
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  auto q_transaction(connection->CreateTransaction());
+  q_transaction->Query(query);
+  q_transaction->ExecuteAsynchronously();
+  auto t_stat = q_transaction->WaitForIt();
+  CAOSDB_LOG_INFO(logger_name) << "status: " << t_stat.GetCode() << " // "
+                               << t_stat.GetDescription();
+  const auto &result_set_2 = q_transaction->GetResultSet();
+  for (const auto &entity : result_set_2) {
+    std::cout << entity.ToString() << std::endl;
+  }
+}
 
+#ifdef BUILD_ACM
+auto list_roles() -> void {
+  std::cout << "Known roles:" << std::endl;
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  auto roles = connection->ListRoles();
+  for (const auto &role : roles) {
+    std::cout << role.ToString() << std::endl;
+  }
+}
+
+auto retrieve_role(const std::string &name) {
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  auto role = connection->RetrieveSingleRole(name);
+  std::cout << role.ToString() << std::endl;
+}
+
+auto create_role(const std::string &name) {
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  Role role(name);
+  connection->CreateSingleRole(role);
+  std::cout << "OK" << std::endl;
+}
+
+auto delete_role(const std::string &name) {
+  const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+  connection->DeleteSingleRole(name);
+  std::cout << "OK" << std::endl;
+}
+#endif
+
+auto main(int argc, const char *argv[]) -> int {
   try {
-    const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection();
+    command_line_parser parser(argc, argv);
 
-    connection->RetrieveVersionInfoNoExceptions();
-    // get version info of the server
-    const auto &v_info = connection->GetVersionInfo();
-    std::cout << "Server Version: " << v_info->GetMajor() << "." << v_info->GetMinor() << "."
-              << v_info->GetPatch() << "-" << v_info->GetPreRelease() << "-" << v_info->GetBuild()
-              << std::endl;
+    options_description desc{"Options"};
+    desc.add_options()("help,h", "Show help.")("version,V", bool_switch()->notifier(print_version),
+                                               "Print version of libcaosdb.")(
+      "test-connection,t", "Test the connection to the caosdb server.")(
+      "retrieve", value<std::string>()->notifier(retrieve_entity_by_id),
+      "Retrieve an entity by id and print its JSON representation.")(
+      "execute-query", value<std::string>()->notifier(execute_query),
+      "Execute a query and print the results")
+#ifdef BUILD_ACM
+      ("list-roles", "List all known roles")(
+        "retrieve-role", value<std::string>()->notifier(retrieve_role), "Retrieve a role by name")(
+        "create-role", value<std::string>()->notifier(create_role), "Create a new role")(
+        "delete-role", value<std::string>()->notifier(delete_role), "Create a new role")
+#endif
+      ;
 
-    // retrieve an entity
-    auto transaction(connection->CreateTransaction());
-    transaction->RetrieveById("21");
-    transaction->ExecuteAsynchronously();
-    auto t_stat = transaction->WaitForIt();
-    CAOSDB_LOG_INFO(logger_name) << "status: " << t_stat.GetCode() << " // "
-                                 << t_stat.GetDescription();
-    const auto &result_set = transaction->GetResultSet();
-    for (const auto &entity : result_set) {
-      std::cout << entity.ToString() << std::endl;
-    }
+    parser.options(desc);
+
+    // TODO(tf) add positional parameters
+    // positional_options_description pos_desc;
+    // pos_desc.add("phone", 1);
+    // parser.positional(pos_desc);
+
+    variables_map vm;
+    store(parser.run(), vm);
+    notify(vm);
 
-    // execute a query
-    std::string query("FIND Property \"Prop *\"");
-    std::cout << "Trying to execute a query:\n" << query << std::endl;
-    auto q_transaction(connection->CreateTransaction());
-    q_transaction->Query(query);
-    q_transaction->ExecuteAsynchronously();
-    t_stat = q_transaction->WaitForIt();
-    CAOSDB_LOG_INFO(logger_name) << "status: " << t_stat.GetCode() << " // "
-                                 << t_stat.GetDescription();
-    const auto &result_set_2 = q_transaction->GetResultSet();
-    for (const auto &entity : result_set_2) {
-      std::cout << entity.ToString() << std::endl;
+    if (vm.count("help") != 0U) {
+      std::cout << desc << std::endl;
+    } else if (vm["version"].as<bool>()) {
+    } else if (vm.count("test-connection") != 0U) {
+      test_connection();
+#ifdef BUILD_ACM
+    } else if (vm.count("list-roles") != 0U) {
+      list_roles();
+    } else if (vm.count("retrieve-role") != 0U) {
+    } else if (vm.count("retrieve") != 0U) {
+    } else if (vm.count("create-role") != 0U) {
+    } else if (vm.count("delete-role") != 0U) {
+    } else if (vm.count("execute-query") != 0U) {
+#endif
+    } else {
+      print_version(true);
+      test_connection();
     }
 
     return 0;
@@ -92,7 +189,7 @@ auto main() -> int {
     std::cout << "Exception: " << exc.what() << std::endl;
     return 1;
   } catch (...) {
-    std::cout << "Some other exception." << std::endl;
+    std::cout << "Unknown error." << std::endl;
     return 2;
   }
 }
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 659254bf1e215c6c203eb82c0d2bca030a12a0b9..ab517063ccc31828d251e93c55ceafa0a2ed5e78 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -37,6 +37,7 @@ set(test_cases
 
 IF(BUILD_ACM)
     list(APPEND test_cases
+         test_role
          test_user
          )
 ENDIF()
diff --git a/test/test_connection.cpp b/test/test_connection.cpp
index 9b8783a0ae88f6669de4e5ec4e5a43dea26447c5..0426c35d675bc7ddfe20e7d0edeb069e6277f40e 100644
--- a/test/test_connection.cpp
+++ b/test/test_connection.cpp
@@ -117,6 +117,13 @@ TEST_F(test_connection, test_retrieve_single_user) {
                        "The attempt to execute this transaction was not successful because the "
                        "connection to the server could not be established.");
 }
+
+TEST_F(test_connection, test_list_roles) {
+  auto connection = ConnectionManager::GetDefaultConnection();
+  EXPECT_THROW_MESSAGE(auto results = connection->ListRoles(), caosdb::exceptions::ConnectionError,
+                       "The attempt to execute this transaction was not successful because the "
+                       "connection to the server could not be established.");
+}
 #endif
 
 } // namespace caosdb::connection
diff --git a/test/test_role.cpp b/test/test_role.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..dc9a8b825e08a991eb220dd07b589a454cfddb03
--- /dev/null
+++ b/test/test_role.cpp
@@ -0,0 +1,131 @@
+/*
+ *
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2022 Timm Fitschen <t.fitschen@indiscale.com>
+ * Copyright (C) 2022 IndiScale GmbH <info@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "caosdb/acm/role.h"            // for Role
+#include "caosdb/acm/permission_rule.h" // for PermissionRules, PermissionRule
+#include <gtest/gtest-message.h>        // for Message
+#include <gtest/gtest-test-part.h>      // for TestPartResult, SuiteApiRe...
+#include <gtest/gtest_pred_impl.h>      // for Test, EXPECT_EQ, TEST
+#include <string>                       // for allocator, string
+#include <unordered_set>                // for _Node_const_iterator, operat...
+
+namespace caosdb::acm {
+
+TEST(test_role, role_name) {
+  auto role = Role("role1");
+  EXPECT_EQ(role.GetName(), "role1");
+  role.SetName("role2");
+  EXPECT_EQ(role.GetName(), "role2");
+}
+
+TEST(test_role, role_description) {
+  auto role1 = Role("role1", "description1");
+  EXPECT_EQ(role1.GetName(), "role1");
+  EXPECT_EQ(role1.GetDescription(), "description1");
+  role1.SetName("role3");
+  EXPECT_EQ(role1.GetDescription(), "description1");
+
+  auto role2 = Role("role2", "description1");
+  EXPECT_EQ(role2.GetDescription(), "description1");
+  EXPECT_EQ(role2.GetName(), "role2");
+  role2.SetDescription("description2");
+  EXPECT_EQ(role2.GetDescription(), "description2");
+  EXPECT_EQ(role2.GetName(), "role2");
+}
+
+TEST(test_role, grant_revoke) {
+  auto role1 = Role("role1", "description1");
+  EXPECT_TRUE(role1.GetPermissionRules().empty());
+  EXPECT_EQ(role1.ToString(), "{\n \"name\": \"role1\",\n \"description\": \"description1\"\n}\n");
+
+  role1.Grant("SOME_PERMISSION", false);
+  EXPECT_FALSE(role1.GetPermissionRules().empty());
+
+  EXPECT_EQ(role1.ToString(),
+            "{\n \"name\": \"role1\",\n \"description\": \"description1\",\n \"permissionRules\": "
+            "[\n  {\n   \"permission\": \"SOME_PERMISSION\",\n   \"grant\": true\n  }\n ]\n}\n");
+
+  role1.Grant("SOME_OTHER_PERMISSION", true);
+  EXPECT_EQ(role1.GetPermissionRules().size(), 2);
+
+  role1.Grant("SOME_OTHER_PERMISSION", true);
+  // is a set
+  EXPECT_EQ(role1.GetPermissionRules().size(), 2);
+
+  role1.RevokeGrant("SOME_PERMISSION", false);
+  EXPECT_FALSE(role1.GetPermissionRules().empty());
+  role1.RevokeGrant("NOT_EXISTING_PERMISSION", true);
+
+  // is immutable
+  for (const auto &rule : role1.GetPermissionRules()) {
+    EXPECT_EQ(rule.GetPermission(), "SOME_OTHER_PERMISSION");
+  }
+
+  role1.RevokeGrant("SOME_OTHER_PERMISSION", true);
+  EXPECT_TRUE(role1.GetPermissionRules().empty());
+
+  EXPECT_EQ(role1.ToString(), "{\n \"name\": \"role1\",\n \"description\": \"description1\"\n}\n");
+}
+
+TEST(test_role, deny_revoke) {
+  auto role1 = Role("role1", "description1");
+  EXPECT_TRUE(role1.GetPermissionRules().empty());
+  EXPECT_EQ(role1.ToString(), "{\n \"name\": \"role1\",\n \"description\": \"description1\"\n}\n");
+
+  role1.Deny("SOME_PERMISSION", false);
+  EXPECT_FALSE(role1.GetPermissionRules().empty());
+
+  EXPECT_EQ(role1.ToString(),
+            "{\n \"name\": \"role1\",\n \"description\": \"description1\",\n \"permissionRules\": "
+            "[\n  {\n   \"permission\": \"SOME_PERMISSION\"\n  }\n ]\n}\n");
+
+  role1.Deny("SOME_OTHER_PERMISSION", true);
+  EXPECT_EQ(role1.GetPermissionRules().size(), 2);
+
+  role1.Deny("SOME_OTHER_PERMISSION", true);
+  // is a set
+  EXPECT_EQ(role1.GetPermissionRules().size(), 2);
+
+  role1.RevokeDenial("SOME_PERMISSION", false);
+  EXPECT_FALSE(role1.GetPermissionRules().empty());
+  role1.RevokeDenial("NOT_EXISTING_PERMISSION", true);
+
+  // is immutable
+  for (const auto &rule : role1.GetPermissionRules()) {
+    EXPECT_EQ(rule.GetPermission(), "SOME_OTHER_PERMISSION");
+  }
+
+  role1.ClearPermissionRules();
+  EXPECT_TRUE(role1.GetPermissionRules().empty());
+
+  EXPECT_EQ(role1.ToString(), "{\n \"name\": \"role1\",\n \"description\": \"description1\"\n}\n");
+}
+
+TEST(test_role, permission_rule) {
+  auto rule = PermissionRule("SOME_PERMISSION", true, false);
+  EXPECT_EQ(rule.GetPermission(), "SOME_PERMISSION");
+  EXPECT_TRUE(rule.IsGrant());
+  EXPECT_FALSE(rule.IsPriority());
+  EXPECT_EQ(rule.ToString(), "{\n \"permission\": \"SOME_PERMISSION\",\n \"grant\": true\n}\n");
+}
+
+} // namespace caosdb::acm