RBAC: Implement generic OAuth2 authority extractor. Resolves #2844

This commit is contained in:
Roman Zabaluev 2023-04-27 09:30:56 +08:00
parent 5efb380c42
commit ae5985eddc
13 changed files with 102 additions and 23 deletions

View file

@ -2,6 +2,7 @@ package com.provectus.kafka.ui.config.auth;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.Data; import lombok.Data;
@ -14,7 +15,16 @@ public class OAuthProperties {
private Map<String, OAuth2Provider> client = new HashMap<>(); private Map<String, OAuth2Provider> client = new HashMap<>();
@PostConstruct @PostConstruct
public void validate() { public void init() {
getClient().values().forEach((provider) -> {
if (provider.getCustomParams() == null) {
provider.setCustomParams(new HashMap<>());
}
if (provider.getScope() == null) {
provider.setScope(new HashSet<>());
}
});
getClient().values().forEach(this::validateProvider); getClient().values().forEach(this::validateProvider);
} }

View file

@ -73,8 +73,7 @@ public final class OAuthPropertiesConverter {
} }
private static boolean isGoogle(OAuth2Provider provider) { private static boolean isGoogle(OAuth2Provider provider) {
return provider.getCustomParams() != null return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
&& GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
} }
} }

View file

@ -114,17 +114,17 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
@Nullable @Nullable
private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) { private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
final String provider = getProviderByProviderId(providerId); final var provider = getProviderByProviderId(providerId);
Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors() Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
.stream() .stream()
.filter(e -> e.isApplicable(provider)) .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))
.findFirst(); .findFirst();
return extractor.orElse(null); return extractor.orElse(null);
} }
private String getProviderByProviderId(final String providerId) { private OAuthProperties.OAuth2Provider getProviderByProviderId(final String providerId) {
return properties.getClient().get(providerId).getProvider(); return properties.getClient().get(providerId);
} }
} }

View file

@ -46,10 +46,8 @@ public class CognitoLogoutSuccessHandler implements LogoutSuccessHandler {
.fragment(null) .fragment(null)
.build(); .build();
Assert.isTrue( Assert.isTrue(provider.getCustomParams().containsKey("logoutUrl"),
provider.getCustomParams() != null && provider.getCustomParams().containsKey("logoutUrl"), "Custom params should contain 'logoutUrl'");
"Custom params should contain 'logoutUrl'"
);
final var uri = UriComponentsBuilder final var uri = UriComponentsBuilder
.fromUri(URI.create(provider.getCustomParams().get("logoutUrl"))) .fromUri(URI.create(provider.getCustomParams().get("logoutUrl")))
.queryParam("client_id", provider.getClientId()) .queryParam("client_id", provider.getClientId())

View file

@ -10,6 +10,8 @@ public enum Provider {
OAUTH_COGNITO, OAUTH_COGNITO,
OAUTH,
LDAP, LDAP,
LDAP_AD; LDAP_AD;
@ -22,6 +24,8 @@ public enum Provider {
public static String GOOGLE = "google"; public static String GOOGLE = "google";
public static String GITHUB = "github"; public static String GITHUB = "github";
public static String COGNITO = "cognito"; public static String COGNITO = "cognito";
public static String OAUTH = "oauth";
} }
} }

View file

@ -20,6 +20,7 @@ import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;
import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor; import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import java.util.Collections; import java.util.Collections;
@ -71,6 +72,7 @@ public class AccessControlService {
case OAUTH_COGNITO -> new CognitoAuthorityExtractor(); case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
case OAUTH_GOOGLE -> new GoogleAuthorityExtractor(); case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
case OAUTH_GITHUB -> new GithubAuthorityExtractor(); case OAUTH_GITHUB -> new GithubAuthorityExtractor();
case OAUTH -> new OauthAuthorityExtractor();
case LDAP, LDAP_AD -> new LdapAuthorityExtractor(); case LDAP, LDAP_AD -> new LdapAuthorityExtractor();
}).collect(Collectors.toSet())) }).collect(Collectors.toSet()))
.flatMap(Set::stream) .flatMap(Set::stream)

View file

@ -1,5 +1,7 @@
package com.provectus.kafka.ui.service.rbac.extractor; package com.provectus.kafka.ui.service.rbac.extractor;
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.COGNITO;
import com.provectus.kafka.ui.model.rbac.Role; import com.provectus.kafka.ui.model.rbac.Role;
import com.provectus.kafka.ui.model.rbac.provider.Provider; import com.provectus.kafka.ui.model.rbac.provider.Provider;
import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.service.rbac.AccessControlService;
@ -18,8 +20,8 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups"; private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups";
@Override @Override
public boolean isApplicable(String provider) { public boolean isApplicable(String provider, Map<String, String> customParams) {
return Provider.Name.COGNITO.equalsIgnoreCase(provider); return COGNITO.equalsIgnoreCase(provider) || COGNITO.equalsIgnoreCase(customParams.get(TYPE));
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package com.provectus.kafka.ui.service.rbac.extractor; package com.provectus.kafka.ui.service.rbac.extractor;
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GITHUB;
import com.provectus.kafka.ui.model.rbac.Role; import com.provectus.kafka.ui.model.rbac.Role;
import com.provectus.kafka.ui.model.rbac.provider.Provider; import com.provectus.kafka.ui.model.rbac.provider.Provider;
import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.service.rbac.AccessControlService;
@ -28,8 +30,8 @@ public class GithubAuthorityExtractor implements ProviderAuthorityExtractor {
private static final String DUMMY = "dummy"; private static final String DUMMY = "dummy";
@Override @Override
public boolean isApplicable(String provider) { public boolean isApplicable(String provider, Map<String, String> customParams) {
return Provider.Name.GITHUB.equalsIgnoreCase(provider); return GITHUB.equalsIgnoreCase(provider) || GITHUB.equalsIgnoreCase(customParams.get(TYPE));
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package com.provectus.kafka.ui.service.rbac.extractor; package com.provectus.kafka.ui.service.rbac.extractor;
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GOOGLE;
import com.provectus.kafka.ui.model.rbac.Role; import com.provectus.kafka.ui.model.rbac.Role;
import com.provectus.kafka.ui.model.rbac.provider.Provider; import com.provectus.kafka.ui.model.rbac.provider.Provider;
import com.provectus.kafka.ui.service.rbac.AccessControlService; import com.provectus.kafka.ui.service.rbac.AccessControlService;
@ -19,8 +21,8 @@ public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
public static final String EMAIL_ATTRIBUTE_NAME = "email"; public static final String EMAIL_ATTRIBUTE_NAME = "email";
@Override @Override
public boolean isApplicable(String provider) { public boolean isApplicable(String provider, Map<String, String> customParams) {
return Provider.Name.GOOGLE.equalsIgnoreCase(provider); return GOOGLE.equalsIgnoreCase(provider) || GOOGLE.equalsIgnoreCase(customParams.get(TYPE));
} }
@Override @Override

View file

@ -11,7 +11,7 @@ import reactor.core.publisher.Mono;
public class LdapAuthorityExtractor implements ProviderAuthorityExtractor { public class LdapAuthorityExtractor implements ProviderAuthorityExtractor {
@Override @Override
public boolean isApplicable(String provider) { public boolean isApplicable(String provider, Map<String, String> params) {
return false; // TODO #2752 return false; // TODO #2752
} }

View file

@ -1,8 +1,19 @@
package com.provectus.kafka.ui.service.rbac.extractor; package com.provectus.kafka.ui.service.rbac.extractor;
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.OAUTH;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
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 com.provectus.kafka.ui.service.rbac.AccessControlService;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -11,12 +22,14 @@ import reactor.core.publisher.Mono;
public class OauthAuthorityExtractor implements ProviderAuthorityExtractor { public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
@Override @Override
public boolean isApplicable(String provider) { public boolean isApplicable(String provider, Map<String, String> customParams) {
return false; // TODO #2844 return OAUTH.equalsIgnoreCase(provider) || OAUTH.equalsIgnoreCase(customParams.get(TYPE));
} }
@Override @Override
public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) { public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
log.debug("Extracting OAuth2 user authorities");
DefaultOAuth2User principal; DefaultOAuth2User principal;
try { try {
principal = (DefaultOAuth2User) value; principal = (DefaultOAuth2User) value;
@ -25,7 +38,52 @@ public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
throw new RuntimeException(); throw new RuntimeException();
} }
return Mono.just(Set.of(principal.getName())); // TODO #2844 Set<String> groupsByUsername = acs.getRoles()
.stream()
.filter(r -> r.getSubjects()
.stream()
.filter(s -> s.getProvider().equals(Provider.OAUTH))
.filter(s -> s.getType().equals("user"))
.anyMatch(s -> s.getValue().equals(principal.getName())))
.map(Role::getName)
.collect(Collectors.toSet());
Set<String> groupsByGroupField = acs.getRoles()
.stream()
.filter(role -> role.getSubjects()
.stream()
.filter(s -> s.getProvider().equals(Provider.OAUTH))
.filter(s -> s.getType().equals("groupsfield"))
.anyMatch(subject -> convertGroups(principal.getAttribute(subject.getValue())).contains(role.getName()))
)
//subject.getValue()
.map(Role::getName)
.collect(Collectors.toSet());
return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByGroupField.stream()).collect(Collectors.toSet()));
}
@SuppressWarnings("unchecked")
private Collection<String> convertGroups(Object groups) {
try {
if ((groups instanceof List<?>) || (groups instanceof Set<?>)) {
log.trace("The field is either a set or a list, returning as is");
return (Collection<String>) groups;
}
if (!(groups instanceof String)) {
log.debug("The field is not a string, skipping");
return Collections.emptySet();
}
log.trace("Trying to deserialize the field");
//@formatter:off
return new ObjectMapper().readValue((String) groups, new TypeReference<>() {});
//@formatter:on
} catch (Exception e) {
log.error("Error deserializing field", e);
return Collections.emptySet();
}
} }
} }

View file

@ -7,7 +7,9 @@ import reactor.core.publisher.Mono;
public interface ProviderAuthorityExtractor { public interface ProviderAuthorityExtractor {
boolean isApplicable(String provider); String TYPE = "type";
boolean isApplicable(String provider, Map<String, String> customParams);
Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams); Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams);

View file

@ -224,7 +224,7 @@ public class DynamicConfigOperations {
Optional.ofNullable(auth) Optional.ofNullable(auth)
.flatMap(a -> Optional.ofNullable(a.oauth2)) .flatMap(a -> Optional.ofNullable(a.oauth2))
.ifPresent(OAuthProperties::validate); .ifPresent(OAuthProperties::init);
Optional.ofNullable(webclient) Optional.ofNullable(webclient)
.ifPresent(WebclientProperties::validate); .ifPresent(WebclientProperties::validate);