diff --git a/docs/documentation/server_admin/topics/admin-cli.adoc b/docs/documentation/server_admin/topics/admin-cli.adoc index 2e41a8d84c..566398a255 100644 --- a/docs/documentation/server_admin/topics/admin-cli.adoc +++ b/docs/documentation/server_admin/topics/admin-cli.adoc @@ -1031,15 +1031,15 @@ You can filter users by `username`, `firstName`, `lastName`, or `email`. For example: [options="nowrap"] ---- -$ kcadm.sh get users -r demorealm -q email=google.com -$ kcadm.sh get users -r demorealm -q username=testuser +$ kcadm.sh get users -r demorealm -q q=email:google.com +$ kcadm.sh get users -r demorealm -q q=username:testuser ---- [NOTE] ==== Filtering does not use exact matching. This example matches the value of the `username` attribute against the `\*testuser*` pattern. ==== -You can filter across multiple attributes by specifying multiple `-q` options. {project_name} returns users that match the condition for all the attributes only. +For clients, groups, and users you can filter across multiple attributes by specifying a more complex `q` query parameter. you may use something like -q q="field1:value1 field2:value2". {project_name} returns users that match the condition for all the attributes only. [discrete] ==== Getting a specific user diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java index 69e4cf9be0..ca166d3fe8 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AbstractRequestCmd.java @@ -113,7 +113,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { @ArgGroup(exclusive = true, multiplicity = "0..*") List rawAttributeOperations = new ArrayList<>(); - @Option(names = {"-q", "--query"}, description = "Add to request URI a NAME query parameter with value VALUE") + @Option(names = {"-q", "--query"}, description = "Add to request URI a NAME query parameter with value VALUE, for example --query q=username:admin") List rawFilters = new LinkedList<>(); @Parameters(arity = "0..1") @@ -142,12 +142,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } for (String arg : rawFilters) { - String[] keyVal; - if (arg.indexOf("=") == -1) { - keyVal = new String[] {"", arg}; - } else { - keyVal = parseKeyVal(arg); - } + String[] keyVal = parseKeyVal(arg); filter.put(keyVal[0], keyVal[1]); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java index 3240589186..04e8ded8d0 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/CreateCmd.java @@ -110,7 +110,7 @@ public class CreateCmd extends AbstractRequestCmd { out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body"); out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'"); out.println(" -b, --body CONTENT Content to be sent as-is or used as a JSON object template"); - out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE"); + out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE, for example --query q=username:admin"); out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(); out.println(" -H, --print-headers Print response headers"); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java index 829cfd9301..85a52e458c 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/DeleteCmd.java @@ -74,7 +74,7 @@ public class DeleteCmd extends CreateCmd { out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body"); out.println(" -f, --file FILENAME Send a body with request - read object from file or standard input if FILENAME is set to '-'"); out.println(" -b, --body CONTENT Content to be sent as-is or used as a JSON object template"); - out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE"); + out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE, for example --query q=username:admin"); out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(); out.println(" -H, --print-headers Print response headers"); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java index 17ddb090b5..13866bb5c8 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetCmd.java @@ -106,7 +106,7 @@ public class GetCmd extends AbstractRequestCmd { out.println(" realms, users, roles, groups, clients, keys, serverinfo, components ..."); out.println(" If it starts with 'http://' then it will be used as target resource url"); out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against"); - out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE"); + out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE, for example --query q=username:admin"); out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(" -o, --offset OFFSET Set paging offset - adds a query parameter 'first' which some endpoints recognize"); out.println(" -l, --limit LIMIT Set limit to number of items in result - adds a query parameter 'max' "); @@ -164,7 +164,7 @@ public class GetCmd extends AbstractRequestCmd { out.println("Note: 'users' endpoint knows how to handle --offset and --limit. Most other endpoints don't."); out.println(); out.println("Get all users whose 'username' matches '*test*' pattern, and 'email' matches '*@google.com*':"); - out.println(" " + PROMPT + " " + CMD + " get users -r demorealm -q username=test -q email=@google.com"); + out.println(" " + PROMPT + " " + CMD + " get users -r demorealm -q q=\"username:test email:@google.com\""); out.println(); out.println("Note: it is the 'users' endpoint that interprets query parameters 'username', and 'email' in such a way that"); out.println("it results in the described semantics. Another endpoint may provide a different semantics."); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java index 11ccc18693..afc569ba09 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/UpdateCmd.java @@ -117,7 +117,7 @@ public class UpdateCmd extends AbstractRequestCmd { out.println(" -d, --delete NAME Remove a specific attribute NAME from JSON request body"); out.println(" -f, --file FILENAME Read object from file or standard input if FILENAME is set to '-'"); out.println(" -b, --body CONTENT Content to be sent as-is or used as a JSON object template"); - out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE"); + out.println(" -q, --query NAME=VALUE Add to request URI a NAME query parameter with value VALUE, for example --query q=username:admin"); out.println(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(" -m, --merge Merge new values with existing configuration on the server"); out.println(" Merge is automatically enabled unless --file is specified"); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/HttpUtil.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/HttpUtil.java index 5af609f5a0..72db013b1d 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/HttpUtil.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/cli/util/HttpUtil.java @@ -314,7 +314,7 @@ public class HttpUtil { } query.append(params.getKey()).append("=").append(URLEncoder.encode(params.getValue(), "utf-8")); } catch (Exception e) { - throw new RuntimeException("Failed to encode query params: " + params.getKey() + "=" + params.getValue()); + throw new IllegalArgumentException("Failed to encode query params: " + params.getKey() + "=" + params.getValue()); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java index a8c686e580..ee9153c924 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java @@ -1,7 +1,5 @@ package org.keycloak.testsuite.cli.admin; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.keycloak.client.cli.config.FileConfigHandler; @@ -13,9 +11,14 @@ import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import static org.hamcrest.Matchers.equalTo; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.node.ObjectNode; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.cli.KcAdmExec.execute; @@ -48,6 +51,16 @@ public class KcAdmSessionTest extends AbstractAdmCliTest { assertExitCodeAndStreamSizes(exe, 0, 1, 0); String userId = exe.stdoutLines().get(0); + exe = execute("get users --config '" + configFile.getName() + "' -r demorealm -q q=username:testuser"); + assertExitCodeAndStdErrSize(exe, 0, 0); + String result = exe.stdoutString(); + assertTrue(result.contains(userId)); + + exe = execute("get users --config '" + configFile.getName() + "' -r demorealm -q q=username:non-existent"); + assertExitCodeAndStdErrSize(exe, 0, 0); + String emptyResult = exe.stdoutString(); + assertFalse(emptyResult.contains(userId)); + // add realm admin capabilities to user exe = execute("add-roles --config '" + configFile.getName() + "' -r demorealm --uusername testuser --cclientid realm-management --rolename realm-admin");