Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve JWT format with namespaces grouping #500

Merged
merged 5 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ The delivered JWT token will have the following format:
{
"roleBindings": [
{
"namespace": "myNamespace",
"namespaces": ["myNamespace"],
"verbs": [
"GET",
"POST",
Expand All @@ -165,16 +165,14 @@ The delivered JWT token will have the following format:
"schemas",
"schemas/config",
"topics",
"topics/import",
"topics/delete-records",
"connectors",
"connectors/import",
"connectors/change-state",
"connect-clusters",
"connect-clusters/vaults",
"acls",
"consumer-groups/reset",
"streams"
"streams",
"connect-clusters",
"connect-clusters/vaults"
]
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,12 @@ public SecurityRuleResult checkSecurity(HttpRequest<?> request, @Nullable Authen

AuthenticationInfo authenticationInfo = AuthenticationInfo.of(authentication);

// No role binding for the target namespace. User is targeting a namespace that he is not allowed to access
// No role binding for the target namespace: the user is not allowed to access the target namespace
List<AuthenticationRoleBinding> namespaceRoleBindings = authenticationInfo.getRoleBindings()
.stream()
.filter(roleBinding -> roleBinding.getNamespace().equals(namespace))
.filter(roleBinding -> roleBinding.getNamespaces()
.stream()
.anyMatch(ns -> ns.equals(namespace)))
.toList();

if (namespaceRoleBindings.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationRoleBinding {
private String namespace;
private List<String> namespaces;
private List<RoleBinding.Verb> verbs;
private List<String> resourceTypes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -23,7 +24,6 @@
@Singleton
public class AuthenticationService {


@Inject
ResourceBasedSecurityRule resourceBasedSecurityRule;

Expand All @@ -50,10 +50,18 @@ public AuthenticationResponse buildAuthJwtGroups(String username, List<String> g
return AuthenticationResponse.success(username, resourceBasedSecurityRule.computeRolesFromGroups(groups),
Map.of(ROLE_BINDINGS, roleBindings
.stream()
.map(roleBinding -> AuthenticationRoleBinding.builder()
.namespace(roleBinding.getMetadata().getNamespace())
.verbs(new ArrayList<>(roleBinding.getSpec().getRole().getVerbs()))
.resourceTypes(new ArrayList<>(roleBinding.getSpec().getRole().getResourceTypes()))
// group the namespaces by roles in a mapping
.collect(Collectors.groupingBy(
roleBinding -> roleBinding.getSpec().getRole(),
Collectors.mapping(roleBinding -> roleBinding.getMetadata().getNamespace(), Collectors.toList())
))
// build JWT with a list of namespaces for each different role
.entrySet()
.stream()
.map(entry -> AuthenticationRoleBinding.builder()
.namespaces(entry.getValue())
.verbs(new ArrayList<>(entry.getKey().getVerbs()))
.resourceTypes(new ArrayList<>(entry.getKey().getResourceTypes()))
.build())
.toList()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

@ExtendWith(MockitoExtension.class)
class ResourceBasedSecurityRuleTest {
private static final String NAMESPACE = "namespace";
private static final String NAMESPACES = "namespaces";
private static final String VERBS = "verbs";
private static final String RESOURCE_TYPES = "resourceTypes";

Expand Down Expand Up @@ -133,7 +133,7 @@ void checkReturnsAllowedNamespaceAsAdmin() {
"topics,/api/namespaces/test/topics/topic.with.dots"})
void shouldReturnAllowedWhenHyphenAndDotResourcesAndHandleRoleBindingsType(String resourceType, String path) {
List<Map<String, ?>> jwtRoleBindings = List.of(
Map.of(NAMESPACE, "test",
Map.of(NAMESPACES, List.of("test"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of(resourceType)));

Expand All @@ -148,7 +148,7 @@ void shouldReturnAllowedWhenHyphenAndDotResourcesAndHandleRoleBindingsType(Strin

List<AuthenticationRoleBinding> basicAuthRoleBindings = List.of(
AuthenticationRoleBinding.builder()
.namespace("test")
.namespaces(List.of("test"))
.verbs(List.of(GET))
.resourceTypes(List.of(resourceType))
.build());
Expand All @@ -167,7 +167,7 @@ void shouldReturnAllowedWhenHyphenAndDotResourcesAndHandleRoleBindingsType(Strin
@Test
void shouldReturnAllowedWhenSubResource() {
List<Map<String, ?>> jwtRoleBindings = List.of(
Map.of(NAMESPACE, "test",
Map.of(NAMESPACES, List.of("test"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("connectors/restart", "topics/delete-records")));

Expand All @@ -192,7 +192,7 @@ void shouldReturnAllowedWhenSubResource() {
@CsvSource({"namespace", "name-space", "name.space", "_name_space_", "namespace123"})
void shouldReturnAllowedWhenSpecialNamespaceName(String namespace) {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACE, namespace,
Map.of(NAMESPACES, List.of(namespace),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("topics")));

Expand All @@ -207,6 +207,45 @@ void shouldReturnAllowedWhenSpecialNamespaceName(String namespace) {
assertEquals(SecurityRuleResult.ALLOWED, actual);
}

@Test
void shouldReturnAllowedWhenMultipleNamespaces() {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACES, List.of("ns1", "ns2", "ns3"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("topics")));

Map<String, Object> claims = Map.of(SUBJECT, "user", ROLES, List.of(), ROLE_BINDINGS, roleBindings);
Authentication auth = Authentication.build("user", claims);

when(namespaceRepository.findByName("ns3"))
.thenReturn(Optional.of(Namespace.builder().build()));

SecurityRuleResult actual =
resourceBasedSecurityRule.checkSecurity(HttpRequest.GET("/api/namespaces/ns3/topics"), auth);
assertEquals(SecurityRuleResult.ALLOWED, actual);
}

@Test
void shouldReturnAllowedWhenMultipleVerbsResourceTypesCombinations() {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACES, List.of("ns1"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("topics")),
Map.of(NAMESPACES, List.of("ns2"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("connectors")));

Map<String, Object> claims = Map.of(SUBJECT, "user", ROLES, List.of(), ROLE_BINDINGS, roleBindings);
Authentication auth = Authentication.build("user", claims);

when(namespaceRepository.findByName("ns2"))
.thenReturn(Optional.of(Namespace.builder().build()));

SecurityRuleResult actual =
resourceBasedSecurityRule.checkSecurity(HttpRequest.GET("/api/namespaces/ns2/connectors"), auth);
assertEquals(SecurityRuleResult.ALLOWED, actual);
}

@Test
void shouldReturnForbiddenNamespaceWhenNoRoleBinding() {
Map<String, Object> claims = Map.of(SUBJECT, "user", ROLES, List.of(), ROLE_BINDINGS, List.of());
Expand All @@ -226,7 +265,7 @@ void shouldReturnForbiddenNamespaceWhenNoRoleBinding() {
@Test
void shouldReturnForbiddenNamespaceWhenNoRoleBindingMatchingRequestedNamespace() {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACE, "namespace",
Map.of(NAMESPACES, List.of("namespace"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("connectors")));

Expand All @@ -247,7 +286,7 @@ void shouldReturnForbiddenNamespaceWhenNoRoleBindingMatchingRequestedNamespace()
@Test
void checkReturnsUnknownSubResource() {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACE, "test",
Map.of(NAMESPACES, List.of("test"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("connectors")));

Expand All @@ -266,7 +305,7 @@ void checkReturnsUnknownSubResource() {
@Test
void checkReturnsUnknownSubResourceWithDot() {
List<Map<String, ?>> roleBindings = List.of(
Map.of(NAMESPACE, "test",
Map.of(NAMESPACES, List.of("test"),
VERBS, List.of(GET),
RESOURCE_TYPES, List.of("connectors")));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class AuthenticationInfoTest {
@Test
void shouldConvertFromMapRoleBindingsType() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(ROLE_BINDINGS, List.of(Map.of("namespace", "namespace",
attributes.put(ROLE_BINDINGS, List.of(Map.of("namespaces", List.of("namespace"),
"verbs", List.of("GET"),
"resourceTypes", List.of("topics"))));
Authentication authentication = Authentication.build("name", List.of("role"), attributes);
Expand All @@ -28,22 +28,22 @@ void shouldConvertFromMapRoleBindingsType() {

assertEquals("name", authenticationInfo.getName());
assertEquals(List.of("role"), authenticationInfo.getRoles().stream().toList());
assertIterableEquals(List.of(new AuthenticationRoleBinding("namespace", List.of(GET), List.of("topics"))),
authenticationInfo.getRoleBindings().stream().toList());
assertIterableEquals(List.of(new AuthenticationRoleBinding(List.of("namespace"), List.of(GET),
List.of("topics"))), authenticationInfo.getRoleBindings().stream().toList());
}

@Test
void shouldConvertFromAuthenticationRoleBindingsType() {
Map<String, Object> attributes = new HashMap<>();
attributes.put(ROLE_BINDINGS, List.of(new AuthenticationRoleBinding("namespace", List.of(GET),
attributes.put(ROLE_BINDINGS, List.of(new AuthenticationRoleBinding(List.of("namespace"), List.of(GET),
List.of("topics"))));
Authentication authentication = Authentication.build("name", List.of("role"), attributes);

AuthenticationInfo authenticationInfo = AuthenticationInfo.of(authentication);

assertEquals("name", authenticationInfo.getName());
assertEquals(List.of("role"), authenticationInfo.getRoles().stream().toList());
assertIterableEquals(List.of(new AuthenticationRoleBinding("namespace", List.of(GET), List.of("topics"))),
authenticationInfo.getRoleBindings().stream().toList());
assertIterableEquals(List.of(new AuthenticationRoleBinding(List.of("namespace"), List.of(GET),
List.of("topics"))), authenticationInfo.getRoleBindings().stream().toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,19 @@ void shouldReturnAuthenticationSuccessWhenAdminWithGroups() {
assertEquals("admin", response.getAuthentication().get().getName());
assertTrue(response.getAuthentication().get().getRoles().contains(ResourceBasedSecurityRule.IS_ADMIN));
assertTrue(response.getAuthentication().get().getAttributes()
.containsKey("roleBindings"));
assertEquals("ns1",
.containsKey(ROLE_BINDINGS));
assertEquals(List.of("ns1"),
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.getNamespace());
.get(ROLE_BINDINGS)).getFirst()
.getNamespaces());
assertTrue(
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.get(ROLE_BINDINGS)).getFirst()
.getVerbs()
.containsAll(List.of(RoleBinding.Verb.POST, RoleBinding.Verb.GET)));
assertTrue(
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.get(ROLE_BINDINGS)).getFirst()
.getResourceTypes()
.containsAll(List.of("topics", "acls")));
}
Expand Down Expand Up @@ -155,20 +155,105 @@ void shouldReturnAuthenticationSuccessWhenUserWithGroups() {
assertEquals("user", response.getAuthentication().get().getName());
assertTrue(response.getAuthentication().get().getRoles().isEmpty());
assertTrue(response.getAuthentication().get().getAttributes()
.containsKey("roleBindings"));
assertEquals("ns1",
.containsKey(ROLE_BINDINGS));
assertEquals(List.of("ns1"),
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.getNamespace());
.get(ROLE_BINDINGS)).getFirst()
.getNamespaces());
assertTrue(
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.get(ROLE_BINDINGS)).getFirst()
.getVerbs()
.containsAll(List.of(RoleBinding.Verb.POST, RoleBinding.Verb.GET)));
assertTrue(
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes()
.get("roleBindings")).getFirst()
.get(ROLE_BINDINGS)).getFirst()
.getResourceTypes()
.containsAll(List.of("topics", "acls")));
}

@Test
@SuppressWarnings("unchecked")
void shouldReturnAuthenticationSuccessWhenMultipleGroupsWithSameVerbsAndResourceTypes() {
RoleBinding roleBinding1 = RoleBinding.builder()
.metadata(Metadata.builder()
.name("ns1-rb")
.namespace("ns1")
.build())
.spec(RoleBinding.RoleBindingSpec.builder()
.role(RoleBinding.Role.builder()
.resourceTypes(List.of("topics", "acls"))
.verbs(List.of(RoleBinding.Verb.POST, RoleBinding.Verb.GET))
.build())
.subject(RoleBinding.Subject.builder()
.subjectName("group1")
.subjectType(RoleBinding.SubjectType.GROUP)
.build())
.build())
.build();

RoleBinding roleBinding2 = RoleBinding.builder()
.metadata(Metadata.builder()
.name("ns2-rb")
.namespace("ns2")
.build())
.spec(RoleBinding.RoleBindingSpec.builder()
.role(RoleBinding.Role.builder()
.resourceTypes(List.of("topics"))
.verbs(List.of(RoleBinding.Verb.GET))
.build())
.subject(RoleBinding.Subject.builder()
.subjectName("group2")
.subjectType(RoleBinding.SubjectType.GROUP)
.build())
.build())
.build();

RoleBinding roleBinding3 = RoleBinding.builder()
.metadata(Metadata.builder()
.name("ns3-rb")
.namespace("ns3")
.build())
.spec(RoleBinding.RoleBindingSpec.builder()
.role(RoleBinding.Role.builder()
.resourceTypes(List.of("topics", "acls"))
.verbs(List.of(RoleBinding.Verb.POST, RoleBinding.Verb.GET))
.build())
.subject(RoleBinding.Subject.builder()
.subjectName("group3")
.subjectType(RoleBinding.SubjectType.GROUP)
.build())
.build())
.build();

when(roleBindingService.findAllByGroups(any()))
.thenReturn(List.of(roleBinding1, roleBinding2, roleBinding3));

when(resourceBasedSecurityRule.computeRolesFromGroups(any()))
.thenReturn(List.of());

AuthenticationResponse response = authenticationService.buildAuthJwtGroups("user", List.of("group1"));

assertTrue(response.getAuthentication().isPresent());
assertEquals("user", response.getAuthentication().get().getName());
assertTrue(response.getAuthentication().get().getRoles().isEmpty());
assertTrue(response.getAuthentication().get().getAttributes().containsKey(ROLE_BINDINGS));
assertTrue(
((List<AuthenticationRoleBinding>) response.getAuthentication().get().getAttributes().get(ROLE_BINDINGS))
.containsAll(
List.of(
AuthenticationRoleBinding.builder()
.namespaces(List.of("ns1", "ns3"))
.verbs(List.of(RoleBinding.Verb.POST, RoleBinding.Verb.GET))
.resourceTypes(List.of("topics", "acls"))
.build(),
AuthenticationRoleBinding.builder()
.namespaces(List.of("ns2"))
.verbs(List.of(RoleBinding.Verb.GET))
.resourceTypes(List.of("topics"))
.build()
)
)
);
}
}
Loading
Loading