From 8d0dc7afa56b7e269dd4f30a591f792b9929272b Mon Sep 17 00:00:00 2001 From: Piotr Gawron <piotr.gawron@uni.lu> Date: Wed, 18 Jul 2018 09:10:42 +0200 Subject: [PATCH] ldap connector implemented --- .../model/user/ConfigurationElementType.java | 579 +++++++++--------- .../user/ConfigurationElementTypeGroup.java | 1 + pom.xml | 2 + service/pom.xml | 7 + .../java/lcsb/mapviewer/services/UserDTO.java | 49 ++ .../mapviewer/services/impl/LdapService.java | 240 ++++++++ .../services/interfaces/ILdapService.java | 61 ++ .../resources/applicationContext-service.xml | 2 + .../services/impl/LdapServiceTest.java | 135 ++++ service/testFiles/ldap/testdata.ldif | 128 ++++ 10 files changed, 927 insertions(+), 277 deletions(-) create mode 100644 service/src/main/java/lcsb/mapviewer/services/UserDTO.java create mode 100644 service/src/main/java/lcsb/mapviewer/services/impl/LdapService.java create mode 100644 service/src/main/java/lcsb/mapviewer/services/interfaces/ILdapService.java create mode 100644 service/src/test/java/lcsb/mapviewer/services/impl/LdapServiceTest.java create mode 100644 service/testFiles/ldap/testdata.ldif diff --git a/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementType.java b/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementType.java index bc5e5dba08..dae902490a 100644 --- a/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementType.java +++ b/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementType.java @@ -1,277 +1,302 @@ -package lcsb.mapviewer.model.user; - -/** - * This enumerate defines all possible configuration parameter that are - * configurable by the user. - * - * @author Piotr Gawron - * - */ -public enum ConfigurationElementType { - - /** - * Email address used for sending email from the system. - */ - EMAIL_ADDRESS("E-mail address", "your.account@domain.com", ConfigurationElementEditType.EMAIL, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Login for the email account. - */ - EMAIL_LOGIN("E-mail server login", "your@login", ConfigurationElementEditType.STRING, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Password for the email account. - */ - EMAIL_PASSWORD("E-mail server password", "email.secret.password", ConfigurationElementEditType.PASSWORD, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Address of the IMAP server. - */ - EMAIL_IMAP_SERVER("IMAP server", "your.imap.domain.com", ConfigurationElementEditType.STRING, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Address of the SMTP server. - */ - EMAIL_SMTP_SERVER("SMTP server", "your.smtp.domain.com", ConfigurationElementEditType.STRING, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Port used for SMTP connection (sending e-mails). - */ - EMAIL_SMTP_PORT("SMTP port", "25", ConfigurationElementEditType.INTEGER, true, - ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // - - /** - * Default map that should be presented if no map is selected by user side. - */ - DEFAULT_MAP("Default Project Id", "empty", ConfigurationElementEditType.STRING, false, - ConfigurationElementTypeGroup.SERVER_CONFIGURATION), // - - /** - * Logo presented in the system. - */ - LOGO_IMG("Logo icon", "udl.png", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), // - - /** - * Address connected to the logo. - */ - LOGO_LINK("Logo link (after click)", "http://wwwen.uni.lu/", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), // - - /** - * Maximum distance (in pixels) that is allowed during finding closest element - * on the map. - */ - SEARCH_DISTANCE("Max distance for clicking on element (px)", "10", ConfigurationElementEditType.DOUBLE, false, - ConfigurationElementTypeGroup.POINT_AND_CLICK), - - /** - * Email used for requesting an account (in client side). - */ - REQUEST_ACCOUNT_EMAIL("Email used for requesting an account", "your.email@domain.com", - ConfigurationElementEditType.EMAIL, false, ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), - - /** - * Max number of results in search box. - */ - SEARCH_RESULT_NUMBER("Max number of results (this value indicates the max number of elements that will be returned from search - not the number of aggregated elements in the search box).", "100", ConfigurationElementEditType.INTEGER, false, - ConfigurationElementTypeGroup.POINT_AND_CLICK), - - /** - * Google Analytics tracking ID used for statistics. This tracking ID should - * look like "UA-000000-01". More information about tracking ID can be found - * <a href="https://support.google.com/analytics/answer/1032385?hl=en"> here - * </a>. - */ - GOOGLE_ANALYTICS_IDENTIFIER("Google Analytics tracking ID used for statistics", "", - ConfigurationElementEditType.STRING, false, ConfigurationElementTypeGroup.SERVER_CONFIGURATION), - - /** - * Description of the logo presented in the system. - */ - LOGO_TEXT("Logo description", "University of Luxembourg", ConfigurationElementEditType.STRING, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * Domain allowed to connect via x-frame technology. - */ - X_FRAME_DOMAIN("Domain allowed to connect via x-frame technology", "", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.SERVER_CONFIGURATION), - - /** - * Relative directory (in webapps folder) where big files will be stored. - */ - BIG_FILE_STORAGE_DIR("Path to store big files", "minerva-big/", ConfigurationElementEditType.STRING, false, - ConfigurationElementTypeGroup.SERVER_CONFIGURATION), - - /** - * File where legend 1/4 is stored. - */ - LEGEND_FILE_1("Legend 1 image file", "resources/images/legend_a.png", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * File where legend 2/4 is stored. - */ - LEGEND_FILE_2("Legend 2 image file", "resources/images/legend_b.png", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * File where legend 3/4 is stored. - */ - LEGEND_FILE_3("Legend 3 image file", "resources/images/legend_c.png", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * File where legend 4/4 is stored. - */ - LEGEND_FILE_4("Legend 4 image file", "resources/images/legend_d.png", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * File where legend 4/4 is stored. - */ - USER_MANUAL_FILE("User manual file", "resources/other/user_guide.pdf", ConfigurationElementEditType.URL, false, - ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - /** - * Color used for negative overlay values. - */ - MIN_COLOR_VAL("Overlay color for negative values", "FF0000", ConfigurationElementEditType.COLOR, false, - ConfigurationElementTypeGroup.OVERLAYS), - - /** - * Color used for positive overlay values. - */ - MAX_COLOR_VAL("Overlay color for postive values", "0000FF", ConfigurationElementEditType.COLOR, false, - ConfigurationElementTypeGroup.OVERLAYS), - - /** - * Color used for undefined overlay values. - */ - SIMPLE_COLOR_VAL("Overlay color when no values are defined", "00FF00", ConfigurationElementEditType.COLOR, false, - ConfigurationElementTypeGroup.OVERLAYS), - - /** - * Color used for 0 overlay value. - */ - NEUTRAL_COLOR_VAL("Overlay color for value=0", "FFFFFF", ConfigurationElementEditType.COLOR, false, - ConfigurationElementTypeGroup.OVERLAYS), - - /** - * Opacity of data overlay objects in the frontend. - */ - OVERLAY_OPACITY("Opacity used when drwaing data overlays (value between 0.0-1.0)", "0.8", - ConfigurationElementEditType.DOUBLE, false, ConfigurationElementTypeGroup.OVERLAYS), - - /** - * Default content of the email when requesting for an account in the system. - */ - REQUEST_ACCOUNT_DEFAULT_CONTENT("Email content used for requesting an account", - "Dear Disease map team,\nI would like to request an account in the system.\nKind regards", - ConfigurationElementEditType.TEXT, false, ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), - - DEFAULT_VIEW_PROJECT("Default user privilege for: " + PrivilegeType.VIEW_PROJECT.getCommonName(), "true", - ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), - - DEFAULT_EDIT_COMMENTS_PROJECT("Default user privilege for: " + PrivilegeType.EDIT_COMMENTS_PROJECT.getCommonName(), - "false", ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), - - DEFAULT_LAYOUT_MANAGEMENT("Default user privilege for: " + PrivilegeType.LAYOUT_MANAGEMENT.getCommonName(), "false", - ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), - - SHOW_REACTION_TYPE("Show reaction type when browsing map", "true", - ConfigurationElementEditType.BOOLEAN, false, ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - GOOGLE_MAPS_API_KEY("By providing this Google Maps Platform API key I declare that I am aware that " - + "I am a Customer of the Google Maps Platform and I agree to the terms of the <a href=\"https://cloud.google.com/maps-platform/terms/\" target='_blank'>license of Google Maps Platform</a>." + - "In particular, I warrant that neither any of the maps nor publicly available data overlays " - + "(\"General overlays\") on this MINERVA server contain Protected Health Information (as defined in and subject to HIPAA).", - "", ConfigurationElementEditType.STRING, false, ConfigurationElementTypeGroup.SERVER_CONFIGURATION), - - /** - * Terms of use. - */ - TERMS_OF_USE("URL of platform Terms of Use file", "", ConfigurationElementEditType.URL, false, ConfigurationElementTypeGroup.LEGEND_AND_LOGO), - - ; - - /** - * Default value of the configuration parameter (it will be used only when value - * doesn't exist in the DAO). - */ - private String defaultValue = ""; - - /** - * Common name used for visualization (query user). - */ - private String commonName = ""; - - /** - * How we want to edit specific parameter. - */ - private ConfigurationElementEditType editType = null; - - private boolean serverSide = true; - private ConfigurationElementTypeGroup group = null; - - /** - * Default constructor. - * - * @param commonName - * common name used for this parameter - * @param editType - * type defining how we want to edit this configuration parameter - * @param defaultVal - * default value assigned to this parameter - */ - ConfigurationElementType(String commonName, String defaultVal, ConfigurationElementEditType editType, - boolean serverSide, ConfigurationElementTypeGroup group) { - this.defaultValue = defaultVal; - this.commonName = commonName; - this.editType = editType; - this.serverSide = serverSide; - this.group = group; - } - - /** - * @return the defaultValue - * @see #defaultValue - */ - public String getDefaultValue() { - return defaultValue; - } - - /** - * @return the commonName - * @see #commonName - */ - public String getCommonName() { - return commonName; - } - - /** - * @return the editType - * @see #editType - */ - public ConfigurationElementEditType getEditType() { - return editType; - } - - /** - * @return the serverSide - * @see #serverSide - */ - public boolean isServerSide() { - return serverSide; - } - - public ConfigurationElementTypeGroup getGroup() { - return group; - } -} +package lcsb.mapviewer.model.user; + +/** + * This enumerate defines all possible configuration parameter that are + * configurable by the user. + * + * @author Piotr Gawron + * + */ +public enum ConfigurationElementType { + + /** + * Email address used for sending email from the system. + */ + EMAIL_ADDRESS("E-mail address", "your.account@domain.com", ConfigurationElementEditType.EMAIL, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Login for the email account. + */ + EMAIL_LOGIN("E-mail server login", "your@login", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Password for the email account. + */ + EMAIL_PASSWORD("E-mail server password", "email.secret.password", ConfigurationElementEditType.PASSWORD, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Address of the IMAP server. + */ + EMAIL_IMAP_SERVER("IMAP server", "your.imap.domain.com", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Address of the SMTP server. + */ + EMAIL_SMTP_SERVER("SMTP server", "your.smtp.domain.com", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Port used for SMTP connection (sending e-mails). + */ + EMAIL_SMTP_PORT("SMTP port", "25", ConfigurationElementEditType.INTEGER, true, + ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), // + + /** + * Default map that should be presented if no map is selected by user side. + */ + DEFAULT_MAP("Default Project Id", "empty", ConfigurationElementEditType.STRING, false, + ConfigurationElementTypeGroup.SERVER_CONFIGURATION), // + + /** + * Logo presented in the system. + */ + LOGO_IMG("Logo icon", "udl.png", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), // + + /** + * Address connected to the logo. + */ + LOGO_LINK("Logo link (after click)", "http://wwwen.uni.lu/", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), // + + /** + * Maximum distance (in pixels) that is allowed during finding closest element + * on the map. + */ + SEARCH_DISTANCE("Max distance for clicking on element (px)", "10", ConfigurationElementEditType.DOUBLE, false, + ConfigurationElementTypeGroup.POINT_AND_CLICK), + + /** + * Email used for requesting an account (in client side). + */ + REQUEST_ACCOUNT_EMAIL("Email used for requesting an account", "your.email@domain.com", + ConfigurationElementEditType.EMAIL, false, ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), + + /** + * Max number of results in search box. + */ + SEARCH_RESULT_NUMBER( + "Max number of results (this value indicates the max number of elements that will be returned from search - not the number of aggregated elements in the search box).", + "100", ConfigurationElementEditType.INTEGER, false, ConfigurationElementTypeGroup.POINT_AND_CLICK), + + /** + * Google Analytics tracking ID used for statistics. This tracking ID should + * look like "UA-000000-01". More information about tracking ID can be found + * <a href="https://support.google.com/analytics/answer/1032385?hl=en"> here + * </a>. + */ + GOOGLE_ANALYTICS_IDENTIFIER("Google Analytics tracking ID used for statistics", "", + ConfigurationElementEditType.STRING, false, ConfigurationElementTypeGroup.SERVER_CONFIGURATION), + + /** + * Description of the logo presented in the system. + */ + LOGO_TEXT("Logo description", "University of Luxembourg", ConfigurationElementEditType.STRING, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * Domain allowed to connect via x-frame technology. + */ + X_FRAME_DOMAIN("Domain allowed to connect via x-frame technology", "", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.SERVER_CONFIGURATION), + + /** + * Relative directory (in webapps folder) where big files will be stored. + */ + BIG_FILE_STORAGE_DIR("Path to store big files", "minerva-big/", ConfigurationElementEditType.STRING, false, + ConfigurationElementTypeGroup.SERVER_CONFIGURATION), + + /** + * File where legend 1/4 is stored. + */ + LEGEND_FILE_1("Legend 1 image file", "resources/images/legend_a.png", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * File where legend 2/4 is stored. + */ + LEGEND_FILE_2("Legend 2 image file", "resources/images/legend_b.png", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * File where legend 3/4 is stored. + */ + LEGEND_FILE_3("Legend 3 image file", "resources/images/legend_c.png", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * File where legend 4/4 is stored. + */ + LEGEND_FILE_4("Legend 4 image file", "resources/images/legend_d.png", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * File where legend 4/4 is stored. + */ + USER_MANUAL_FILE("User manual file", "resources/other/user_guide.pdf", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + /** + * Color used for negative overlay values. + */ + MIN_COLOR_VAL("Overlay color for negative values", "FF0000", ConfigurationElementEditType.COLOR, false, + ConfigurationElementTypeGroup.OVERLAYS), + + /** + * Color used for positive overlay values. + */ + MAX_COLOR_VAL("Overlay color for postive values", "0000FF", ConfigurationElementEditType.COLOR, false, + ConfigurationElementTypeGroup.OVERLAYS), + + /** + * Color used for undefined overlay values. + */ + SIMPLE_COLOR_VAL("Overlay color when no values are defined", "00FF00", ConfigurationElementEditType.COLOR, false, + ConfigurationElementTypeGroup.OVERLAYS), + + /** + * Color used for 0 overlay value. + */ + NEUTRAL_COLOR_VAL("Overlay color for value=0", "FFFFFF", ConfigurationElementEditType.COLOR, false, + ConfigurationElementTypeGroup.OVERLAYS), + + /** + * Opacity of data overlay objects in the frontend. + */ + OVERLAY_OPACITY("Opacity used when drwaing data overlays (value between 0.0-1.0)", "0.8", + ConfigurationElementEditType.DOUBLE, false, ConfigurationElementTypeGroup.OVERLAYS), + + /** + * Default content of the email when requesting for an account in the system. + */ + REQUEST_ACCOUNT_DEFAULT_CONTENT("Email content used for requesting an account", + "Dear Disease map team,\nI would like to request an account in the system.\nKind regards", + ConfigurationElementEditType.TEXT, false, ConfigurationElementTypeGroup.EMAIL_NOTIFICATION), + + DEFAULT_VIEW_PROJECT("Default user privilege for: " + PrivilegeType.VIEW_PROJECT.getCommonName(), "true", + ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), + + DEFAULT_EDIT_COMMENTS_PROJECT("Default user privilege for: " + PrivilegeType.EDIT_COMMENTS_PROJECT.getCommonName(), + "false", ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), + + DEFAULT_LAYOUT_MANAGEMENT("Default user privilege for: " + PrivilegeType.LAYOUT_MANAGEMENT.getCommonName(), "false", + ConfigurationElementEditType.BOOLEAN, true, ConfigurationElementTypeGroup.DEFAULT_USER_PRIVILEGES), + + SHOW_REACTION_TYPE("Show reaction type when browsing map", "true", ConfigurationElementEditType.BOOLEAN, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + GOOGLE_MAPS_API_KEY("By providing this Google Maps Platform API key I declare that I am aware that " + + "I am a Customer of the Google Maps Platform and I agree to the terms of the <a href=\"https://cloud.google.com/maps-platform/terms/\" target='_blank'>license of Google Maps Platform</a>." + + "In particular, I warrant that neither any of the maps nor publicly available data overlays " + + "(\"General overlays\") on this MINERVA server contain Protected Health Information (as defined in and subject to HIPAA).", + "", ConfigurationElementEditType.STRING, false, ConfigurationElementTypeGroup.SERVER_CONFIGURATION), + + /** + * Terms of use. + */ + TERMS_OF_USE("URL of platform Terms of Use file", "", ConfigurationElementEditType.URL, false, + ConfigurationElementTypeGroup.LEGEND_AND_LOGO), + + LDAP_ADDRESS("LDAP address", "", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_PORT("LDAP port", "389", ConfigurationElementEditType.INTEGER, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_SSL("LDAP uses SSL", "false", ConfigurationElementEditType.BOOLEAN, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_BIND_DN("LDAP bind DN", "", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_PASSWORD("LDAP password", "", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_BASE_DN("LDAP base DN", "", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_OBJECT_CLASS("LDAP filter objectClass", "*", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_FIRST_NAME_ATTRIBUTE("LDAP first name attribute", "givenName", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_LAST_NAME_ATTRIBUTE("LDAP last name attribute", "sn", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + LDAP_EMAIL_ATTRIBUTE("LDAP email attribute", "mail", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION),// + LDAP_FILTER("LDAP filter ", "(memberof=cn=gitlab,cn=groups,cn=accounts,dc=uni,dc=lu)", ConfigurationElementEditType.STRING, true, + ConfigurationElementTypeGroup.LDAP_CONFIGURATION), // + + ; + + /** + * Default value of the configuration parameter (it will be used only when value + * doesn't exist in the DAO). + */ + private String defaultValue = ""; + + /** + * Common name used for visualization (query user). + */ + private String commonName = ""; + + /** + * How we want to edit specific parameter. + */ + private ConfigurationElementEditType editType = null; + + private boolean serverSide = true; + private ConfigurationElementTypeGroup group = null; + + /** + * Default constructor. + * + * @param commonName + * common name used for this parameter + * @param editType + * type defining how we want to edit this configuration parameter + * @param defaultVal + * default value assigned to this parameter + */ + ConfigurationElementType(String commonName, String defaultVal, ConfigurationElementEditType editType, + boolean serverSide, ConfigurationElementTypeGroup group) { + this.defaultValue = defaultVal; + this.commonName = commonName; + this.editType = editType; + this.serverSide = serverSide; + this.group = group; + } + + /** + * @return the defaultValue + * @see #defaultValue + */ + public String getDefaultValue() { + return defaultValue; + } + + /** + * @return the commonName + * @see #commonName + */ + public String getCommonName() { + return commonName; + } + + /** + * @return the editType + * @see #editType + */ + public ConfigurationElementEditType getEditType() { + return editType; + } + + /** + * @return the serverSide + * @see #serverSide + */ + public boolean isServerSide() { + return serverSide; + } + + public ConfigurationElementTypeGroup getGroup() { + return group; + } +} diff --git a/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementTypeGroup.java b/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementTypeGroup.java index baa88ee297..6ca381ccc8 100644 --- a/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementTypeGroup.java +++ b/model/src/main/java/lcsb/mapviewer/model/user/ConfigurationElementTypeGroup.java @@ -7,6 +7,7 @@ public enum ConfigurationElementTypeGroup { OVERLAYS("Overlays"), // POINT_AND_CLICK("Point and click"), // SERVER_CONFIGURATION("Server configuration"),// + LDAP_CONFIGURATION("LDAP configuration"),// ; private String commonName; diff --git a/pom.xml b/pom.xml index be583c05b2..d5d32a9cad 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,8 @@ <cglib.version>2.2.2</cglib.version> + + <unboundid-ldapsdk.version>4.0.6</unboundid-ldapsdk.version> <mockito.version>1.10.19</mockito.version> </properties> diff --git a/service/pom.xml b/service/pom.xml index 683846f0af..f80ec93ad1 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -118,6 +118,13 @@ <version>3.12</version> </dependency> + <!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk --> + <dependency> + <groupId>com.unboundid</groupId> + <artifactId>unboundid-ldapsdk</artifactId> + <version>${unboundid-ldapsdk.version}</version> + </dependency> + <!-- mockito used for testing --> <dependency> <groupId>org.mockito</groupId> diff --git a/service/src/main/java/lcsb/mapviewer/services/UserDTO.java b/service/src/main/java/lcsb/mapviewer/services/UserDTO.java new file mode 100644 index 0000000000..d84f9836e4 --- /dev/null +++ b/service/src/main/java/lcsb/mapviewer/services/UserDTO.java @@ -0,0 +1,49 @@ +package lcsb.mapviewer.services; + +public class UserDTO { + private String login; + private String firstName; + private String lastName; + private String email; + private String bindDn; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getBindDn() { + return bindDn; + } + + public void setBindDn(String bindDn) { + this.bindDn = bindDn; + } +} diff --git a/service/src/main/java/lcsb/mapviewer/services/impl/LdapService.java b/service/src/main/java/lcsb/mapviewer/services/impl/LdapService.java new file mode 100644 index 0000000000..8a2a96dbe3 --- /dev/null +++ b/service/src/main/java/lcsb/mapviewer/services/impl/LdapService.java @@ -0,0 +1,240 @@ +package lcsb.mapviewer.services.impl; + +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +import com.unboundid.ldap.sdk.Attribute; +import com.unboundid.ldap.sdk.BindResult; +import com.unboundid.ldap.sdk.Filter; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.ResultCode; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SearchScope; +import com.unboundid.ldap.sdk.SimpleBindRequest; +import com.unboundid.util.ssl.SSLUtil; +import com.unboundid.util.ssl.TrustAllTrustManager; + +import lcsb.mapviewer.common.exception.InvalidStateException; +import lcsb.mapviewer.model.user.ConfigurationElementType; +import lcsb.mapviewer.services.UserDTO; +import lcsb.mapviewer.services.interfaces.IConfigurationService; +import lcsb.mapviewer.services.interfaces.ILdapService; + +@Transactional(value = "txManager") +public class LdapService implements ILdapService { + Logger logger = Logger.getLogger(LdapService.class); + + @Autowired + private IConfigurationService configurationService; + + protected LDAPConnection getConnection() throws LDAPException { + String address = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_ADDRESS); + if (address == null || address.trim().isEmpty()) { + return null; + } + boolean ssl = "true" + .equalsIgnoreCase(configurationService.getConfigurationValue(ConfigurationElementType.LDAP_SSL)); + + String portString = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_PORT); + if (portString == null || address.trim().isEmpty()) { + if (ssl) { + portString = "636"; + } else { + portString = "389"; + } + } + int port = Integer.parseInt(portString); + LDAPConnection connection; + if (ssl) { + SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager()); + try { + connection = new LDAPConnection(sslUtil.createSSLSocketFactory()); + } catch (GeneralSecurityException e) { + throw new InvalidStateException(e); + } + } else { + connection = new LDAPConnection(); + } + connection.connect(address, port); + + String bindDn = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_BIND_DN); + String password = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_PASSWORD); + + if (bindDn == null || bindDn.trim().isEmpty()) { + connection.bind(new SimpleBindRequest()); + } else { + connection.bind(bindDn, password); + } + + return connection; + } + + @Override + public boolean login(String login, String password) throws LDAPException { + if (!isValidConfiguratio()) { + logger.warn("Invalid LDAP configuration"); + return false; + } + LDAPConnection connection = getConnection(); + UserDTO user = getUserByLogin(login); + if (user != null) { + try { + BindResult result = connection.bind(user.getBindDn(), password); + return result.getResultCode().equals(ResultCode.SUCCESS); + } catch (Exception e) { + return false; + } + } + + return false; + } + + @Override + public List<String> getUsernames() throws LDAPException { + if (!isValidConfiguratio()) { + logger.warn("Invalid LDAP configuration"); + return new ArrayList<>(); + } + List<String> result = new ArrayList<>(); + LDAPConnection connection = getConnection(); + + Filter f2 = createObjectClassFilter(); + Filter f3 = createAttributeFilter(); + + Filter filter = Filter.createANDFilter(f2, f3); + + String baseDn = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_BASE_DN); + SearchResult searchResult = connection.search(baseDn, SearchScope.SUB, filter); + + for (SearchResultEntry entry : searchResult.getSearchEntries()) { + Attribute uid = entry.getAttribute("uid"); + if (uid != null) { + result.add(uid.getValue()); + } else { + logger.warn("Invalid ldap entry: " + entry); + } + } + connection.close(); + + return result; + } + + @Override + public UserDTO getUserByLogin(String login) throws LDAPException { + if (!isValidConfiguratio()) { + logger.warn("Invalid LDAP configuration"); + return null; + } + LDAPConnection connection = getConnection(); + try { + String baseDn = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_BASE_DN); + String firstNameAttribute = configurationService + .getConfigurationValue(ConfigurationElementType.LDAP_FIRST_NAME_ATTRIBUTE); + String lastNameAttribute = configurationService + .getConfigurationValue(ConfigurationElementType.LDAP_LAST_NAME_ATTRIBUTE); + String emailAttribute = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_EMAIL_ATTRIBUTE); + + Filter f1 = createLoginFilter(login); + Filter f2 = createObjectClassFilter(); + Filter f3 = createAttributeFilter(); + + Filter filter = Filter.createANDFilter(f1, f2, f3); + SearchResult searchResult = connection.search(baseDn, SearchScope.SUB, filter); + + for (SearchResultEntry entry : searchResult.getSearchEntries()) { + UserDTO result = new UserDTO(); + result.setBindDn(entry.getDN()); + + Attribute uidAttribute = entry.getAttribute("uid"); + if (uidAttribute != null) { + result.setLogin(uidAttribute.getValue()); + } else { + logger.warn("Invalid ldap entry: " + entry); + } + if (!firstNameAttribute.trim().isEmpty()) { + Attribute firstName = entry.getAttribute(firstNameAttribute); + if (firstName != null) { + result.setFirstName(firstName.getValue()); + } + } + + if (!lastNameAttribute.trim().isEmpty()) { + Attribute lastName = entry.getAttribute(lastNameAttribute); + if (lastName != null) { + result.setLastName(lastName.getValue()); + } + } + + if (!emailAttribute.trim().isEmpty()) { + Attribute emailName = entry.getAttribute(emailAttribute); + if (emailName != null) { + result.setEmail(emailName.getValue()); + } + } + + return result; + } + return null; + } finally { + connection.close(); + } + } + + private Filter createObjectClassFilter() { + String objectClass = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_OBJECT_CLASS); + + if (objectClass == null || objectClass.trim().isEmpty()) { + objectClass = "*"; + } + + return Filter.createEqualityFilter("objectClass", objectClass); + } + + private Filter createAttributeFilter() throws LDAPException { + String ldapStringFilter = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_FILTER); + + if (ldapStringFilter == null || ldapStringFilter.trim().isEmpty()) { + return Filter.create(""); + } + + return Filter.create(ldapStringFilter); + } + + private Filter createLoginFilter(String login) { + return Filter.createEqualityFilter("uid", login); + } + + public IConfigurationService getConfigurationService() { + return configurationService; + } + + public void setConfigurationService(IConfigurationService configurationService) { + this.configurationService = configurationService; + } + + @Override + public boolean isValidConfiguratio() { + try { + String baseDn = configurationService.getConfigurationValue(ConfigurationElementType.LDAP_BASE_DN); + if (baseDn == null || baseDn.trim().isEmpty()) { + return false; + } + LDAPConnection connection = getConnection(); + if (connection != null) { + connection.close(); + return true; + } + return false; + } catch (Exception e) { + logger.error(e, e); + return false; + } + } +} diff --git a/service/src/main/java/lcsb/mapviewer/services/interfaces/ILdapService.java b/service/src/main/java/lcsb/mapviewer/services/interfaces/ILdapService.java new file mode 100644 index 0000000000..d449879390 --- /dev/null +++ b/service/src/main/java/lcsb/mapviewer/services/interfaces/ILdapService.java @@ -0,0 +1,61 @@ +package lcsb.mapviewer.services.interfaces; + +import java.util.List; + +import com.unboundid.ldap.sdk.LDAPException; + +import lcsb.mapviewer.model.user.ConfigurationElementTypeGroup; +import lcsb.mapviewer.services.UserDTO; + +/** + * Connection service to LDAP server. + * + * @author Piotr Gawron + * + */ +public interface ILdapService { + + /** + * Checks if login and password match + * + * @param login + * user login + * @param password + * password + * @return true if user login/password match + * @throws LDAPException + * thrown when there is problem with LDAP connection + */ + boolean login(String login, String password) throws LDAPException; + + /** + * Returns list of user names available in the LDAP server. + * + * @return list of user names + * @throws LDAPException + * thrown when there is problem with LDAP connection + */ + List<String> getUsernames() throws LDAPException; + + /** + * Returns user data information from LDAP for given login. + * + * @param login + * user for which we obtain data + * @return user data information from LDAP for given login or null if such user + * doesn't exist + * @throws LDAPException + * thrown when there is problem with LDAP connection + */ + UserDTO getUserByLogin(String login) throws LDAPException; + + /** + * Checks if LDAP configuration + * ({@link ConfigurationElementTypeGroup#LDAP_CONFIGURATION}) is valid. + * + * @return true if LDAP configuration + * ({@link ConfigurationElementTypeGroup#LDAP_CONFIGURATION}) is valid + */ + boolean isValidConfiguratio(); + +} diff --git a/service/src/main/resources/applicationContext-service.xml b/service/src/main/resources/applicationContext-service.xml index 1cbe2873d1..961c5a7b03 100644 --- a/service/src/main/resources/applicationContext-service.xml +++ b/service/src/main/resources/applicationContext-service.xml @@ -23,6 +23,8 @@ <bean id="LayoutService" class="lcsb.mapviewer.services.impl.LayoutService"/> + <bean id="LdapService" class="lcsb.mapviewer.services.impl.LdapService"/> + <bean id="LogService" class="lcsb.mapviewer.services.impl.LogService"/> <bean id="MiriamService" class="lcsb.mapviewer.services.impl.MiriamService"/> diff --git a/service/src/test/java/lcsb/mapviewer/services/impl/LdapServiceTest.java b/service/src/test/java/lcsb/mapviewer/services/impl/LdapServiceTest.java new file mode 100644 index 0000000000..f1bb417c2a --- /dev/null +++ b/service/src/test/java/lcsb/mapviewer/services/impl/LdapServiceTest.java @@ -0,0 +1,135 @@ +package lcsb.mapviewer.services.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.apache.log4j.Logger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.test.annotation.Rollback; + +import com.unboundid.ldap.listener.InMemoryDirectoryServer; +import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; +import com.unboundid.ldap.sdk.LDAPConnection; + +import lcsb.mapviewer.model.user.ConfigurationElementType; +import lcsb.mapviewer.services.ServiceTestFunctions; +import lcsb.mapviewer.services.UserDTO; + +@Rollback(true) +public class LdapServiceTest extends ServiceTestFunctions { + static Logger logger = Logger.getLogger(LdapServiceTest.class); + + LdapService ldapService; + + @Before + public void setUp() throws Exception { + try { + configurationService.setConfigurationValue(ConfigurationElementType.LDAP_BASE_DN, "dc=uni,dc=lu"); + configurationService.setConfigurationValue(ConfigurationElementType.LDAP_OBJECT_CLASS, "person"); + + ldapService = Mockito.spy(LdapService.class); + ldapService.setConfigurationService(configurationService); + Mockito.when(ldapService.getConnection()).thenAnswer(new Answer<LDAPConnection>() { + + @Override + public LDAPConnection answer(InvocationOnMock invocation) throws Throwable { + // Create the configuration to use for the server. + InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=uni,dc=lu"); + config.addAdditionalBindCredentials("uid=piotr.gawron,cn=users,cn=accounts,dc=uni,dc=lu", "test_passwd"); + config.setSchema(null); + + // Create the directory server instance, populate it with data from the + // "test-data.ldif" file, and start listening for client connections. + InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); + ds.importFromLDIF(true, "testFiles/ldap/testdata.ldif"); + ds.startListening(); + return ds.getConnection(); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testIsValidConfiguration() throws Exception { + try { + assertTrue(ldapService.isValidConfiguratio()); + configurationService.setConfigurationValue(ConfigurationElementType.LDAP_BASE_DN, ""); + assertFalse(ldapService.isValidConfiguratio()); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test + public void testLogin() throws Exception { + try { + assertTrue(ldapService.login("piotr.gawron", "test_passwd")); + assertFalse(ldapService.login("piotr.gawron", "invalid_password")); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test + public void testGetUsernames() throws Exception { + try { + List<String> list = ldapService.getUsernames(); + assertEquals(2, list.size()); + assertTrue(list.contains("piotr.gawron")); + assertFalse(list.contains("john.doe")); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test + public void testGetUsernamesWithFiltering() throws Exception { + try { + configurationService.setConfigurationValue(ConfigurationElementType.LDAP_FILTER, + "(memberof=cn=owncloud,cn=groups,cn=accounts,dc=uni,dc=lu)"); + + List<String> list = ldapService.getUsernames(); + assertEquals(1, list.size()); + assertTrue(list.contains("piotr.gawron")); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Test + public void testGetUserByLogin() throws Exception { + try { + UserDTO user = ldapService.getUserByLogin("piotr.gawron"); + assertEquals("Piotr", user.getFirstName()); + assertEquals("Gawron", user.getLastName()); + assertEquals("piotr.gawron", user.getLogin()); + assertEquals("piotr.gawron@uni.lu", user.getEmail()); + assertNotNull(user.getBindDn()); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + +} diff --git a/service/testFiles/ldap/testdata.ldif b/service/testFiles/ldap/testdata.ldif new file mode 100644 index 0000000000..b319630ec5 --- /dev/null +++ b/service/testFiles/ldap/testdata.ldif @@ -0,0 +1,128 @@ +version: 1 + +dn: dc=uni,dc=lu +objectClass: top +objectClass: domain +dc: uni + +dn: cn=accounts,dc=uni,dc=lu +objectClass: extensibleObject +cn: accounts + +dn: cn=users,cn=accounts,dc=uni,dc=lu +objectClass: extensibleObject +cn: users + +dn: uid=piotr.gawron,cn=users,cn=accounts,dc=uni,dc=lu +objectClass: mepOriginEntry +objectClass: ipaSshGroupOfPubKeys +objectClass: posixaccount +objectClass: inetuser +objectClass: krbprincipalaux +objectClass: krbticketpolicyaux +objectClass: organizationalperson +objectClass: inetorgperson +objectClass: ipasshuser +objectClass: top +objectClass: person +objectClass: ipaobject +cn: Piotr Gawron +gidNumber: 369550501 +homeDirectory: /home/piotr.gawron +ipaUniqueID: adf723e6-20e4-11e5-8907-001a4ae51219 +sn: Gawron +uid: piotr.gawron +uidNumber: 369550501 +displayName: Piotr Gawron +gecos: Piotr Gawron +givenName: Piotr +initials: PG +krbLastPwdChange: 20180608111630Z +krbPasswordExpiration: 20190223111630Z +krbPrincipalName: piotr.gawron@UNI.LU +loginShell: /bin/bash +mail: piotr.gawron@uni.lu +memberOf: cn=lcsb,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: ipaUniqueID=d7549fa2-03a2-11e5-95f9-00163e0a4f7b,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=eff7677e-03a2-11e5-add5-00163e0a4f7b,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: ipaUniqueID=f10ec0f6-7ef6-11e5-957b-001a4ae5121e,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=2cf9b59e-7ef7-11e5-89c0-001a4ae5121e,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: ipaUniqueID=eeb5f68e-9775-11e5-81fb-00163e0a4f7b,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=176f7fb4-9776-11e5-a097-00163e0a4f7b,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: ipaUniqueID=7dda82ae-99e1-11e5-834b-001a4ae5121e,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=a44d842c-99e1-11e5-9c2f-001a4ae5121e,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: cn=webdav-public-minerva,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: ipaUniqueID=1d1f58b8-a247-11e5-ac5d-00163e0a4f7b,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=41f879a8-a247-11e5-9d34-00163e0a4f7b,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: ipaUniqueID=33e9e6a2-c8d1-11e5-b578-001a4ae5121e,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=572fea30-c8d1-11e5-b770-001a4ae5121e,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: cn=lcsb-biocore,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=gitlab,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=xwiki,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=owncloud,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=xwiki-biocore,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=webdav-public-biocore,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=grafana-biocore-gitlab-viewers,cn=groups,cn=accounts,dc=uni,dc= + lu +memberOf: cn=grafana-biocore-icinga-viewers,cn=groups,cn=accounts,dc=uni,dc= + lu +memberOf: ipaUniqueID=634b5286-3781-11e6-93c5-001a4ae5121e,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=7fe6a210-3781-11e6-b515-001a4ae5121e,cn=sudorules,cn=s + udo,dc=uni,dc=lu +memberOf: cn=ncer-pd-ada,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=r3lab-docker,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: ipaUniqueID=390a3ce0-854b-11e6-85c3-001a4ae5127c,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=6d558608-854b-11e6-9aa7-001a4ae5127c,cn=sudorules,cn=s + udo,dc=uni,dc=lu +telephoneNumber: +3524666445526 + +dn: uid=piotr.atyjaszyk,cn=users,cn=accounts,dc=uni,dc=lu +objectClass: mepOriginEntry +objectClass: ipaSshGroupOfPubKeys +objectClass: posixaccount +objectClass: inetuser +objectClass: krbprincipalaux +objectClass: krbticketpolicyaux +objectClass: organizationalperson +objectClass: inetorgperson +objectClass: ipasshuser +objectClass: top +objectClass: person +objectClass: ipaobject +cn: Piotr Matyjaszyk +gidNumber: 369513507 +homeDirectory: /home/piotr.atyjaszyk +ipaUniqueID: ab674608-854a-11e6-8b8d-001a4ae5127c +sn: Matyjaszyk +uid: piotr.atyjaszyk +uidNumber: 369513507 +displayName: Piotr Matyjaszyk +gecos: Piotr Matyjaszyk +givenName: Piotr +initials: PM +krbLastPwdChange: 20161003204712Z +krbPasswordExpiration: 20170531204712Z +krbPrincipalName: piotr.atyjaszyk@UNI.LU +loginShell: /bin/bash +mail: piotrmk1@gmail.com +manager: uid=piotr.gawron,cn=users,cn=accounts,dc=uni,dc=lu +memberOf: cn=gitlab,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: cn=external,cn=groups,cn=accounts,dc=uni,dc=lu +memberOf: ipaUniqueID=929b4e02-854b-11e6-b870-001a4ae5127c,cn=hbac,dc=uni,dc + =lu +memberOf: ipaUniqueID=bf23444e-a1bf-11e6-bf86-001a4ae5121e,cn=sudorules,cn=s + udo,dc=uni,dc=lu -- GitLab