RBAC: Implement generic OAuth2 authority extractor. Resolves #2844
This commit is contained in:
parent
5efb380c42
commit
ae5985eddc
13 changed files with 102 additions and 23 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Reference in a new issue