Skip to content

Commit

Permalink
Sort results according to user/group query ordering properties
Browse files Browse the repository at this point in the history
  • Loading branch information
VonDerBeck committed Mar 11, 2019
1 parent 699ccda commit 0e955e8
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 29 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ Features:
* Broad support for user and group queries
* Compatible with Spring Boot OAuth2 SSO

Current version: `0.4.0-SNAPSHOT`<br >
Tested with: Keycloak `4.8.3.Final`, Camunda `7.10.0` and Camunda `7.10.1-ee`
Current version: `0.5.0-SNAPSHOT`<br >
Tested with: Keycloak `4.8.3.Final`, Camunda `7.10.0` and Camunda `7.10.3-ee`

Known limitations:

* A strategy to distinguish SYSTEM and WORKFLOW groups is missing. Currently only the administrator group is mapped to type SYSTEM.
* Some query filters are applied on the client side - the REST API does not allow full criteria search in all required cases
* Sort criteria for queries not yet implemented
* Some query filters are applied on the client side - the Keycloak REST API does not allow full criteria search in all required cases.
* Sort criteria for queries are implemented on the client side - the Keycloak REST API does not allow result ordering.
* Tenants are currently not supported.

## Prerequisites in your Keycloak realm

Expand All @@ -50,7 +51,7 @@ Maven Dependencies:
<dependency>
<groupId>de.vonderbeck.bpm.identity</groupId>
<artifactId>camunda-identity-keycloak</artifactId>
<version>0.4.0-SNAPSHOT</version>
<version>0.5.0-SNAPSHOT</version>
</dependency>


Expand Down Expand Up @@ -92,7 +93,7 @@ A complete list of configuration options can be found below:
| `clientSecret` | The Client Secret of your application. |
| `useEmailAsCamundaUserId` | Whether to use the Keycloak email attribute as Camunda's user ID. Default is `false`.<br /><br />This is option is a fallback in case you don't use SSO and want to login using Camunda's web interface with your mail address and not the cryptic Keycloak ID. Keep in mind that you will only be able to login without SSO with Keycloak's internally managed users and users managed by the LDAP / Keberos User federation.|
| `administratorGroupName` | The name of the administrator group. If this name is set and engine authorization is enabled, the plugin will create group-level Administrator authorizations on all built-in resources. |
| `administratorUserName` | The name of the administrator user. If this name is set and engine authorization is enabled, the plugin will create user-level Administrator authorizations on all built-in resources. |
| `administratorUserId` | The ID of the administrator user. If this ID is set and engine authorization is enabled, the plugin will create user-level Administrator authorizations on all built-in resources. |
| `authorizationCheckEnabled` | If this property is set to true, then authorization checks are performed when querying for users or groups. Otherwise authorization checks are not performed when querying for users or groups. Default: `true`.<br />*Note*: If you have a huge amount of Keycloak users or groups we advise to set this property to false to improve the performance of the user and group query. |
| `maxHttpConnections` | Maximum number HTTP connections for the Keycloak connection pool. Default: `50`|
| `disableSSLCertificateValidation` | Whether to disable SSL certificate validation. Default: `false`. Useful in test environments. |
Expand Down Expand Up @@ -235,7 +236,6 @@ In order to run the unit tests I have used a local docker setup of Keycloak with
KEYCLOAK_PASSWORD: keycloak1!
ports:
- "9001:8443"
- "9000:8080"

For details see documentation on [Keycloak Docker Hub](https://hub.docker.com/r/jboss/keycloak/ "Keycloak Docker Images").

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>de.vonderbeck.bpm.identity</groupId>
<artifactId>camunda-identity-keycloak</artifactId>
<version>0.4.0-SNAPSHOT</version>
<version>0.5.0-SNAPSHOT</version>

<packaging>jar</packaging>
<name>camunda BPM - engine plugins - identity - keycloak</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ public class KeycloakConfiguration {
* on all built-in resources. */
protected String administratorGroupName;

/** The name of the administrator user.
/** The ID of the administrator user.
*
* If this name is set to a non-null and non-empty value,
* If this ID is set to a non-null and non-empty value,
* the plugin will create user-level Administrator authorizations
* on all built-in resources. */
protected String administratorUserName;
protected String administratorUserId;

/** Whether to enable Camunda authorization checks for groups and users. */
protected boolean authorizationCheckEnabled = true;
Expand Down Expand Up @@ -136,17 +136,17 @@ public void setAdministratorGroupName(String administratorGroupName) {
}

/**
* @return the administratorUserName
* @return the administratorUserId
*/
public String getAdministratorUserName() {
return administratorUserName;
public String getAdministratorUserId() {
return administratorUserId;
}

/**
* @param administratorUserName the administratorUserName to set
* @param administratorUserId the administratorUserId to set
*/
public void setAdministratorUserName(String administratorUserName) {
this.administratorUserName = administratorUserName;
public void setAdministratorUserId(String administratorUserId) {
this.administratorUserId = administratorUserId;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import org.camunda.bpm.engine.BadUserRequestException;
Expand All @@ -19,7 +20,11 @@
import org.camunda.bpm.engine.identity.TenantQuery;
import org.camunda.bpm.engine.identity.User;
import org.camunda.bpm.engine.identity.UserQuery;
import org.camunda.bpm.engine.impl.Direction;
import org.camunda.bpm.engine.impl.GroupQueryProperty;
import org.camunda.bpm.engine.impl.QueryOrderingProperty;
import org.camunda.bpm.engine.impl.UserQueryImpl;
import org.camunda.bpm.engine.impl.UserQueryProperty;
import org.camunda.bpm.engine.impl.identity.IdentityProviderException;
import org.camunda.bpm.engine.impl.identity.ReadOnlyIdentityProvider;
import org.camunda.bpm.engine.impl.interceptor.CommandContext;
Expand Down Expand Up @@ -203,6 +208,10 @@ protected List<User> requestUsersByGroupId(KeycloakUserQuery query) {
KeycloakPluginLogger.INSTANCE.userQueryResult(resultLogger.toString());
}

if (query.getOrderingProperties().size() > 0) {
userList.sort(new UserComparator(query.getOrderingProperties()));
}

return userList;
}

Expand Down Expand Up @@ -277,6 +286,10 @@ protected List<User> requestUsersWithoutGroupId(KeycloakUserQuery query) {
KeycloakPluginLogger.INSTANCE.userQueryResult(resultLogger.toString());
}

if (query.getOrderingProperties().size() > 0) {
userList.sort(new UserComparator(query.getOrderingProperties()));
}

return userList;
}

Expand Down Expand Up @@ -362,6 +375,12 @@ protected String getKeycloakUserID(String userId) throws KeycloakUserNotFoundExc
}
}

/**
* Maps a Keycloak JSON result to a User object
* @param result the Keycloak JSON result
* @return the User object
* @throws JSONException in case of errors
*/
protected UserEntity transformUser(JSONObject result) throws JSONException {
UserEntity user = new UserEntity();
if (keycloakConfiguration.isUseEmailAsCamundaUserId()) {
Expand All @@ -377,6 +396,64 @@ protected UserEntity transformUser(JSONObject result) throws JSONException {
user.setEmail(getStringValue(result, "email"));
return user;
}

/**
* Helper for client side user ordering.
*/
private static class UserComparator implements Comparator<User> {
private final static int USER_ID = 0;
private final static int EMAIL = 1;
private final static int FIRST_NAME = 2;
private final static int LAST_NAME = 3;
private int[] order;
private boolean[] desc;
public UserComparator(List<QueryOrderingProperty> orderList) {
// Prepare query ordering
this.order = new int[orderList.size()];
this.desc = new boolean[orderList.size()];
for (int i = 0; i< orderList.size(); i++) {
QueryOrderingProperty qop = orderList.get(i);
if (qop.getQueryProperty().equals(UserQueryProperty.USER_ID)) {
order[i] = USER_ID;
} else if (qop.getQueryProperty().equals(UserQueryProperty.EMAIL)) {
order[i] = EMAIL;
} else if (qop.getQueryProperty().equals(UserQueryProperty.FIRST_NAME)) {
order[i] = FIRST_NAME;
} else if (qop.getQueryProperty().equals(UserQueryProperty.LAST_NAME)) {
order[i] = LAST_NAME;
} else {
order[i] = -1;
}
desc[i] = Direction.DESCENDING.equals(qop.getDirection());
}
}
@Override
public int compare(User u1, User u2) {
int c = 0;
for (int i = 0; i < order.length; i ++) {
switch (order[i]) {
case USER_ID:
c = KeycloakIdentityProviderSession.compare(u1.getId(), u2.getId());
break;
case EMAIL:
c = KeycloakIdentityProviderSession.compare(u1.getEmail(), u2.getEmail());
break;
case FIRST_NAME:
c = KeycloakIdentityProviderSession.compare(u1.getFirstName(), u2.getFirstName());
break;
case LAST_NAME:
c = KeycloakIdentityProviderSession.compare(u1.getLastName(), u2.getLastName());
break;
default:
// do nothing
}
if (c != 0) {
return desc[i] ? -c : c;
}
}
return c;
}
}

//-------------------------------------------------------------------------
// Login / Password check
Expand Down Expand Up @@ -580,6 +657,10 @@ protected List<Group> requestGroupsByUserId(KeycloakGroupQuery query) {
KeycloakPluginLogger.INSTANCE.groupQueryResult(resultLogger.toString());
}

if (query.getOrderingProperties().size() > 0) {
groupList.sort(new GroupComparator(query.getOrderingProperties()));
}

return groupList;
}

Expand Down Expand Up @@ -641,6 +722,10 @@ protected List<Group> requestGroupsWithoutUserId(KeycloakGroupQuery query) {
KeycloakPluginLogger.INSTANCE.groupQueryResult(resultLogger.toString());
}

if (query.getOrderingProperties().size() > 0) {
groupList.sort(new GroupComparator(query.getOrderingProperties()));
}

return groupList;
}

Expand All @@ -660,6 +745,12 @@ protected ResponseEntity<String> requestGroupById(String groupId) throws RestCli
}
}

/**
* Maps a Keycloak JSON result to a Group object
* @param result the Keycloak JSON result
* @return the Group object
* @throws JSONException in case of errors
*/
protected GroupEntity transformGroup(JSONObject result) throws JSONException {
GroupEntity group = new GroupEntity();
group.setId(result.getString("id"));
Expand All @@ -672,6 +763,11 @@ protected GroupEntity transformGroup(JSONObject result) throws JSONException {
return group;
}

/**
* Checks whether a Keycloak JSON result represents a SYSTEM group.
* @param result the Keycloak JSON result
* @return {@code true} in case the result is a SYSTEM group.
*/
private boolean isSystemGroup(JSONObject result) {
String name = result.getString("name");
if (Groups.CAMUNDA_ADMIN.equals(name) ||
Expand All @@ -691,6 +787,59 @@ private boolean isSystemGroup(JSONObject result) {
return false;
}

/**
* Helper for client side group ordering.
*/
private static class GroupComparator implements Comparator<Group> {
private final static int GROUP_ID = 0;
private final static int NAME = 1;
private final static int TYPE = 2;
private int[] order;
private boolean[] desc;
public GroupComparator(List<QueryOrderingProperty> orderList) {
// Prepare query ordering
this.order = new int[orderList.size()];
this.desc = new boolean[orderList.size()];
for (int i = 0; i< orderList.size(); i++) {
QueryOrderingProperty qop = orderList.get(i);
if (qop.getQueryProperty().equals(GroupQueryProperty.GROUP_ID)) {
order[i] = GROUP_ID;
} else if (qop.getQueryProperty().equals(GroupQueryProperty.NAME)) {
order[i] = NAME;
} else if (qop.getQueryProperty().equals(GroupQueryProperty.TYPE)) {
order[i] = TYPE;
} else {
order[i] = -1;
}
desc[i] = Direction.DESCENDING.equals(qop.getDirection());
}
}

@Override
public int compare(Group g1, Group g2) {
int c = 0;
for (int i = 0; i < order.length; i ++) {
switch (order[i]) {
case GROUP_ID:
c = KeycloakIdentityProviderSession.compare(g1.getId(), g2.getId());
break;
case NAME:
c = KeycloakIdentityProviderSession.compare(g1.getName(), g2.getName());
break;
case TYPE:
c = KeycloakIdentityProviderSession.compare(g1.getType(), g2.getType());
break;
default:
// do nothing
}
if (c != 0) {
return desc[i] ? -c : c;
}
}
return c;
}
}

//-------------------------------------------------------------------------
// Tenants
//-------------------------------------------------------------------------
Expand Down Expand Up @@ -761,7 +910,7 @@ protected boolean matches(Object[] queryParameter, Object attribute) {
protected boolean matchesLike(String queryParameter, String attribute) {
return queryParameter == null || attribute.matches(queryParameter.replaceAll("[%\\*]", ".*"));
}

/**
* @return true if the passed-in user is currently authenticated
*/
Expand All @@ -785,4 +934,23 @@ protected boolean isAuthorized(Permission permission, Resource resource, String
.getCommandContext().getAuthorizationManager().isAuthorized(permission, resource, resourceId);
}

/**
* Null safe compare of two strings.
* @param str1 string 1
* @param str2 string 2
* @return 0 if both strings are equal; -1 if string 1 is less, +1 if string 1 is greater than string 2
*/
protected static int compare(final String str1, final String str2) {
if (str1 == str2) {
return 0;
}
if (str1 == null) {
return -1;
}
if (str2 == null) {
return 1;
}
return str1.compareTo(str2);
}

}
Loading

0 comments on commit 0e955e8

Please sign in to comment.