8000 AuthZEN allow configurable subject lookup semantics by ryanemerson · Pull Request #48635 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
F438
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.NotAuthorizedException;
import jakarta.ws.rs.POST;
Expand All @@ -47,13 +49,19 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.util.JsonSerialization;

public class AuthZenResource {

private static final Response DECISION_FALSE = jakarta.ws.rs.core.Response.ok(new AuthZen.EvaluationResponse(false)).build();
private static final Response DECISION_FALSE = Response.ok(new AuthZen.EvaluationResponse(false)).build();
private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$");

private static final String NAMESPACE_ID = "id:";
private static final String NAMESPACE_USERNAME = "username:";
private static final String NAMESPACE_EMAIL = "email:";

private final KeycloakSession session;

Expand Down Expand Up @@ -143,7 +151,7 @@ private Identity resolveSubjectIdentity(RealmModel realm, AuthZen.EvaluationRequ
AuthZen.Subject subject = request.subject();
Identity identity = switch (subject.type()) {
case USER -> {
UserModel user = session.users().getUserByUsername(realm, subject.id());
UserModel user = resolveUserId(realm, subject.id());
yield user != null ? new UserModelIdentity(realm, user) : null;
}
case CLIENT -> {
Expand All @@ -157,6 +165,35 @@ private Identity resolveSubjectIdentity(RealmModel realm, AuthZen.EvaluationRequ
return identity;
}

private UserModel resolveUserId(RealmModel realm, String subjectId) {
UserProvider users = session.users();
if (subjectId.startsWith(NAMESPACE_ID)) {
String id = extractNamespaceValue(subjectId, NAMESPACE_ID);
return users.getUserById(realm, id);
} else if (subjectId.startsWith(NAMESPACE_USERNAME)) {
String username = extractNamespaceValue(subjectId, NAMESPACE_USERNAME);
return users.getUserByUsername(realm, username);
} else if (subjectId.startsWith(NAMESPACE_EMAIL)) {
if (realm.isDuplicateEmailsAllowed()) {
throw new BadRequestException("email namespace cannot be used when duplicate emails are allowed");
}
String email = extractNamespaceValue(subjectId, NAMESPACE_EMAIL);
return users.getUserByEmail(realm, email);
} else if (UUID_PATTERN.matcher(subjectId).matches()) {
return users.getUserById(realm, subjectId);
} else {
return users.getUserByUsername(realm, subjectId);
}
}

private static String extractNamespaceValue(String subjectId, String namespace) {
String value = subjectId.substring(namespace.length());
if (value.isEmpty()) {
throw new BadRequestException("subject id namespace '" + namespace + "' requires a non-empty value");
}
return value;
}

private static Identity withSubjectProperties(Identity delegate, Map<String, Object> properties) {
Map<String, Collection<String>> extra = new HashMap<>();
convertValues(properties, extra);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.keycloak.tests.authzen;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;

Expand All @@ -25,6 +26,7 @@
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.http.simple.SimpleHttp;
import org.keycloak.http.simple.SimpleHttpResponse;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.RegexPolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
Expand Down Expand Up @@ -650,6 +652,223 @@ public void testXRequestIdEchoedOnBadRequestResponse() throws IOException {
}
}

@Test
public void testUsernameNamespaceResolvesUser() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "username:" + ADMIN_USER)
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertTrue(result.decision());
}

@Test
public void testIdNamespaceResolvesUser() throws IOException {
String userId = lookupUserId(ADMIN_USER);

EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "id:" + userId)
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertTrue(result.decision());
}

@Test
public void testEmailNamespaceResolvesUser() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "email:admin@localhost")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertTrue(result.decision());
}

@Test
public void testEmailNamespaceResolvesCorrectUser() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "email:regular@localhost")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

@Test
public void testNoNamespaceUUIDFallsBackToIdLookup() throws IOException {
String userId = lookupUserId(ADMIN_USER);

EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, userId)
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertTrue(result.decision());
}

@Test
public void testNoNamespaceNonUUIDFallsBackToUsernameLookup() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, ADMIN_USER)
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertTrue(result.decision());
}

@Test
public void testEmailNamespaceWithDuplicateEmailsReturnsBadRequest() throws IOException {
realm.updateWithCleanup(r -> r.duplicateEmailsAllowed(true));

EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "email:admin@localhost")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(400, result.statusCode());
}

@Test
public void testUsernameNamespaceUnknownUserReturnsDenied() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "username:nonexistent-user")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

@Test
public void testIdNamespaceUnknownUserReturnsDenied() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "id:00000000-0000-0000-0000-000000000000")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

@Test
public void testEmailNamespaceUnknownUserReturnsDenied() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "email:nonexistent@localhost")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

@Test
public void testNoNamespaceUUIDUnknownUserReturnsDenied() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

@Test
public void testEmptyIdNamespaceReturnsBadRequest() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "id:")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(400, result.statusCode());
}

@Test
public void testEmptyUsernameNamespaceReturnsBadRequest() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "username:")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(400, result.statusCode());
}

@Test
public void testEmptyEmailNamespaceReturnsBadRequest() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "email:")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(400, result.statusCode());
}
< BD0 br>
@Test
public void testMixedCaseNamespaceFallsBackToUsernameLookup() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "Email:admin@localhost")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
// "Email:admin@localhost" is treated as a username, which won't exist
assertFalse(result.decision());
}

@Test
public void testIdNamespaceWithNonUUIDReturnsDenied() throws IOException {
EvaluationResult result = authzenClient("admin-user", "password")
.evaluate(AuthZenClient.evaluationRequest()
.subject(USER, "id:not-a-uuid")
.action("read")
.resource("endpoint", "/admin")
.build());

assertEquals(200, result.statusCode());
assertFalse(result.decision());
}

private String lookupUserId(String username) {
List<UserRepresentation> users = realm.admin().users().search(username, true);
assertEquals(1, users.size(), "Expected exactly one user with username: " + username);
return users.get(0).getId();
}

private String realmUrl() {
return keycloakUrls.getBase() + "/realms/" + realm.getName();
}
Expand Down
Loading
0