Repository: OpenAEV-Platform/openaev (java)

Issue:
RBAC: Implement capability hierarchy in the backend
## Context
Capabilities are currently flat, without any hierarchy. This makes it difficult to express relationships between permissions.

## Goal
Allow capabilities to inherit or be structured in a hierarchy in the backend.


Discussion:

Patch:
diff --git a/openbas-api/src/main/java/io/openbas/service/RoleService.java b/openbas-api/src/main/java/io/openbas/service/RoleService.java
--- a/openbas-api/src/main/java/io/openbas/service/RoleService.java
+++ b/openbas-api/src/main/java/io/openbas/service/RoleService.java
@@ -2,6 +2,7 @@
 
 import static io.openbas.utils.pagination.PaginationUtils.buildPaginationJPA;
 
+import com.google.common.annotations.VisibleForTesting;
 import io.openbas.database.model.Capability;
 import io.openbas.database.model.Role;
 import io.openbas.database.repository.RoleRepository;
@@ -35,7 +36,7 @@ public Role createRole(
       @NotBlank final String roleName, @NotNull final Set<Capability> capabilities) {
     Role role = new Role();
     role.setName(roleName);
-    role.setCapabilities(capabilities);
+    role.setCapabilities(getCapabilitiesWithParents(capabilities));
     return roleRepository.save(role);
   }
 
@@ -52,7 +53,7 @@ public Role updateRole(
 
     role.setUpdatedAt(Instant.now());
     role.setName(roleName);
-    role.setCapabilities(capabilities);
+    role.setCapabilities(getCapabilitiesWithParents(capabilities));
 
     return roleRepository.save(role);
   }
@@ -70,4 +71,24 @@ public void deleteRole(@NotBlank final String roleId) {
 
     roleRepository.deleteById(roleId);
   }
+
+  /**
+   * Get a set of capabilities as input and return a set containing the input + their parent
+   *
+   * @param capabilitiesInput
+   * @return
+   */
+  @VisibleForTesting
+  protected Set<Capability> getCapabilitiesWithParents(
+      @NotNull final Set<Capability> capabilitiesInput) {
+    Set<Capability> result = new HashSet<>();
+
+    for (Capability capability : capabilitiesInput) {
+      Capability current = capability;
+      while (current != null && result.add(current)) {
+        current = current.getParent();
+      }
+    }
+    return result;
+  }
 }
diff --git a/openbas-model/src/main/java/io/openbas/database/model/Capability.java b/openbas-model/src/main/java/io/openbas/database/model/Capability.java
--- a/openbas-model/src/main/java/io/openbas/database/model/Capability.java
+++ b/openbas-model/src/main/java/io/openbas/database/model/Capability.java
@@ -7,72 +7,78 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import lombok.Getter;
 
 public enum Capability {
 
   // Superuser
-  BYPASS(pair(null, null)),
+  BYPASS(null, pair(null, null)),
 
   // Atomic Testing
-  ACCESS_ATOMIC_TESTING(pair(ResourceType.ATOMIC_TESTING, Action.READ)),
-  MANAGE_ATOMIC_TESTING(pair(ResourceType.ATOMIC_TESTING, Action.WRITE)),
-  DELETE_ATOMIC_TESTING(pair(ResourceType.ATOMIC_TESTING, Action.DELETE)),
-  LAUNCH_ATOMIC_TESTING(pair(ResourceType.ATOMIC_TESTING, Action.LAUNCH)),
+  ACCESS_ATOMIC_TESTING(null, pair(ResourceType.ATOMIC_TESTING, Action.READ)),
+  MANAGE_ATOMIC_TESTING(ACCESS_ATOMIC_TESTING, pair(ResourceType.ATOMIC_TESTING, Action.WRITE)),
+  DELETE_ATOMIC_TESTING(MANAGE_ATOMIC_TESTING, pair(ResourceType.ATOMIC_TESTING, Action.DELETE)),
+  LAUNCH_ATOMIC_TESTING(MANAGE_ATOMIC_TESTING, pair(ResourceType.ATOMIC_TESTING, Action.LAUNCH)),
 
   // Teams & Players
   MANAGE_TEAMS_AND_PLAYERS(
-      pair(ResourceType.TEAM, Action.WRITE), pair(ResourceType.PLAYER, Action.WRITE)),
+      null, pair(ResourceType.TEAM, Action.WRITE), pair(ResourceType.PLAYER, Action.WRITE)),
   DELETE_TEAMS_AND_PLAYERS(
-      pair(ResourceType.TEAM, Action.DELETE), pair(ResourceType.PLAYER, Action.DELETE)),
+      MANAGE_TEAMS_AND_PLAYERS,
+      pair(ResourceType.TEAM, Action.DELETE),
+      pair(ResourceType.PLAYER, Action.DELETE)),
 
   // Assets (Endpoints, Groups)
-  ACCESS_ASSETS(pair(ResourceType.ASSET, Action.READ)),
-  MANAGE_ASSETS(pair(ResourceType.ASSET, Action.WRITE)),
-  DELETE_ASSETS(pair(ResourceType.ASSET, Action.DELETE)),
+  ACCESS_ASSETS(null, pair(ResourceType.ASSET, Action.READ)),
+  MANAGE_ASSETS(ACCESS_ASSETS, pair(ResourceType.ASSET, Action.WRITE)),
+  DELETE_ASSETS(MANAGE_ASSETS, pair(ResourceType.ASSET, Action.DELETE)),
 
   // Payloads
-  ACCESS_PAYLOADS(pair(ResourceType.PAYLOAD, Action.READ)),
-  MANAGE_PAYLOADS(pair(ResourceType.PAYLOAD, Action.WRITE)),
-  DELETE_PAYLOADS(pair(ResourceType.PAYLOAD, Action.DELETE)),
+  ACCESS_PAYLOADS(null, pair(ResourceType.PAYLOAD, Action.READ)),
+  MANAGE_PAYLOADS(ACCESS_PAYLOADS, pair(ResourceType.PAYLOAD, Action.WRITE)),
+  DELETE_PAYLOADS(MANAGE_PAYLOADS, pair(ResourceType.PAYLOAD, Action.DELETE)),
 
   // Dashboards
-  ACCESS_DASHBOARDS(pair(ResourceType.DASHBOARD, Action.READ)),
-  MANAGE_DASHBOARDS(pair(ResourceType.DASHBOARD, Action.WRITE)),
-  DELETE_DASHBOARDS(pair(ResourceType.DASHBOARD, Action.DELETE)),
+  ACCESS_DASHBOARDS(null, pair(ResourceType.DASHBOARD, Action.READ)),
+  MANAGE_DASHBOARDS(ACCESS_DASHBOARDS, pair(ResourceType.DASHBOARD, Action.WRITE)),
+  DELETE_DASHBOARDS(MANAGE_DASHBOARDS, pair(ResourceType.DASHBOARD, Action.DELETE)),
 
   // Findings
-  ACCESS_FINDINGS(pair(ResourceType.FINDING, Action.READ)),
-  MANAGE_FINDINGS(pair(ResourceType.FINDING, Action.WRITE)),
-  DELETE_FINDINGS(pair(ResourceType.FINDING, Action.DELETE)),
+  ACCESS_FINDINGS(null, pair(ResourceType.FINDING, Action.READ)),
+  MANAGE_FINDINGS(ACCESS_FINDINGS, pair(ResourceType.FINDING, Action.WRITE)),
+  DELETE_FINDINGS(MANAGE_FINDINGS, pair(ResourceType.FINDING, Action.DELETE)),
 
   // Documents
-  ACCESS_DOCUMENTS(pair(ResourceType.DOCUMENT, Action.READ)),
-  MANAGE_DOCUMENTS(pair(ResourceType.DOCUMENT, Action.WRITE)),
-  DELETE_DOCUMENTS(pair(ResourceType.DOCUMENT, Action.DELETE)),
+  ACCESS_DOCUMENTS(null, pair(ResourceType.DOCUMENT, Action.READ)),
+  MANAGE_DOCUMENTS(ACCESS_DOCUMENTS, pair(ResourceType.DOCUMENT, Action.WRITE)),
+  DELETE_DOCUMENTS(MANAGE_DOCUMENTS, pair(ResourceType.DOCUMENT, Action.DELETE)),
 
   // Channels
-  ACCESS_CHANNELS(pair(ResourceType.CHANNEL, Action.READ)),
-  MANAGE_CHANNELS(pair(ResourceType.CHANNEL, Action.WRITE)),
-  DELETE_CHANNELS(pair(ResourceType.CHANNEL, Action.DELETE)),
+  ACCESS_CHANNELS(null, pair(ResourceType.CHANNEL, Action.READ)),
+  MANAGE_CHANNELS(ACCESS_CHANNELS, pair(ResourceType.CHANNEL, Action.WRITE)),
+  DELETE_CHANNELS(MANAGE_CHANNELS, pair(ResourceType.CHANNEL, Action.DELETE)),
 
   // Challenges
-  ACCESS_CHALLENGES(pair(ResourceType.CHALLENGE, Action.READ)),
-  MANAGE_CHALLENGES(pair(ResourceType.CHALLENGE, Action.WRITE)),
-  DELETE_CHALLENGES(pair(ResourceType.CHALLENGE, Action.DELETE)),
+  ACCESS_CHALLENGES(null, pair(ResourceType.CHALLENGE, Action.READ)),
+  MANAGE_CHALLENGES(ACCESS_CHALLENGES, pair(ResourceType.CHALLENGE, Action.WRITE)),
+  DELETE_CHALLENGES(MANAGE_CHALLENGES, pair(ResourceType.CHALLENGE, Action.DELETE)),
 
   // Lessons Learned
-  ACCESS_LESSONS_LEARNED(pair(ResourceType.LESSON_LEARNED, Action.READ)),
-  MANAGE_LESSONS_LEARNED(pair(ResourceType.LESSON_LEARNED, Action.WRITE)),
-  DELETE_LESSONS_LEARNED(pair(ResourceType.LESSON_LEARNED, Action.DELETE)),
+  ACCESS_LESSONS_LEARNED(null, pair(ResourceType.LESSON_LEARNED, Action.READ)),
+  MANAGE_LESSONS_LEARNED(ACCESS_LESSONS_LEARNED, pair(ResourceType.LESSON_LEARNED, Action.WRITE)),
+  DELETE_LESSONS_LEARNED(MANAGE_LESSONS_LEARNED, pair(ResourceType.LESSON_LEARNED, Action.DELETE)),
 
   // Security Platforms
-  ACCESS_SECURITY_PLATFORMS(pair(ResourceType.SECURITY_PLATFORM, Action.READ)),
-  MANAGE_SECURITY_PLATFORMS(pair(ResourceType.SECURITY_PLATFORM, Action.WRITE)),
-  DELETE_SECURITY_PLATFORMS(pair(ResourceType.SECURITY_PLATFORM, Action.DELETE)),
+  ACCESS_SECURITY_PLATFORMS(null, pair(ResourceType.SECURITY_PLATFORM, Action.READ)),
+  MANAGE_SECURITY_PLATFORMS(
+      ACCESS_SECURITY_PLATFORMS, pair(ResourceType.SECURITY_PLATFORM, Action.WRITE)),
+  DELETE_SECURITY_PLATFORMS(
+      MANAGE_SECURITY_PLATFORMS, pair(ResourceType.SECURITY_PLATFORM, Action.DELETE)),
 
   // Platform Settings
-  ACCESS_PLATFORM_SETTINGS(pair(ResourceType.PLATFORM_SETTING, Action.READ)),
-  MANAGE_PLATFORM_SETTINGS(pair(ResourceType.PLATFORM_SETTING, Action.WRITE));
+  ACCESS_PLATFORM_SETTINGS(null, pair(ResourceType.PLATFORM_SETTING, Action.READ)),
+  MANAGE_PLATFORM_SETTINGS(
+      ACCESS_PLATFORM_SETTINGS, pair(ResourceType.PLATFORM_SETTING, Action.WRITE));
 
   private record ResourceTypeActionPair(ResourceType resource, Action action) {}
 
@@ -82,7 +88,10 @@ private static ResourceTypeActionPair pair(ResourceType r, Action a) {
 
   private final Set<ResourceTypeActionPair> pairs;
 
-  Capability(ResourceTypeActionPair... pairs) {
+  @Getter private final Capability parent;
+
+  Capability(Capability parent, ResourceTypeActionPair... pairs) {
+    this.parent = parent;
     this.pairs = Set.of(pairs);
   }
 


Test patch:
diff --git a/openbas-api/src/test/java/io/openbas/service/RoleServiceTest.java b/openbas-api/src/test/java/io/openbas/service/RoleServiceTest.java
--- a/openbas-api/src/test/java/io/openbas/service/RoleServiceTest.java
+++ b/openbas-api/src/test/java/io/openbas/service/RoleServiceTest.java
@@ -1,6 +1,11 @@
 package io.openbas.service;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.openbas.database.model.Capability;
 import io.openbas.database.repository.RoleRepository;
+import java.util.Set;
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
@@ -14,5 +19,19 @@ public class RoleServiceTest {
   @InjectMocks RoleService roleService;
 
   @Test
-  void testFindById() {}
+  void test_getCapabilitiesWithParents_when_inputwithmissingparent_then_should_add_parent() {
+    Set<Capability> input = Set.of(Capability.MANAGE_CHANNELS);
+    Set<Capability> output = roleService.getCapabilitiesWithParents(input);
+    assertEquals(2, output.size());
+    assertTrue(output.contains(Capability.MANAGE_CHANNELS));
+    assertTrue(output.contains(Capability.ACCESS_CHANNELS));
+  }
+
+  @Test
+  void test_getCapabilitiesWithParents_when_inputwithnomissingparent_then_should_return_input() {
+    Set<Capability> input = Set.of(Capability.ACCESS_CHANNELS);
+    Set<Capability> output = roleService.getCapabilitiesWithParents(input);
+    assertEquals(1, output.size());
+    assertTrue(output.contains(Capability.ACCESS_CHANNELS));
+  }
 }