fix: fix permission check rule resolving
This commit fix the way for resolving the permission check rule. Now it respect the request path instead of randomly picking one.
This commit is contained in:
@@ -187,17 +187,19 @@ public class AuthController {
|
|||||||
log.info("Received forwardAuth request: Method='{}', Uri='{}'", originalMethod,
|
log.info("Received forwardAuth request: Method='{}', Uri='{}'", originalMethod,
|
||||||
originalUriString);
|
originalUriString);
|
||||||
// first detect the client id and service relative path
|
// first detect the client id and service relative path
|
||||||
final var targetServiceClientIdOpt = getTargetServiceClientId(originalUriString);
|
final var optionalRule = getPermissionCheckRuleByUri(originalUriString);
|
||||||
if (targetServiceClientIdOpt.isEmpty()) {
|
if (optionalRule.isEmpty()) {
|
||||||
log.warn("ForwardAuth: No Keycloak client mapping found for URI: {}",
|
log.warn("ForwardAuth: No Keycloak client mapping found for URI: {}",
|
||||||
originalUriString);
|
originalUriString);
|
||||||
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
|
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
|
||||||
}
|
}
|
||||||
final var targetServiceClientId = targetServiceClientIdOpt.get();
|
final var rule = optionalRule.get();
|
||||||
|
|
||||||
|
final var targetServiceClientId = rule.getKeycloakClientId();
|
||||||
final var serviceRelativePath =
|
final var serviceRelativePath =
|
||||||
extractServiceRelativePath(originalUriString, targetServiceClientId);
|
extractServiceRelativePath(originalUriString, rule);
|
||||||
// then check pass-through rule
|
// then check pass-through rule
|
||||||
if (shouldPassThrough(targetServiceClientId, serviceRelativePath)) {
|
if (shouldPassThrough(rule, serviceRelativePath)) {
|
||||||
return Mono.just(ResponseEntity.status(HttpStatus.NO_CONTENT).build());
|
return Mono.just(ResponseEntity.status(HttpStatus.NO_CONTENT).build());
|
||||||
}
|
}
|
||||||
if (authorizationHeader == null) {
|
if (authorizationHeader == null) {
|
||||||
@@ -276,16 +278,15 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldPassThrough(
|
private boolean shouldPassThrough(
|
||||||
final String serviceClientId, final String serviceRelativePath
|
final PermissionMappingConfigProperties.Rule rule, final String serviceRelativePath
|
||||||
) {
|
) {
|
||||||
final var rules = this.permissionMapLookupService
|
final var passthroughs = rule.getPassthroughs();
|
||||||
.getPassThoughRulesByClientId(serviceClientId);
|
|
||||||
try {
|
try {
|
||||||
return rules.stream()
|
return passthroughs.stream()
|
||||||
.anyMatch(it -> executePassThroughRule(it, serviceRelativePath));
|
.anyMatch(it -> executePassThroughRule(it, serviceRelativePath));
|
||||||
} catch (final Throwable throwable) {
|
} catch (final Throwable throwable) {
|
||||||
log.error("Error executing pass-through rule for client {} : {}",
|
log.error("Error executing pass-through rule for client {} : {}",
|
||||||
serviceClientId, throwable.getMessage(), throwable);
|
rule.getKeycloakClientId(), throwable.getMessage(), throwable);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,13 +301,15 @@ public class AuthController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<String> getTargetServiceClientId(String fullUriString) {
|
private Optional<PermissionMappingConfigProperties.Rule> getPermissionCheckRuleByUri(
|
||||||
|
String fullUriString
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
URI uri = new URI(fullUriString);
|
URI uri = new URI(fullUriString);
|
||||||
String path = uri.getPath(); // Get path part, e.g., /cleverswarm/api/jobs/123
|
String path = uri.getPath(); // Get path part, e.g., /cleverswarm/api/jobs/123
|
||||||
log.debug("Mapping URI path: {}", path);
|
log.debug("Mapping URI path: {}", path);
|
||||||
|
|
||||||
final var pathMatch = this.permissionMapLookupService.matchClientIdByPath(path);
|
final var pathMatch = this.permissionMapLookupService.matchRuleByPath(path);
|
||||||
if (pathMatch.isPresent()) {
|
if (pathMatch.isPresent()) {
|
||||||
return pathMatch;
|
return pathMatch;
|
||||||
}
|
}
|
||||||
@@ -315,7 +318,9 @@ public class AuthController {
|
|||||||
log.debug(
|
log.debug(
|
||||||
"No rule matched for path '{}', using default Keycloak Client ID:{}",
|
"No rule matched for path '{}', using default Keycloak Client ID:{}",
|
||||||
path, defaultValue);
|
path, defaultValue);
|
||||||
return Optional.of(defaultValue);
|
return Optional.of(PermissionMappingConfigProperties.Rule.builder()
|
||||||
|
.keycloakClientId(defaultValue)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
log.error("Invalid URI syntax for X-Forwarded-Uri: {}", fullUriString, e);
|
log.error("Invalid URI syntax for X-Forwarded-Uri: {}", fullUriString, e);
|
||||||
@@ -324,17 +329,11 @@ public class AuthController {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractServiceRelativePath(String fullUriString, String mappedKeycloakClientId) {
|
private String extractServiceRelativePath(
|
||||||
// Find the rule that gave us this mappedKeycloakClientId to get its pathPrefix
|
String fullUriString, PermissionMappingConfigProperties.Rule rule
|
||||||
Optional<PermissionMappingConfigProperties.Rule> matchedRule =
|
) {
|
||||||
this.permissionMapLookupService.matchRuleByClientIdAndPath(
|
String pathPrefixToRemove = rule.getPathPrefix();
|
||||||
mappedKeycloakClientId, fullUriString
|
if (rule.getKeycloakClientId()
|
||||||
);
|
|
||||||
|
|
||||||
String pathPrefixToRemove = "";
|
|
||||||
if (matchedRule.isPresent()) {
|
|
||||||
pathPrefixToRemove = matchedRule.get().getPathPrefix();
|
|
||||||
} else if (mappedKeycloakClientId
|
|
||||||
.equals(this.permissionMapLookupService.getDefaultKeycloakClientId())) {
|
.equals(this.permissionMapLookupService.getDefaultKeycloakClientId())) {
|
||||||
// If it's a default mapping, there's no prefix to remove, use the whole path.
|
// If it's a default mapping, there's no prefix to remove, use the whole path.
|
||||||
// Or, you might decide that default mappings always apply to root paths. This
|
// Or, you might decide that default mappings always apply to root paths. This
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.cleverthis.authservice.service;
|
package com.cleverthis.authservice.service;
|
||||||
|
|
||||||
import com.cleverthis.authservice.config.PermissionMappingConfigProperties;
|
import com.cleverthis.authservice.config.PermissionMappingConfigProperties;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,18 +16,5 @@ public interface PermissionMapLookupService {
|
|||||||
/**
|
/**
|
||||||
* Given the path, find the client id of the first rule matching the path prefix.
|
* Given the path, find the client id of the first rule matching the path prefix.
|
||||||
*/
|
*/
|
||||||
Optional<String> matchClientIdByPath(String path);
|
Optional<PermissionMappingConfigProperties.Rule> matchRuleByPath(String path);
|
||||||
|
|
||||||
/**
|
|
||||||
* Given the client id and path, find the rule that both matches the client id exactly,
|
|
||||||
* and match the path prefix.
|
|
||||||
*/
|
|
||||||
Optional<PermissionMappingConfigProperties.Rule> matchRuleByClientIdAndPath(
|
|
||||||
String clientId, String path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the list of pass-through rules by client id.
|
|
||||||
*/
|
|
||||||
List<PermissionMappingConfigProperties.PassThrough> getPassThoughRulesByClientId(
|
|
||||||
String clientId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-26
@@ -5,7 +5,6 @@ import com.cleverthis.authservice.service.PermissionMapLookupService;
|
|||||||
import com.ecwid.consul.v1.ConsulClient;
|
import com.ecwid.consul.v1.ConsulClient;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -102,38 +101,15 @@ public final class FallbackPermissionMapLookupService implements PermissionMapLo
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> matchClientIdByPath(final String path) {
|
public Optional<PermissionMappingConfigProperties.Rule> matchRuleByPath(final String path) {
|
||||||
final var config = getConfig();
|
final var config = getConfig();
|
||||||
for (final var rule : config.getRules()) {
|
for (final var rule : config.getRules()) {
|
||||||
if (path.startsWith(rule.getPathPrefix())) {
|
if (path.startsWith(rule.getPathPrefix())) {
|
||||||
log.debug("Path '{}' matched prefix '{}', using Keycloak Client ID: {}",
|
log.debug("Path '{}' matched prefix '{}', using Keycloak Client ID: {}",
|
||||||
path, rule.getPathPrefix(), rule.getKeycloakClientId());
|
path, rule.getPathPrefix(), rule.getKeycloakClientId());
|
||||||
return Optional.of(rule.getKeycloakClientId());
|
return Optional.of(rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<PermissionMappingConfigProperties.Rule> matchRuleByClientIdAndPath(
|
|
||||||
final String clientId, final String path
|
|
||||||
) {
|
|
||||||
final var config = getConfig();
|
|
||||||
return config.getRules().stream()
|
|
||||||
.filter(rule ->
|
|
||||||
rule.getKeycloakClientId().equals(clientId)
|
|
||||||
&& path.startsWith(rule.getPathPrefix()))
|
|
||||||
.findFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<PermissionMappingConfigProperties.PassThrough> getPassThoughRulesByClientId(
|
|
||||||
final String clientId
|
|
||||||
) {
|
|
||||||
final var config = getConfig();
|
|
||||||
return config.getRules().stream()
|
|
||||||
.filter(rule -> rule.getKeycloakClientId().equals(clientId))
|
|
||||||
.flatMap(rule -> rule.getPassthroughs().stream())
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-33
@@ -55,7 +55,8 @@ class FallbackPermissionMapLookupServiceTest {
|
|||||||
assertNull(service.getConfig()); // fallback is null
|
assertNull(service.getConfig()); // fallback is null
|
||||||
// has json content
|
// has json content
|
||||||
when(value.getDecodedValue())
|
when(value.getDecodedValue())
|
||||||
.thenReturn(this.objectMapper.writeValueAsString(new PermissionMappingConfigProperties()));
|
.thenReturn(
|
||||||
|
this.objectMapper.writeValueAsString(new PermissionMappingConfigProperties()));
|
||||||
service.refreshConsul();
|
service.refreshConsul();
|
||||||
assertEquals(new PermissionMappingConfigProperties(), service.getConfig());
|
assertEquals(new PermissionMappingConfigProperties(), service.getConfig());
|
||||||
}
|
}
|
||||||
@@ -76,45 +77,22 @@ class FallbackPermissionMapLookupServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMatchClientIdByPath() {
|
void testMatchRuleByPath() {
|
||||||
final var defaultConfig = new PermissionMappingConfigProperties();
|
final var defaultConfig = new PermissionMappingConfigProperties();
|
||||||
defaultConfig.setRules(List.of(
|
final var ruleA = PermissionMappingConfigProperties.Rule.builder()
|
||||||
PermissionMappingConfigProperties.Rule.builder()
|
.keycloakClientId("service-A").pathPrefix("/a/").build();
|
||||||
.keycloakClientId("service-A").pathPrefix("/a/").build(),
|
final var ruleB = PermissionMappingConfigProperties.Rule.builder()
|
||||||
PermissionMappingConfigProperties.Rule.builder()
|
.keycloakClientId("service-B").pathPrefix("/b/").build();
|
||||||
.keycloakClientId("service-B").pathPrefix("/b/").build()
|
defaultConfig.setRules(List.of(ruleA, ruleB));
|
||||||
));
|
|
||||||
final var service = new FallbackPermissionMapLookupService(
|
final var service = new FallbackPermissionMapLookupService(
|
||||||
defaultConfig, this.consulClient, this.objectMapper);
|
defaultConfig, this.consulClient, this.objectMapper);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"service-B",
|
ruleB,
|
||||||
service.matchClientIdByPath("/b/items/123").orElseThrow());
|
service.matchRuleByPath("/b/items/123").orElseThrow());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
service.matchClientIdByPath("/c/items/123"));
|
service.matchRuleByPath("/c/items/123"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testMatchRuleByClientIdAndPath() {
|
|
||||||
final var defaultConfig = new PermissionMappingConfigProperties();
|
|
||||||
defaultConfig.setRules(List.of(
|
|
||||||
PermissionMappingConfigProperties.Rule.builder()
|
|
||||||
.keycloakClientId("service-A").pathPrefix("/a/").build(),
|
|
||||||
PermissionMappingConfigProperties.Rule.builder()
|
|
||||||
.keycloakClientId("service-B").pathPrefix("/b/").build()
|
|
||||||
));
|
|
||||||
final var service = new FallbackPermissionMapLookupService(
|
|
||||||
defaultConfig, this.consulClient, this.objectMapper);
|
|
||||||
|
|
||||||
service.matchRuleByClientIdAndPath("service-A", "/a/items/123")
|
|
||||||
.ifPresentOrElse(
|
|
||||||
rule -> assertEquals("service-A", rule.getKeycloakClientId()),
|
|
||||||
() -> Assertions.fail("No rule matched"));
|
|
||||||
service.matchRuleByClientIdAndPath("service-B", "/a/items/123")
|
|
||||||
.ifPresentOrElse(
|
|
||||||
it -> Assertions.fail("Rule matched but should not have"),
|
|
||||||
() -> {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+73
@@ -28,6 +28,7 @@ import org.keycloak.representations.idm.GroupRepresentation;
|
|||||||
import org.keycloak.representations.idm.MemberRepresentation;
|
import org.keycloak.representations.idm.MemberRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
|
||||||
import org.keycloak.representations.idm.OrganizationRepresentation;
|
import org.keycloak.representations.idm.OrganizationRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@@ -558,4 +559,76 @@ class KeycloakIdentityManagerServiceTest {
|
|||||||
|
|
||||||
Mockito.verify(this.adminClient).getGroupById(groupId);
|
Mockito.verify(this.adminClient).getGroupById(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listSubGroups_success() {
|
||||||
|
// Arrange
|
||||||
|
final var parentGroupId = "parent-group-id";
|
||||||
|
final var subGroups = List.of(
|
||||||
|
new GroupRepresentation() {{
|
||||||
|
setId("sub-group-1");
|
||||||
|
setName("SubGroup1");
|
||||||
|
}},
|
||||||
|
new GroupRepresentation() {{
|
||||||
|
setId("sub-group-2");
|
||||||
|
setName("SubGroup2");
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
Mockito.when(this.adminClient.listSubGroups(parentGroupId))
|
||||||
|
.thenReturn(Mono.just(subGroups));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final var result = this.service.listSubGroups(parentGroupId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(groups -> groups.size() == 2 &&
|
||||||
|
"SubGroup1".equals(groups.getFirst().getName()) &&
|
||||||
|
"sub-group-2".equals(groups.get(1).getId()))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
Mockito.verify(this.adminClient).listSubGroups(parentGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getGroupMembers_success() {
|
||||||
|
// Arrange
|
||||||
|
final var groupId = "test-group-id";
|
||||||
|
final var mockMembers = List.of(
|
||||||
|
new UserRepresentation() {{
|
||||||
|
setId("user1");
|
||||||
|
setUsername("user1Username");
|
||||||
|
setFirstName("User1");
|
||||||
|
setLastName("One");
|
||||||
|
setEmail("user1@example.com");
|
||||||
|
setEnabled(true);
|
||||||
|
}},
|
||||||
|
new UserRepresentation() {{
|
||||||
|
setId("user2");
|
||||||
|
setUsername("user2Username");
|
||||||
|
setFirstName("User2");
|
||||||
|
setLastName("Two");
|
||||||
|
setEmail("user2@example.com");
|
||||||
|
setEnabled(true);
|
||||||
|
}}
|
||||||
|
);
|
||||||
|
|
||||||
|
Mockito.when(this.adminClient.getGroupMembers(groupId))
|
||||||
|
.thenReturn(Mono.just(mockMembers));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
final var result = this.service.getGroupMembers(groupId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectNextMatches(members -> members.size() == 2
|
||||||
|
&& "user1".equals(members.getFirst().getId())
|
||||||
|
&& "user1@example.com".equals(members.getFirst().getEmail())
|
||||||
|
&& "user2".equals(members.get(1).getId())
|
||||||
|
&& "user2@example.com".equals(members.get(1).getEmail()))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
Mockito.verify(this.adminClient).getGroupMembers(groupId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user