Repository: provectus/kafka-ui
Issue: RBAC: Support github teams
Labels: type/enhancement
scope/backend
status/accepted
area/rbac

Currently as of #753 we do support only usernames/organizations roles.

Please upvote if you're interested in roles via gh teams.

```
        - provider: oauth_github
          type: team
          value: "provectus/kafka-backend"
```

Issue comments:
Would be really nice to have
It would be amazing!
Feel free to test the things out:
image: `public.ecr.aws/provectus/kafka-ui-custom-build:4093`
Config example:
```
        - provider: oauth_github
          type: team
          value: "provectus/kafka-backend"
```


Pull request review comments:
ORGANIZATION constant?
is it really "login"
please write type here for better readability
distinct not useful here
rm .distinct()
Yes it is!

Patch that resolved the issue:
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java
index 654654a05dd..90c4ceebc60 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java
@@ -5,6 +5,8 @@
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +28,8 @@ public class GithubAuthorityExtractor implements ProviderAuthorityExtractor {
   private static final String ORGANIZATION_ATTRIBUTE_NAME = "organizations_url";
   private static final String USERNAME_ATTRIBUTE_NAME = "login";
   private static final String ORGANIZATION_NAME = "login";
+  private static final String ORGANIZATION = "organization";
+  private static final String TEAM_NAME = "slug";
   private static final String GITHUB_ACCEPT_HEADER = "application/vnd.github+json";
   private static final String DUMMY = "dummy";
   // The number of results (max 100) per page of list organizations for authenticated user.
@@ -46,7 +50,7 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
       throw new RuntimeException();
     }
 
-    Set<String> groupsByUsername = new HashSet<>();
+    Set<String> rolesByUsername = new HashSet<>();
     String username = principal.getAttribute(USERNAME_ATTRIBUTE_NAME);
     if (username == null) {
       log.debug("Github username param is not present");
@@ -59,13 +63,7 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
               .filter(s -> s.getType().equals("user"))
               .anyMatch(s -> s.getValue().equals(username)))
           .map(Role::getName)
-          .forEach(groupsByUsername::add);
-    }
-
-    String organization = principal.getAttribute(ORGANIZATION_ATTRIBUTE_NAME);
-    if (organization == null) {
-      log.debug("Github organization param is not present");
-      return Mono.just(groupsByUsername);
+          .forEach(rolesByUsername::add);
     }
 
     OAuth2UserRequest req = (OAuth2UserRequest) additionalParams.get("request");
@@ -80,8 +78,24 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
           .getUserInfoEndpoint()
           .getUri();
     }
+    var webClient = WebClient.create(infoEndpoint);
 
-    WebClient webClient = WebClient.create(infoEndpoint);
+    Mono<Set<String>> rolesByOrganization = getOrganizationRoles(principal, additionalParams, acs, webClient);
+    Mono<Set<String>> rolesByTeams = getTeamRoles(webClient, additionalParams, acs);
+
+    return Mono.zip(rolesByOrganization, rolesByTeams)
+        .map((t) -> Stream.of(t.getT1(), t.getT2(), rolesByUsername)
+            .flatMap(Collection::stream)
+            .collect(Collectors.toSet()));
+  }
+
+  private Mono<Set<String>> getOrganizationRoles(DefaultOAuth2User principal, Map<String, Object> additionalParams,
+                                                 AccessControlService acs, WebClient webClient) {
+    String organization = principal.getAttribute(ORGANIZATION_ATTRIBUTE_NAME);
+    if (organization == null) {
+      log.debug("Github organization param is not present");
+      return Mono.just(Collections.emptySet());
+    }
 
     final Mono<List<Map<String, Object>>> userOrganizations = webClient
         .get()
@@ -99,22 +113,76 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
     //@formatter:on
 
     return userOrganizations
-        .map(orgsMap -> {
-          var groupsByOrg = acs.getRoles()
-              .stream()
-              .filter(role -> role.getSubjects()
-                  .stream()
-                  .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
-                  .filter(s -> s.getType().equals("organization"))
-                  .anyMatch(subject -> orgsMap.stream()
-                      .map(org -> org.get(ORGANIZATION_NAME).toString())
-                      .distinct()
-                      .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue()))
-                  ))
-              .map(Role::getName);
-
-          return Stream.concat(groupsByOrg, groupsByUsername.stream()).collect(Collectors.toSet());
-        });
+        .map(orgsMap -> acs.getRoles()
+            .stream()
+            .filter(role -> role.getSubjects()
+                .stream()
+                .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
+                .filter(s -> s.getType().equals(ORGANIZATION))
+                .anyMatch(subject -> orgsMap.stream()
+                    .map(org -> org.get(ORGANIZATION_NAME).toString())
+                    .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue()))
+                ))
+            .map(Role::getName)
+            .collect(Collectors.toSet()));
+  }
+
+  @SuppressWarnings("unchecked")
+  private Mono<Set<String>> getTeamRoles(WebClient webClient, Map<String, Object> additionalParams,
+                                         AccessControlService acs) {
+
+    var requestedTeams = acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
+            .anyMatch(s -> s.getType().equals("team")))
+        .collect(Collectors.toSet());
+
+    if (requestedTeams.isEmpty()) {
+      log.debug("No roles with github teams found, skipping");
+      return Mono.just(Collections.emptySet());
+    }
+
+    final Mono<List<Map<String, Object>>> rawTeams = webClient
+        .get()
+        .uri(uriBuilder -> uriBuilder.path("/teams")
+            .queryParam("per_page", ORGANIZATIONS_PER_PAGE)
+            .build())
+        .headers(headers -> {
+          headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER);
+          OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get("request");
+          headers.setBearerAuth(request.getAccessToken().getTokenValue());
+        })
+        .retrieve()
+        //@formatter:off
+        .bodyToMono(new ParameterizedTypeReference<>() {});
+    //@formatter:on
+
+    final Mono<List<String>> mappedTeams = rawTeams
+        .map(teams -> teams.stream()
+            .map(teamInfo -> {
+              var name = teamInfo.get(TEAM_NAME);
+              var orgInfo = (Map<String, Object>) teamInfo.get(ORGANIZATION);
+              var orgName = orgInfo.get(ORGANIZATION_NAME);
+              return orgName + "/" + name;
+            })
+            .map(Object::toString)
+            .collect(Collectors.toList())
+        );
+
+    return mappedTeams
+        .map(teams -> acs.getRoles()
+            .stream()
+            .filter(role -> role.getSubjects()
+                .stream()
+                .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
+                .filter(s -> s.getType().equals("team"))
+                .anyMatch(subject -> teams.stream()
+                    .anyMatch(teamName -> teamName.equalsIgnoreCase(subject.getValue()))
+                ))
+            .map(Role::getName)
+            .collect(Collectors.toSet()));
   }
 
 }


Test patch: