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 84d957963f..81d893c329 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 @@ -73,6 +73,8 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { String file; + String body; + String fields; boolean printHeaders; @@ -210,6 +212,10 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { throw new IllegalArgumentException("Options --merge and --no-merge are mutually exclusive"); } + if (body != null && file != null) { + throw new IllegalArgumentException("Options --body and --file are mutually exclusive"); + } + if (file == null && attrs.size() > 0 && !noMerge) { mergeMode = true; } @@ -222,17 +228,17 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { // see if Content-Type header is explicitly set to non-json value Header ctype = headers.get("content-type"); - InputStream body = null; + InputStream content = null; CmdStdinContext ctx = new CmdStdinContext<>(); if (file != null) { if (ctype != null && !"application/json".equals(ctype.getValue())) { if ("-".equals(file)) { - body = System.in; + content = System.in; } else { try { - body = new BufferedInputStream(new FileInputStream(file)); + content = new BufferedInputStream(new FileInputStream(file)); } catch (FileNotFoundException e) { throw new RuntimeException("File not found: " + file); } @@ -240,6 +246,8 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } else { ctx = parseFileOrStdin(file); } + } else if (body != null) { + content = new ByteArrayInputStream(body.getBytes(Charset.forName("utf-8"))); } ConfigData config = loadConfig(); @@ -304,15 +312,15 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } if (attrs.size() > 0) { - if (body != null) { + if (content != null) { throw new RuntimeException("Can't set attributes on content of type other than application/json"); } ctx = mergeAttributes(ctx, MAPPER.createObjectNode(), attrs); } - if (body == null && ctx.getContent() != null) { - body = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8"))); + if (content == null && ctx.getContent() != null) { + content = new ByteArrayInputStream(ctx.getContent().getBytes(Charset.forName("utf-8"))); } ReturnFields returnFields = null; @@ -322,7 +330,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { } // make sure content type is set - if (body != null) { + if (content != null) { headers.addIfMissing("Content-Type", "application/json"); } @@ -339,7 +347,7 @@ public abstract class AbstractRequestCmd extends AbstractAuthOptionsCmd { HeadersBodyStatus response; try { - response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, body)); + response = HttpUtil.doRequest(httpVerb, resourceUrl, new HeadersBody(headers, content)); } catch (IOException e) { throw new RuntimeException("HTTP request error: " + e.getMessage(), e); } diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java index fecd042cc2..2ca6cd39c7 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/AddRolesCmd.java @@ -67,6 +67,12 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { @Option(name = "gid", description = "Target group's 'id'") String gid; + @Option(name = "rname", description = "Composite role's 'name'") + String rname; + + @Option(name = "rid", description = "Composite role's 'id'") + String rid; + @Option(name = "cclientid", description = "Target client's 'clientId'") String cclientid; @@ -116,19 +122,31 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { } if (roleNames.isEmpty() && roleIds.isEmpty()) { - throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles"); + throw new IllegalArgumentException("No role to add specified. Use --rolename or --roleid to specify roles to add"); } if (cid != null && cclientid != null) { throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); } + if (rid != null && rname != null) { + throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); + } + if (isUserSpecified() && isGroupSpecified()) { throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); } - if (!isUserSpecified() && !isGroupSpecified()) { - throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group"); + if (isUserSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); + } + + if (isGroupSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { + throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } @@ -204,9 +222,32 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { GroupOperations.addRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); } - } else { + } else if (isCompositeRoleSpecified()) { + if (rid == null) { + rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); + } + if (isClientSpecified()) { + // list client roles for a composite role + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } - throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group"); + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now add all the roles + RoleOperations.addClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + + } else { + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now add all the roles + RoleOperations.addRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + } + + } else { + throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } return CommandResult.SUCCESS; @@ -257,6 +298,9 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { return uid != null || uusername != null; } + private boolean isCompositeRoleSpecified() { + return rid != null || rname != null; + } @Override protected boolean nothingToDo() { @@ -275,9 +319,10 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { StringWriter sb = new StringWriter(); PrintWriter out = new PrintWriter(sb); out.println("Usage: " + CMD + " add-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); - out.println("Usage: " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); + out.println(" " + CMD + " add-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); + out.println(" " + CMD + " add-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); out.println(); - out.println("Command to add realm or client roles to a user or group."); + out.println("Command to add realm or client roles to a user, a group or a composite role."); out.println(); out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS"); out.println("to perform one time authentication."); @@ -285,7 +330,8 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { out.println("If client is specified using --cclientid or --cid then roles to add are client roles, otherwise they are realm roles."); out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are added"); out.println("to a specific user. If group is specified using --gname, --gpath or --gid then roles are added to a specific group."); - out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group or a user."); + out.println("If composite role is specified using --rname or --rid then roles are added to a specific composite role."); + out.println("One or more roles have to be specified using --rolename or --roleid so that they are added to a group, a user or a composite role."); out.println(); out.println("Arguments:"); out.println(); @@ -306,6 +352,8 @@ public class AddRolesCmd extends AbstractAuthOptionsCmd { out.println(" to use --gid, or --gpath to specify the target group"); out.println(" --gpath Group's 'path' attribute"); out.println(" --gid Group's 'id' attribute"); + out.println(" --rname Composite role's 'name' attribute"); + out.println(" --rid Composite role's 'id' attribute"); out.println(" --cclientid Client's 'clientId' attribute"); out.println(" --cid Client's 'id' attribute"); out.println(" --rolename Role's 'name' attribute"); 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 63591f129a..86ca3f19b3 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 @@ -34,9 +34,12 @@ import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; @CommandDefinition(name = "create", description = "Command to create new resources") public class CreateCmd extends AbstractRequestCmd { - @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'", hasValue = true) + @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'") String file; + @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template") + String body; + @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header", hasValue = true) String fields; @@ -59,6 +62,7 @@ public class CreateCmd extends AbstractRequestCmd { void initOptions() { // set options on parent super.file = file; + super.body = body; super.fields = fields; super.printHeaders = printHeaders; super.returnId = returnId; @@ -69,7 +73,7 @@ public class CreateCmd extends AbstractRequestCmd { @Override protected boolean nothingToDo() { - return noOptions() && file == null && (args == null || args.size() == 0); + return noOptions() && file == null && body == null && (args == null || args.size() == 0); } protected String suggestHelp() { @@ -109,6 +113,7 @@ public class CreateCmd extends AbstractRequestCmd { out.println(" -s, --set NAME=VALUE Set a specific attribute NAME to a specified value VALUE"); 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(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(); @@ -153,8 +158,19 @@ public class CreateCmd extends AbstractRequestCmd { out.println(" EOF"); } out.println(); + out.println("Create a new group using configuration JSON passed as 'body' argument:"); + if (OS_ARCH.isWindows()) { + out.println(" " + PROMPT + " " + CMD + " create groups -r demorealm -b \"{ \\\"name\\\": \\\"Admins\\\" }\""); + } else { + out.println(" " + PROMPT + " " + CMD + " create groups -r demorealm -b '{ \"name\": \"Admins\" }'"); + } + out.println(); out.println("Create a client using file as a template, and override some attributes - return an 'id' of new client:"); - out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i"); + if (OS_ARCH.isWindows()) { + out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s \"redirectUris=[\\\"http://localhost:8980/myapp/*\\\"]\" -i"); + } else { + out.println(" " + PROMPT + " " + CMD + " create clients -r demorealm -f my_client.json -s clientId=my_client2 -s 'redirectUris=[\"http://localhost:8980/myapp/*\"]' -i"); + } out.println(); out.println("Create a new client role for client my_client in realm 'demorealm' (replace ID with output of previous example command):"); out.println(" " + PROMPT + " " + CMD + " create clients/ID/roles -r demorealm -s name=client_role"); 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 7ef31e1c89..d84833db49 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 @@ -80,6 +80,7 @@ public class DeleteCmd extends CreateCmd { out.println(" -s, --set NAME=VALUE Send a body with request - set a specific attribute NAME to a specified value VALUE"); 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(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java index 7b0fd5f2bc..9a4fb659be 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/GetRolesCmd.java @@ -57,10 +57,10 @@ public class GetRolesCmd extends GetCmd { @Option(name = "cid", description = "Target client's 'id'") String cid; - @Option(name = "rolename", description = "Target role's 'name'") + @Option(name = "rname", description = "Composite role's 'name'") String rname; - @Option(name = "roleid", description = "Target role's 'id'") + @Option(name = "rid", description = "Composite role's 'id'") String rid; @Option(name = "gname", description = "Target group's 'name'") @@ -72,6 +72,12 @@ public class GetRolesCmd extends GetCmd { @Option(name = "gid", description = "Target group's 'id'") String gid; + @Option(name = "rolename", description = "Target role's 'name'") + String rolename; + + @Option(name = "roleid", description = "Target role's 'id'") + String roleid; + @Option(name = "available", description = "List only available roles", hasValue = false) boolean available; @@ -108,10 +114,14 @@ public class GetRolesCmd extends GetCmd { throw new IllegalArgumentException("Incompatible options: --gid, --gname and --gpath are mutually exclusive"); } - if (rid != null && rname != null) { + if (roleid != null && rolename != null) { throw new IllegalArgumentException("Incompatible options: --roleid and --rolename are mutually exclusive"); } + if (rid != null && rname != null) { + throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); + } + if (cid != null && cclientid != null) { throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); } @@ -120,6 +130,22 @@ public class GetRolesCmd extends GetCmd { throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); } + if (isUserSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); + } + + if (isGroupSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (all && effective) { + throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --effective"); + } + + if (all && available) { + throw new IllegalArgumentException("Incompatible options: --all can't be used at the same time as --available"); + } + super.processOptions(commandInvocation); } @@ -167,7 +193,7 @@ public class GetRolesCmd extends GetCmd { } else if (effective) { super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + "/role-mappings/realm"); + super.url = composeResourceUrl(adminRoot, realm, "users/" + uid + (all ? "/role-mappings" : "/role-mappings/realm")); } } } else if (isGroupSpecified()) { @@ -195,9 +221,35 @@ public class GetRolesCmd extends GetCmd { } else if (effective) { super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm/composite"); } else { - super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + "/role-mappings/realm"); + super.url = composeResourceUrl(adminRoot, realm, "groups/" + gid + (all ? "/role-mappings" : "/role-mappings/realm")); } } + } else if (isCompositeRoleSpecified()) { + String uri = rname != null ? "roles/" + rname : "roles-by-id/" + rid; + + if (isClientSpecified()) { + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } + if (available) { + throw new IllegalArgumentException("Option --available not supported with composite roles. Try '" + CMD + " get-roles --cid " + cid + "' for full list of client roles for that client"); + } + if (effective) { + throw new IllegalArgumentException("Option --effective not supported with composite roles."); + } + uri += "/composites/clients/" + cid; + } else { + if (available) { + throw new IllegalArgumentException("Option --available not supported with composite roles. Try '" + CMD + " get-roles' for full list of realm roles"); + } + if (effective) { + throw new IllegalArgumentException("Option --effective not supported with composite roles."); + } + + uri += all ? "/composites" : "/composites/realm"; + } + super.url = composeResourceUrl(adminRoot, realm, uri); + } else if (isClientSpecified()) { if (cid == null) { cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); @@ -205,10 +257,10 @@ public class GetRolesCmd extends GetCmd { if (isRoleSpecified()) { // get specific client role - if (rname == null) { - rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid); + if (rolename == null) { + rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid); } - super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rname); + super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles/" + rolename); } else { // list defined client roles super.url = composeResourceUrl(adminRoot, realm, "clients/" + cid + "/roles"); @@ -216,10 +268,10 @@ public class GetRolesCmd extends GetCmd { } else { if (isRoleSpecified()) { // get specific realm role - if (rname == null) { - rname = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, rid); + if (rolename == null) { + rolename = RoleOperations.getClientRoleNameFromId(adminRoot, realm, auth, cid, roleid); } - super.url = composeResourceUrl(adminRoot, realm, "roles/" + rname); + super.url = composeResourceUrl(adminRoot, realm, "roles/" + rolename); } else { // list defined realm roles super.url = composeResourceUrl(adminRoot, realm, "roles"); @@ -230,7 +282,7 @@ public class GetRolesCmd extends GetCmd { } private boolean isRoleSpecified() { - return rid != null || rname != null; + return roleid != null || rolename != null; } private boolean isClientSpecified() { @@ -241,6 +293,10 @@ public class GetRolesCmd extends GetCmd { return gid != null || gname != null || gpath != null; } + private boolean isCompositeRoleSpecified() { + return rid != null || rname != null; + } + private boolean isUserSpecified() { return uid != null || uusername != null; } @@ -261,10 +317,11 @@ public class GetRolesCmd extends GetCmd { StringWriter sb = new StringWriter(); PrintWriter out = new PrintWriter(sb); out.println("Usage: " + CMD + " get-roles [--cclientid CLIENT_ID | --cid ID] [ARGUMENTS]"); - out.println("Usage: " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] (ARGUMENTS)"); - out.println("Usage: " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective] [ARGUMENTS]"); + out.println(" " + CMD + " get-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] (ARGUMENTS)"); + out.println(" " + CMD + " get-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] [ARGUMENTS]"); + out.println(" " + CMD + " get-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] [--available | --effective | --all] [ARGUMENTS]"); out.println(); - out.println("Command to list realm or client roles on a realm, user or group."); + out.println("Command to list realm or client roles of a realm, a user, a group or a composite role."); out.println(); out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS"); out.println("to perform one time authentication."); @@ -272,11 +329,13 @@ public class GetRolesCmd extends GetCmd { out.println("If client is specified using --cclientid or --cid then client roles are listed, otherwise realm roles are listed."); out.println("If user is specified using --uusername or --uid then roles are listed for a specific user."); out.println("If group is specified using --gname, --gpath or --gid then roles are listed for a specific group."); - out.println("If neither user nor group is specified then defined roles are listed for a realm or specific client"); + out.println("If composite role is specified --rname or --rid then roles are listed for a specific composite role."); + out.println("If neither user nor group, nor composite role is specified then defined roles are listed for a realm or specific client."); out.println("If role is specified using --rolename or --roleid then only that specific role is returned."); out.println("If --available is specified, then only roles not yet added to the target user or group are returned."); out.println("If --effective is specified, then roles added to the target user or group are transitively resolved and a full"); - out.println("set of roles in effect for that user or group is returned."); + out.println("set of roles in effect for that user, group or composite role is returned."); + out.println("If --all is specified, then client roles for all clients are returned in addition to realm roles."); out.println(); out.println("Arguments:"); out.println(); @@ -297,10 +356,15 @@ public class GetRolesCmd extends GetCmd { out.println(" to use --gid, or --gpath to specify the target group"); out.println(" --gpath Group's 'path' attribute"); out.println(" --gid Group's 'id' attribute"); + out.println(" --rname Composite role's 'name' attribute"); + out.println(" --rid Composite role's 'id' attribute"); out.println(" --cclientid Client's 'clientId' attribute"); out.println(" --cid Client's 'id' attribute"); out.println(" --rolename Role's 'name' attribute"); out.println(" --roleid Role's 'id' attribute"); + out.println(" --available Return available roles - those that can still be added"); + out.println(" --effective Return effective roles - transitively taking composite roles into account"); + out.println(" --all Return all client roles in addition to realm roles"); out.println(" -a, --admin-root URL URL of Admin REST endpoint root if not default - e.g. http://localhost:8080/auth/admin"); out.println(" -r, --target-realm REALM Target realm to issue requests against if not the one authenticated against"); out.println(); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java index 84698c3e65..5338d232eb 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/commands/RemoveRolesCmd.java @@ -67,6 +67,12 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { @Option(name = "gid", description = "Target group's 'id'") String gid; + @Option(name = "rname", description = "Composite role's 'name'") + String rname; + + @Option(name = "rid", description = "Composite role's 'id'") + String rid; + @Option(name = "cclientid", description = "Target client's 'clientId'") String cclientid; @@ -116,19 +122,31 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { } if (roleNames.isEmpty() && roleIds.isEmpty()) { - throw new IllegalArgumentException("No role specified. Use --rolename or --roleid to specify roles"); + throw new IllegalArgumentException("No role to remove specified. Use --rolename or --roleid to specify roles to remove"); } if (cid != null && cclientid != null) { throw new IllegalArgumentException("Incompatible options: --cid and --cclientid are mutually exclusive"); } + if (rid != null && rname != null) { + throw new IllegalArgumentException("Incompatible options: --rid and --rname are mutually exclusive"); + } + if (isUserSpecified() && isGroupSpecified()) { throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --gname / --gid / --gpath"); } - if (!isUserSpecified() && !isGroupSpecified()) { - throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group"); + if (isUserSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --uusername / --uid can't be used at the same time as --rname / --rid"); + } + + if (isGroupSpecified() && isCompositeRoleSpecified()) { + throw new IllegalArgumentException("Incompatible options: --rname / --rid can't be used at the same time as --gname / --gid / --gpath"); + } + + if (!isUserSpecified() && !isGroupSpecified() && !isCompositeRoleSpecified()) { + throw new IllegalArgumentException("No user nor group nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } @@ -204,9 +222,32 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { GroupOperations.removeRealmRoles(adminRoot, realm, auth, gid, new ArrayList<>(rolesToAdd)); } - } else { + } else if (isCompositeRoleSpecified()) { + if (rid == null) { + rid = RoleOperations.getIdFromRoleName(adminRoot, realm, auth, rname); + } + if (isClientSpecified()) { + // remove client roles from a role + if (cid == null) { + cid = ClientOperations.getIdFromClientId(adminRoot, realm, auth, cclientid); + } - throw new IllegalArgumentException("No user nor group specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group"); + List roles = RoleOperations.getClientRoles(adminRoot, realm, cid, auth); + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, new LocalSearch(roles)); + + // now remove the roles + RoleOperations.removeClientRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + + } else { + Set rolesToAdd = getRoleRepresentations(roleNames, roleIds, + new LocalSearch(RoleOperations.getRealmRolesAsNodes(adminRoot, realm, auth))); + + // now remove the roles + RoleOperations.removeRealmRoles(adminRoot, realm, auth, rid, new ArrayList<>(rolesToAdd)); + } + + } else { + throw new IllegalArgumentException("No user nor group, nor composite role specified. Use --uusername / --uid to specify user or --gname / --gid / --gpath to specify group or --rname / --rid to specify a composite role"); } return CommandResult.SUCCESS; @@ -257,6 +298,9 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { return uid != null || uusername != null; } + private boolean isCompositeRoleSpecified() { + return rid != null || rname != null; + } @Override protected boolean nothingToDo() { @@ -275,9 +319,10 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { StringWriter sb = new StringWriter(); PrintWriter out = new PrintWriter(sb); out.println("Usage: " + CMD + " remove-roles (--uusername USERNAME | --uid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); - out.println("Usage: " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); + out.println(" " + CMD + " remove-roles (--gname NAME | --gpath PATH | --gid ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); + out.println(" " + CMD + " remove-roles (--rname ROLE_NAME | --rid ROLE_ID) [--cclientid CLIENT_ID | --cid ID] (--rolename NAME | --roleid ID)+ [ARGUMENTS]"); out.println(); - out.println("Command to remove realm or client roles from a user or group."); + out.println("Command to remove realm or client roles from a user, a group or a composite role."); out.println(); out.println("Use `" + CMD + " config credentials` to establish an authenticated session, or use CREDENTIALS OPTIONS"); out.println("to perform one time authentication."); @@ -285,7 +330,8 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { out.println("If client is specified using --cclientid or --cid then roles to remove are client roles, otherwise they are realm roles."); out.println("Either a user, or a group needs to be specified. If user is specified using --uusername or --uid then roles are removed"); out.println("from a specific user. If group is specified using --gname, --gpath or --gid then roles are removed from a specific group."); - out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group or a user."); + out.println("If composite role is specified using --rname or --rid then roles are removed from a specific composite role."); + out.println("One or more roles have to be specified using --rolename or --roleid to be removed from a group, a user or a composite role."); out.println(); out.println("Arguments:"); out.println(); @@ -306,6 +352,8 @@ public class RemoveRolesCmd extends AbstractAuthOptionsCmd { out.println(" to use --gid, or --gpath to specify the target group"); out.println(" --gpath Group's 'path' attribute"); out.println(" --gid Group's 'id' attribute"); + out.println(" --rname Composite role's 'name' attribute"); + out.println(" --rid Composite role's 'id' attribute"); out.println(" --cclientid Client's 'clientId' attribute"); out.println(" --cid Client's 'id' attribute"); out.println(" --rolename Role's 'name' attribute"); 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 3ce91b321a..6c194e9cd7 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 @@ -26,6 +26,7 @@ import java.io.StringWriter; import static org.keycloak.client.admin.cli.util.ConfigUtil.DEFAULT_CONFIG_FILE_STRING; import static org.keycloak.client.admin.cli.util.OsUtil.CMD; import static org.keycloak.client.admin.cli.util.OsUtil.EOL; +import static org.keycloak.client.admin.cli.util.OsUtil.OS_ARCH; import static org.keycloak.client.admin.cli.util.OsUtil.PROMPT; /** @@ -37,6 +38,9 @@ public class UpdateCmd extends AbstractRequestCmd { @Option(shortName = 'f', name = "file", description = "Read object from file or standard input if FILENAME is set to '-'") String file; + @Option(shortName = 'b', name = "body", description = "JSON object to be sent as-is or used as a template") + String body; + @Option(shortName = 'F', name = "fields", description = "A pattern specifying which attributes of JSON response body to actually display as result - causes mismatch with Content-Length header") String fields; @@ -63,6 +67,7 @@ public class UpdateCmd extends AbstractRequestCmd { void initOptions() { // set options on parent super.file = file; + super.body = body; super.fields = fields; super.printHeaders = printHeaders; super.returnId = false; @@ -76,7 +81,7 @@ public class UpdateCmd extends AbstractRequestCmd { @Override protected boolean nothingToDo() { - return noOptions() && file == null && (args == null || args.size() == 0); + return noOptions() && file == null && body == null && (args == null || args.size() == 0); } protected String suggestHelp() { @@ -117,6 +122,7 @@ public class UpdateCmd extends AbstractRequestCmd { out.println(" NAME+=VALUE Add item VALUE to list attribute NAME"); 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(" -h, --header NAME=VALUE Set request header NAME to VALUE"); out.println(" -m, --merge Merge new values with existing configuration on the server"); @@ -150,7 +156,11 @@ public class UpdateCmd extends AbstractRequestCmd { out.println(" " + PROMPT + " " + CMD + " update realms/demorealm -s registrationAllowed=true"); out.println(); out.println("Update a client by overwriting existing configuration using local file as a template (replace ID with client's 'id'):"); - out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'"); + if (OS_ARCH.isWindows()) { + out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s \"redirectUris=[\\\"http://localhost:8080/myapp/*\\\"]\""); + } else { + out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s 'redirectUris=[\"http://localhost:8080/myapp/*\"]'"); + } out.println(); out.println("Update client by fetching current configuration from server and merging with specified changes (replace ID with client's 'id'):"); out.println(" " + PROMPT + " " + CMD + " update clients/ID -f new_my_client.json -s enabled=true --merge"); diff --git a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java index d3274fdad2..c7e310844e 100644 --- a/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java +++ b/integration/client-cli/admin-cli/src/main/java/org/keycloak/client/admin/cli/operations/RoleOperations.java @@ -23,8 +23,11 @@ import java.util.ArrayList; import java.util.List; import static org.keycloak.client.admin.cli.util.HttpUtil.composeResourceUrl; +import static org.keycloak.client.admin.cli.util.HttpUtil.doDeleteJSON; import static org.keycloak.client.admin.cli.util.HttpUtil.doGetJSON; +import static org.keycloak.client.admin.cli.util.HttpUtil.doPostJSON; import static org.keycloak.client.admin.cli.util.HttpUtil.getAttrForType; +import static org.keycloak.client.admin.cli.util.HttpUtil.getIdForType; /** * @author Marko Strukelj @@ -34,6 +37,28 @@ public class RoleOperations { public static class LIST_OF_ROLES extends ArrayList{}; public static class LIST_OF_NODES extends ArrayList{}; + public static String getIdFromRoleName(String adminRoot, String realm, String auth, String rname) { + return getIdForType(adminRoot, realm, auth, "roles", "name", rname); + } + + public static void addRealmRoles(String rootUrl, String realm, String auth, String roleid, List roles) { + String resourceUrl = composeResourceUrl(rootUrl, realm, "roles-by-id/" + roleid + "/composites"); + doPostJSON(resourceUrl, auth, roles); + } + + public static void addClientRoles(String rootUrl, String realm, String auth, String roleid, List roles) { + addRealmRoles(rootUrl, realm, auth, roleid, roles); + } + + public static void removeRealmRoles(String rootUrl, String realm, String auth, String roleid, List roles) { + String resourceUrl = composeResourceUrl(rootUrl, realm, "roles-by-id/" + roleid + "/composites"); + doDeleteJSON(resourceUrl, auth, roles); + } + + public static void removeClientRoles(String rootUrl, String realm, String auth, String roleid, List roles) { + removeRealmRoles(rootUrl, realm, auth, roleid, roles); + } + public static String getRoleNameFromId(String adminRoot, String realm, String auth, String rid) { return getAttrForType(adminRoot, realm, auth, "roles", "id", rid, "name"); } 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 new file mode 100644 index 0000000000..6cbe4f5425 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmSessionTest.java @@ -0,0 +1,197 @@ +package org.keycloak.testsuite.cli.admin; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.client.admin.cli.config.FileConfigHandler; +import org.keycloak.testsuite.cli.KcAdmExec; +import org.keycloak.testsuite.util.TempFileResource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; +import static org.keycloak.testsuite.cli.KcAdmExec.execute; + +/** + * @author Marko Strukelj + */ +public class KcAdmSessionTest extends AbstractAdmCliTest { + + static Class> LIST_OF_JSON = new ArrayList() {}.getClass(); + + @Test + public void test() throws IOException { + + FileConfigHandler handler = initCustomConfigFile(); + + try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + + // login as admin + loginAsUser(configFile.getFile(), serverUrl, "master", "admin", "admin"); + + // create realm + KcAdmExec exe = execute("create realms --config '" + configFile.getName() + "' -s realm=demorealm -s enabled=true"); + + assertExitCodeAndStreamSizes(exe, 0, 0, 1); + Assert.assertTrue(exe.stderrLines().get(0).startsWith("Created ")); + + // create user + exe = execute("create users --config '" + configFile.getName() + "' -r demorealm -s username=testuser -s enabled=true -i"); + + assertExitCodeAndStreamSizes(exe, 0, 1, 0); + String userId = exe.stdoutLines().get(0); + + // add realm admin capabilities to user + exe = execute("add-roles --config '" + configFile.getName() + "' -r demorealm --uusername testuser --cclientid realm-management --rolename realm-admin"); + + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + // set password for the user + exe = execute("set-password --config '" + configFile.getName() + "' -r demorealm --username testuser -p password"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + + + // login as testuser + loginAsUser(configFile.getFile(), serverUrl, "demorealm", "testuser", "password"); + + + // get realm roles + exe = execute("get-roles --config '" + configFile.getName() + "'"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + List roles = loadJson(exe.stdout(), LIST_OF_JSON); + Assert.assertTrue("expect two realm roles available", roles.size() == 2); + + // create realm role + exe = execute("create roles --config '" + configFile.getName() + "' -s name=testrole -s 'description=Test role' -o"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + ObjectNode role = loadJson(exe.stdout(), ObjectNode.class); + Assert.assertEquals("testrole", role.get("name").asText()); + String roleId = role.get("id").asText(); + + // get realm roles again + exe = execute("get-roles --config '" + configFile.getName() + "'"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + roles = loadJson(exe.stdout(), LIST_OF_JSON); + Assert.assertTrue("expect three realm roles available", roles.size() == 3); + + // create client + exe = execute("create clients --config '" + configFile.getName() + "' -s clientId=testclient -i"); + + assertExitCodeAndStreamSizes(exe, 0, 1, 0); + String idOfClient = exe.stdoutLines().get(0); + + + // create client role + exe = execute("create clients/" + idOfClient + "/roles --config '" + configFile.getName() + "' -s name=clientrole -s 'description=Test client role'"); + + assertExitCodeAndStreamSizes(exe, 0, 0, 1); + Assert.assertTrue(exe.stderrLines().get(0).startsWith("Created ")); + + // make sure client role has been created + exe = execute("get-roles --config '" + configFile.getName() + "' --cclientid testclient"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + roles = loadJson(exe.stdout(), LIST_OF_JSON); + Assert.assertTrue("expect one role", roles.size() == 1); + Assert.assertEquals("clientrole", roles.get(0).get("name").asText()); + + // add created role to user - we are realm admin so we can add role to ourself + exe = execute("add-roles --config '" + configFile.getName() + "' --uusername testuser --cclientid testclient --rolename clientrole"); + + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + + // make sure the roles have been added + exe = execute("get-roles --config '" + configFile.getName() + "' --uusername testuser --all"); + + assertExitCodeAndStdErrSize(exe, 0, 0); + ObjectNode node = loadJson(exe.stdout(), ObjectNode.class); + Assert.assertNotNull(node.get("realmMappings")); + + List realmMappings = StreamSupport.stream(node.get("realmMappings").spliterator(), false) + .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("offline_access", "uma_authorization"), realmMappings); + + ObjectNode clientRoles = (ObjectNode) node.get("clientMappings"); + //List fields = asSortedList(clientRoles.fieldNames()); + List fields = StreamSupport.stream(clientRoles.spliterator(), false) + .map(o -> o.get("client").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("account", "realm-management", "testclient"), fields); + + realmMappings = StreamSupport.stream(clientRoles.get("account").get("mappings").spliterator(), false) + .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("manage-account", "view-profile"), realmMappings); + + realmMappings = StreamSupport.stream(clientRoles.get("realm-management").get("mappings").spliterator(), false) + .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("realm-admin"), realmMappings); + + realmMappings = StreamSupport.stream(clientRoles.get("testclient").get("mappings").spliterator(), false) + .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("clientrole"), realmMappings); + + + + // add a realm role to the user + exe = execute("add-roles --config '" + configFile.getName() + "' --uusername testuser --rolename testrole"); + + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + + // get all roles for the user again + exe = execute("get-roles --config '" + configFile.getName() + "' --uusername testuser --all"); + assertExitCodeAndStdErrSize(exe, 0, 0); + + node = loadJson(exe.stdout(), ObjectNode.class); + Assert.assertNotNull(node.get("realmMappings")); + + realmMappings = StreamSupport.stream(node.get("realmMappings").spliterator(), false) + .map(o -> o.get("name").asText()).sorted().collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList("offline_access", "testrole", "uma_authorization"), realmMappings); + + // create a group + exe = execute("create groups --config '" + configFile.getName() + "' -s name=TestUsers -i"); + assertExitCodeAndStdErrSize(exe, 0, 0); + String groupId = exe.stdoutLines().get(0); + + // create a sub-group + exe = execute("create groups/" + groupId + "/children --config '" + configFile.getName() + "' -s name=TestPowerUsers -i"); + assertExitCodeAndStdErrSize(exe, 0, 0); + String subGroupId = exe.stdoutLines().get(0); + + // add testuser to TestPowerUsers + exe = execute("update users/" + userId + "/groups/" + subGroupId + " --config '" + configFile.getName() + + "' -s realm=demorealm -s userId=" + userId + " -s groupId=" + subGroupId + " -n"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + // delete everything + exe = execute("delete groups/" + subGroupId + " --config '" + configFile.getName() + "'"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + exe = execute("delete groups/" + groupId + " --config '" + configFile.getName() + "'"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + exe = execute("delete clients/" + idOfClient + " --config '" + configFile.getName() + "'"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + exe = execute("delete roles/testrole --config '" + configFile.getName() + "'"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + exe = execute("delete users/" + userId + " --config '" + configFile.getName() + "'"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + + // delete realm as well - using initial master realm session still saved in config file + exe = execute("delete realms/demorealm --config '" + configFile.getName() + "' --realm master"); + assertExitCodeAndStreamSizes(exe, 0, 0, 0); + } + } +}