From 150fc21fb84770520e669f8681b5c9a2bf3ba344 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Mon, 7 Aug 2023 22:01:39 +0700 Subject: [PATCH] RBAC: Implement roles by github teams (#4093) Co-authored-by: Ilya Kuramshin --- .../extractor/GithubAuthorityExtractor.java | 118 ++++++++++++++---- 1 file changed, 93 insertions(+), 25 deletions(-) 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> extract(AccessControlService acs, Object value, Map groupsByUsername = new HashSet<>(); + Set 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> extract(AccessControlService acs, Object value, Map 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> extract(AccessControlService acs, Object value, Map> rolesByOrganization = getOrganizationRoles(principal, additionalParams, acs, webClient); + Mono> 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> getOrganizationRoles(DefaultOAuth2User principal, Map 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>> userOrganizations = webClient .get() @@ -99,22 +113,76 @@ public Mono> extract(AccessControlService acs, Object value, Map { - 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> getTeamRoles(WebClient webClient, Map 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>> 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> mappedTeams = rawTeams + .map(teams -> teams.stream() + .map(teamInfo -> { + var name = teamInfo.get(TEAM_NAME); + var orgInfo = (Map) 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())); } }