diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 44c53f690fb..b1ea142f7a6 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -336,7 +336,10 @@ public class ApiConstants { public static final String URL = "url"; public static final String USAGE_INTERFACE = "usageinterface"; public static final String USER_DATA = "userdata"; + public static final String USER_FILTER = "userfilter"; public static final String USER_ID = "userid"; + public static final String USER_SOURCE = "usersource"; + public static final String USER_CONFLICT_SOURCE = "conflictingusersource"; public static final String USE_SSL = "ssl"; public static final String USERNAME = "username"; public static final String USER_CONFIGURABLE = "userconfigurable"; diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 68dc31f6708..c4dfe64c739 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -111,6 +111,8 @@ public interface QueryService { ListResponse searchForUsers(ListUsersCmd cmd) throws PermissionDeniedException; + ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException; + ListResponse searchForEvents(ListEventsCmd cmd); ListResponse listTags(ListTagsCmd cmd); diff --git a/plugins/user-authenticators/ldap/pom.xml b/plugins/user-authenticators/ldap/pom.xml index de88530e3a8..fefaba0617d 100644 --- a/plugins/user-authenticators/ldap/pom.xml +++ b/plugins/user-authenticators/ldap/pom.xml @@ -27,12 +27,23 @@ 4.14.0.0-SNAPSHOT ../../pom.xml + + + 2.0.0.AM25 + 1.5 + 1.1.3 + 1.1.3 + 1.1-groovy-2.4 + 0.7 + 4.0.4 + + org.codehaus.gmaven gmaven-plugin - 1.3 + ${gmaven.version} 1.7 @@ -58,7 +69,7 @@ org.codehaus.gmaven.runtime gmaven-runtime-1.7 - 1.3 + ${gmaven.version} org.codehaus.groovy @@ -81,38 +92,126 @@ **/*Spec.groovy **/*Test.java + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + com.btmatthews.maven.plugins ldap-maven-plugin - 1.1.0 + ${ldap-maven.version} 11389 ldap false dc=cloudstack,dc=org 10389 - test/resources/cloudstack.org.ldif + src/test/resources/cloudstack.org.ldif - test + src/test/java + + com.btmatthews.ldapunit + ldapunit + ${ldapunit.version} + org.spockframework spock-core - 1.1-groovy-2.4 + ${groovy.version} test - cglib cglib-nodep test + + org.zapodot + embedded-ldap-junit + ${zapdot.version} + + + com.unboundid + unboundid-ldapsdk + ${unboundedid.version} + test + + + org.mockito + mockito-all + ${cs.mockito.version} + compile + + + junit + junit + ${cs.junit.version} + compile + + + org.apache.directory.server + apacheds-server-integ + ${ads.version} + test + + + + org.apache.directory.shared + shared-ldap-schema + + + + + org.apache.directory.server + apacheds-core-constants + ${ads.version} + compile + + + org.apache.directory.server + apacheds-core-annotations + ${ads.version} + compile + + + org.apache.directory.server + apacheds-core + ${ads.version} + compile + + + org.apache.directory.server + apacheds-protocol-ldap + ${ads.version} + compile + + + org.apache.directory.server + apacheds-jdbm-partition + ${ads.version} + compile + + + org.apache.directory.server + apacheds-ldif-partition + ${ads.version} + compile + + + commons-io + commons-io + ${cs.commons-io.version} + diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/LdapConstants.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/LdapConstants.java new file mode 100644 index 00000000000..21574d57db7 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/LdapConstants.java @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api; + +public interface LdapConstants { + String PRINCIPAL = "principal"; +} diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListUsersCmd.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListUsersCmd.java index b2266dc8fd3..ae601742ed4 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListUsersCmd.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/command/LdapListUsersCmd.java @@ -16,18 +16,26 @@ // under the License. package org.apache.cloudstack.api.command; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; +import com.cloud.domain.Domain; +import com.cloud.user.User; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.context.CallContext; import org.apache.log4j.Logger; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.BaseListCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.response.LdapUserResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserResponse; @@ -38,8 +46,37 @@ import org.apache.cloudstack.query.QueryService; import com.cloud.user.Account; -@APICommand(name = "listLdapUsers", responseObject = LdapUserResponse.class, description = "Lists all LDAP Users", since = "4.2.0", - requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +/** + * a short flow, use plantuml to view (see http://plantuml.com) + * @startuml + * start + * :list ldap users request; + * :get ldap binding; + * if (domain == null) then (true) + * :get global trust domain; + * else (false) + * :get trustdomain for domain; + * endif + * :get ldap users\n using trust domain; + * if (filter == 'NoFilter') then (pass as is) + * elseif (filter == 'AnyDomain') then (anydomain) + * :filterList = all\n\t\tcloudstack\n\t\tusers; + * elseif (filter == 'LocalDomain') + * :filterList = local users\n\t\tfor domain; + * elseif (filter == 'PotentialImport') then (address account\nsynchronisation\nconfigurations) + * :query\n the account\n bindings; + * :check and markup\n ldap users\n for bound OUs\n with usersource; + * else ( unknown value for filter ) + * :throw invalid parameter; + * stop + * endif + * :remove users in filterList\nfrom ldap users list; + * :return remaining; + * stop + * @enduml + */ +@APICommand(name = "listLdapUsers", responseObject = LdapUserResponse.class, description = "Lists LDAP Users according to the specifications from the user request.", since = "4.2.0", + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = {RoleType.Admin,RoleType.DomainAdmin}) public class LdapListUsersCmd extends BaseListCmd { public static final Logger s_logger = Logger.getLogger(LdapListUsersCmd.class.getName()); @@ -47,15 +84,29 @@ public class LdapListUsersCmd extends BaseListCmd { @Inject private LdapManager _ldapManager; - @Inject - private QueryService _queryService; - @Parameter(name = "listtype", - type = CommandType.STRING, - required = false, - description = "Determines whether all ldap users are returned or just non-cloudstack users") + type = CommandType.STRING, + required = false, + description = "Determines whether all ldap users are returned or just non-cloudstack users. This option is deprecated in favour for the more option rich 'userfilter' parameter") + @Deprecated private String listType; + @Parameter(name = ApiConstants.USER_FILTER, + type = CommandType.STRING, + required = false, + since = "4.13", + description = "Determines what type of filter is applied on the list of users returned from LDAP.\n" + + "\tvalid values are\n" + + "\t'NoFilter'\t no filtering is done,\n" + + "\t'LocalDomain'\tusers already in the current or requested domain will be filtered out of the result list,\n" + + "\t'AnyDomain'\tusers that already exist anywhere in cloudstack will be filtered out, and\n" + + "\t'PotentialImport'\tall users that would be automatically imported from the listing will be shown," + + " including those that are already in cloudstack, the later will be annotated with their userSource") + private String userFilter; + + @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = false, entityType = DomainResponse.class, description = "linked domain") + private Long domainId; + public LdapListUsersCmd() { super(); } @@ -66,27 +117,35 @@ public class LdapListUsersCmd extends BaseListCmd { _queryService = queryService; } + /** + * (as a check for isACloudstackUser is done) only non cloudstack users should be shown + * @param users a list of {@code LdapUser}s + * @return a (filtered?) list of user response objects + */ private List createLdapUserResponse(final List users) { final List ldapResponses = new ArrayList(); for (final LdapUser user : users) { - if (getListType().equals("all") || !isACloudstackUser(user)) { - final LdapUserResponse ldapResponse = _ldapManager.createLdapUserResponse(user); - ldapResponse.setObjectName("LdapUser"); - ldapResponses.add(ldapResponse); - } + final LdapUserResponse ldapResponse = _ldapManager.createLdapUserResponse(user); + ldapResponse.setObjectName("LdapUser"); + ldapResponses.add(ldapResponse); } return ldapResponses; } + private List cloudstackUsers = null; + @Override public void execute() throws ServerApiException { - List ldapResponses = null; + cloudstackUsers = null; + List ldapResponses = new ArrayList(); final ListResponse response = new ListResponse(); try { - final List users = _ldapManager.getUsers(null); + final List users = _ldapManager.getUsers(domainId); ldapResponses = createLdapUserResponse(users); +// now filter and annotate + ldapResponses = applyUserFilter(ldapResponses); } catch (final NoLdapUserMatchingQueryException ex) { - ldapResponses = new ArrayList(); + // ok, we'll make do with the empty list ldapResponses = new ArrayList(); } finally { response.setResponses(ldapResponses); response.setResponseName(getCommandName()); @@ -94,6 +153,43 @@ public class LdapListUsersCmd extends BaseListCmd { } } + /** + * get a list of relevant cloudstack users, depending on the userFilter + */ + private List getCloudstackUsers() { + if (cloudstackUsers == null) { + try { + cloudstackUsers = getUserFilter().getCloudstackUserList(this).getResponses(); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("error in program login; we are not filtering but still querying users to filter???", e); + } + traceUserList(); + } + return cloudstackUsers; + } + + private void traceUserList() { + if(s_logger.isTraceEnabled()) { + StringBuilder users = new StringBuilder(); + for (UserResponse user : cloudstackUsers) { + if (users.length()> 0) { + users.append(", "); + } + users.append(user.getUsername()); + } + + s_logger.trace(String.format("checking against %d cloudstackusers: %s.", this.cloudstackUsers.size(), users.toString())); + } + } + + private List applyUserFilter(List ldapResponses) { + if(s_logger.isTraceEnabled()) { + s_logger.trace(String.format("applying filter: %s or %s.", this.getListTypeString(), this.getUserFilter())); + } + List responseList = getUserFilter().filter(this,ldapResponses); + return responseList; + } + @Override public String getCommandName() { return s_name; @@ -104,20 +200,306 @@ public class LdapListUsersCmd extends BaseListCmd { return Account.ACCOUNT_ID_SYSTEM; } - private String getListType() { + String getListTypeString() { return listType == null ? "all" : listType; } - private boolean isACloudstackUser(final LdapUser ldapUser) { - final ListResponse response = _queryService.searchForUsers(new ListUsersCmd()); - final List cloudstackUsers = response.getResponses(); - if (cloudstackUsers != null && cloudstackUsers.size() != 0) { - for (final UserResponse cloudstackUser : response.getResponses()) { + String getUserFilterString() { + return userFilter == null ? getListTypeString() == null ? "NoFilter" : getListTypeString().equals("all") ? "NoFilter" : "AnyDomain" : userFilter; + } + + UserFilter getUserFilter() { + return UserFilter.fromString(getUserFilterString()); + } + + boolean isACloudstackUser(final LdapUser ldapUser) { + boolean rc = false; + final List cloudstackUsers = getCloudstackUsers(); + if (cloudstackUsers != null) { + for (final UserResponse cloudstackUser : cloudstackUsers) { if (ldapUser.getUsername().equals(cloudstackUser.getUsername())) { + if(s_logger.isTraceEnabled()) { + s_logger.trace(String.format("found user %s in cloudstack", ldapUser.getUsername())); + } + + rc = true; + } else { + if(s_logger.isTraceEnabled()) { + s_logger.trace(String.format("ldap user %s does not match cloudstack user", ldapUser.getUsername(), cloudstackUser.getUsername())); + } + } + } + } + return rc; + } + + boolean isACloudstackUser(final LdapUserResponse ldapUser) { + if(s_logger.isTraceEnabled()) { + s_logger.trace("checking response : " + ldapUser.toString()); + } + final List cloudstackUsers = getCloudstackUsers(); + if (cloudstackUsers != null && cloudstackUsers.size() != 0) { + for (final UserResponse cloudstackUser : cloudstackUsers) { + if (ldapUser.getUsername().equals(cloudstackUser.getUsername())) { + if(s_logger.isTraceEnabled()) { + s_logger.trace(String.format("found user %s in cloudstack", ldapUser.getUsername())); + } return true; + } else { + if(s_logger.isTraceEnabled()) { + s_logger.trace(String.format("ldap user %s does not match cloudstack user", ldapUser.getUsername(), cloudstackUser.getUsername())); + } } } } return false; } + /** + * typecheck for userfilter values and filter type dependend functionalities. + * This could have been in two switch statements elsewhere in the code. + * Arguably this is a cleaner solution. + */ + enum UserFilter { + NO_FILTER("NoFilter"){ + @Override public List filter(LdapListUsersCmd cmd, List input) { + return cmd.filterNoFilter(input); + } + + /** + * in case of no filter we should find all users in the current domain for annotation. + */ + @Override public ListResponse getCloudstackUserList(LdapListUsersCmd cmd) { + return cmd._queryService.searchForUsers(cmd.domainId,true); + + } + }, + LOCAL_DOMAIN("LocalDomain"){ + @Override public List filter(LdapListUsersCmd cmd, List input) { + return cmd.filterLocalDomain(input); + } + + /** + * if we are filtering for local domain, only get users for the current domain + */ + @Override public ListResponse getCloudstackUserList(LdapListUsersCmd cmd) { + return cmd._queryService.searchForUsers(cmd.domainId,false); + } + }, + ANY_DOMAIN("AnyDomain"){ + @Override public List filter(LdapListUsersCmd cmd, List input) { + return cmd.filterAnyDomain(input); + } + + /* + * if we are filtering for any domain, get recursive all users for the root domain + */ + @Override public ListResponse getCloudstackUserList(LdapListUsersCmd cmd) { + return cmd._queryService.searchForUsers(CallContext.current().getCallingAccount().getDomainId(), true); + } + }, + POTENTIAL_IMPORT("PotentialImport"){ + @Override public List filter(LdapListUsersCmd cmd, List input) { + return cmd.filterPotentialImport(input); + } + + /** + * if we are filtering for potential imports, + * we are only looking for users in the linked domains/accounts, + * which is only relevant if we ask ldap users for this domain. + * So we are asking for all users in the current domain as well + */ + @Override public ListResponse getCloudstackUserList(LdapListUsersCmd cmd) { + return cmd._queryService.searchForUsers(cmd.domainId,false); + } + }; + + private final String value; + + UserFilter(String val) { + this.value = val; + } + + public abstract List filter(LdapListUsersCmd cmd, List input); + + public abstract ListResponse getCloudstackUserList(LdapListUsersCmd cmd); + + static UserFilter fromString(String val) { + if(NO_FILTER.toString().equalsIgnoreCase(val)) { + return NO_FILTER; + } else if (LOCAL_DOMAIN.toString().equalsIgnoreCase(val)) { + return LOCAL_DOMAIN; + } else if(ANY_DOMAIN.toString().equalsIgnoreCase(val)) { + return ANY_DOMAIN; + } else if(POTENTIAL_IMPORT.toString().equalsIgnoreCase(val)) { + return POTENTIAL_IMPORT; + } else { + throw new IllegalArgumentException(String.format("%s is not a legal 'UserFilter' value", val)); + } + } + + @Override public String toString() { + return value; + } + } + + /** + * no filtering but improve with annotation of source for existing ACS users + * @param input ldap response list of users + * @return unfiltered list of the input list of ldap users + */ + public List filterNoFilter(List input) { + if(s_logger.isTraceEnabled()) { + s_logger.trace("returning unfiltered list of ldap users"); + } + annotateUserListWithSources(input); + return input; + } + + /** + * filter the list of ldap users. no users visible to the caller should be in the returned list + * @param input ldap response list of users + * @return a list of ldap users not already in ACS + */ + public List filterAnyDomain(List input) { + if(s_logger.isTraceEnabled()) { + s_logger.trace("filtering existing users"); + } + final List ldapResponses = new ArrayList(); + for (final LdapUserResponse user : input) { + if (isNotAlreadyImportedInTheCurrentDomain(user)) { + ldapResponses.add(user); + } + } + annotateUserListWithSources(ldapResponses); + + return ldapResponses; + } + + /** + * @return true unless the the user is imported in the specified cloudstack domain from LDAP + */ + private boolean isNotAlreadyImportedInTheCurrentDomain(LdapUserResponse user) { + UserResponse cloudstackUser = getCloudstackUser(user); + String domainId = getCurrentDomainId(); + + return cloudstackUser == null /*doesn't exist in cloudstack*/ + || ! ( + cloudstackUser.getUserSource().equalsIgnoreCase(User.Source.LDAP.toString()) + && domainId.equals(cloudstackUser.getDomainId())); /* is from another source */ + } + + /** + * filter the list of ldap users. no users visible to the caller already in the domain specified should be in the returned list + * @param input ldap response list of users + * @return a list of ldap users not already in ACS + */ + public List filterLocalDomain(List input) { + if(s_logger.isTraceEnabled()) { + s_logger.trace("filtering local domain users"); + } + final List ldapResponses = new ArrayList(); + String domainId = getCurrentDomainId(); + for (final LdapUserResponse user : input) { + UserResponse cloudstackUser = getCloudstackUser(user); + if (cloudstackUser == null /*doesn't exist in cloudstack*/ + || !domainId.equals(cloudstackUser.getDomainId()) /* doesn't exist in this domain */ + || !cloudstackUser.getUserSource().equalsIgnoreCase(User.Source.LDAP.toString()) /* is from another source */ + ) { + ldapResponses.add(user); + } + } + annotateUserListWithSources(ldapResponses); + return ldapResponses; + } + + private String getCurrentDomainId() { + String domainId = null; + if (this.domainId != null) { + Domain domain = _domainService.getDomain(this.domainId); + domainId = domain.getUuid(); + } else { + final CallContext callContext = CallContext.current(); + domainId = _domainService.getDomain(callContext.getCallingAccount().getDomainId()).getUuid(); + } + return domainId; + } + + /** + * + * @param input a list of ldap users + * @return annotated list of the users of the input list, that will be automatically imported or synchronised + */ + public List filterPotentialImport(List input) { + if(s_logger.isTraceEnabled()) { + s_logger.trace("should be filtering potential imports!!!"); + } + // functional possibility do not add only users not yet in cloudstack but include users that would be moved if they are so in ldap? + // this means if they are part of a account linked to an ldap group/ou + input.removeIf(ldapUser -> + ( + (isACloudstackUser(ldapUser)) + && (getCloudstackUser(ldapUser).getUserSource().equalsIgnoreCase(User.Source.LDAP.toString())) + ) + ); + annotateUserListWithSources(input); + return input; + } + + private void annotateUserListWithSources(List input) { + for (final LdapUserResponse user : input) { + annotateCloudstackSource(user); + } + } + + private void annotateCloudstackSource(LdapUserResponse user) { + final UserResponse cloudstackUser = getCloudstackUser(user); + if (cloudstackUser != null) { + user.setUserSource(cloudstackUser.getUserSource()); + } else { + user.setUserSource(""); + } + } + + private UserResponse getCloudstackUser(LdapUserResponse user) { + UserResponse returnObject = null; + final List cloudstackUsers = getCloudstackUsers(); + if (cloudstackUsers != null) { + for (final UserResponse cloudstackUser : cloudstackUsers) { + if (user.getUsername().equals(cloudstackUser.getUsername())) { + returnObject = cloudstackUser; + if (returnObject.getDomainId() == this.getCurrentDomainId()) { + break; + } + } + } + } + return returnObject; + } + + private void checkFilterMethodType(Type returnType) { + String msg = null; + if (returnType instanceof ParameterizedType) { + ParameterizedType type = (ParameterizedType) returnType; + if(type.getRawType().equals(List.class)) { + Type[] typeArguments = type.getActualTypeArguments(); + if (typeArguments.length == 1) { + if (typeArguments[0].equals(LdapUserResponse.class)) { + // we're good' + } else { + msg = new String("list of return type contains " + typeArguments[0].getTypeName()); + } + } else { + msg = String.format("type %s has to the wrong number of arguments", type.getRawType()); + } + } else { + msg = String.format("type %s is not a List<>", type.getTypeName()); + } + } else { + msg = new String("can't even begin to explain; review your method signature"); + } + if(msg != null) { + throw new IllegalArgumentException(msg); + } + } + } \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapUserResponse.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapUserResponse.java index 5648a55cb48..e8a4229b4b5 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapUserResponse.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/api/response/LdapUserResponse.java @@ -18,35 +18,41 @@ package org.apache.cloudstack.api.response; import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseResponse; import com.cloud.serializer.Param; +import org.apache.cloudstack.api.LdapConstants; public class LdapUserResponse extends BaseResponse { - @SerializedName("email") + @SerializedName(ApiConstants.EMAIL) @Param(description = "The user's email") private String email; - @SerializedName("principal") + @SerializedName(LdapConstants.PRINCIPAL) @Param(description = "The user's principle") private String principal; - @SerializedName("firstname") + @SerializedName(ApiConstants.FIRSTNAME) @Param(description = "The user's firstname") private String firstname; - @SerializedName("lastname") + @SerializedName(ApiConstants.LASTNAME) @Param(description = "The user's lastname") private String lastname; - @SerializedName("username") + @SerializedName(ApiConstants.USERNAME) @Param(description = "The user's username") private String username; - @SerializedName("domain") + @SerializedName(ApiConstants.DOMAIN) @Param(description = "The user's domain") private String domain; + @SerializedName(ApiConstants.USER_CONFLICT_SOURCE) + @Param(description = "The authentication source for this user as known to the system or empty if the user is not yet in cloudstack.") + private String userSource; + public LdapUserResponse() { super(); } @@ -61,6 +67,11 @@ public class LdapUserResponse extends BaseResponse { this.domain = domain; } + public LdapUserResponse(final String username, final String email, final String firstname, final String lastname, final String principal, String domain, String userSource) { + this(username, email, firstname, lastname, principal, domain); + setUserSource(userSource); + } + public String getEmail() { return email; } @@ -85,6 +96,10 @@ public class LdapUserResponse extends BaseResponse { return domain; } + public String getUserSource() { + return userSource; + } + public void setEmail(final String email) { this.email = email; } @@ -108,4 +123,67 @@ public class LdapUserResponse extends BaseResponse { public void setDomain(String domain) { this.domain = domain; } + + public void setUserSource(String userSource) { + this.userSource = userSource; + } + + public String toString() { + final String COLUMN = ": "; + final String COMMA = ", "; + StringBuilder selfRepresentation = new StringBuilder(); + selfRepresentation.append(this.getClass().getName()); + selfRepresentation.append('{'); + boolean hascontent = false; + if (this.getUsername() != null) { + selfRepresentation.append(ApiConstants.USERNAME); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getUsername()); + hascontent = true; + } + if (this.getFirstname() != null) { + if(hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(ApiConstants.FIRSTNAME); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getFirstname()); + hascontent = true; + } + if (this.getLastname() != null) { + if(hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(ApiConstants.LASTNAME); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getLastname()); + hascontent = true; + } + if(this.getDomain() != null) { + if(hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(ApiConstants.DOMAIN); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getDomain()); + hascontent = true; + } + if (this.getEmail() != null) { + if(hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(ApiConstants.EMAIL); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getEmail()); + hascontent = true; + } + if (this.getPrincipal() != null) { + if(hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(LdapConstants.PRINCIPAL); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getPrincipal()); + hascontent = true; + } + if (this.getUserSource() != null) { + if (hascontent) selfRepresentation.append(COMMA); + selfRepresentation.append(ApiConstants.USER_CONFLICT_SOURCE); + selfRepresentation.append(COLUMN); + selfRepresentation.append(this.getUserSource()); + } + selfRepresentation.append('}'); + + return selfRepresentation.toString(); + } } \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java index 2d8fe530d9d..2cd035e3a99 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapAuthenticator.java @@ -38,7 +38,7 @@ import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; public class LdapAuthenticator extends AdapterBase implements UserAuthenticator { - private static final Logger s_logger = Logger.getLogger(LdapAuthenticator.class.getName()); + private static final Logger LOGGER = Logger.getLogger(LdapAuthenticator.class.getName()); @Inject private LdapManager _ldapManager; @@ -61,32 +61,51 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator public Pair authenticate(final String username, final String password, final Long domainId, final Map requestParameters) { Pair rc = new Pair(false, null); - // TODO not allowing an empty password is a policy we shouldn't decide on. A private cloud may well want to allow this. - if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { - s_logger.debug("Username or Password cannot be empty"); - return rc; + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Retrieving ldap user: " + username); } - if (_ldapManager.isLdapEnabled()) { - final UserAccount user = _userAccountDao.getUserAccount(username, domainId); - List ldapTrustMapVOs = _ldapManager.getDomainLinkage(domainId); - if(ldapTrustMapVOs != null && ldapTrustMapVOs.size() > 0) { - if(ldapTrustMapVOs.size() == 1 && ldapTrustMapVOs.get(0).getAccountId() == 0) { - // We have a single mapping of a domain to an ldap group or ou - return authenticate(username, password, domainId, user, ldapTrustMapVOs.get(0)); - } else { - // we are dealing with mapping of accounts in a domain to ldap groups - return authenticate(username, password, domainId, user, ldapTrustMapVOs); + // TODO not allowing an empty password is a policy we shouldn't decide on. A private cloud may well want to allow this. + if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { + if (_ldapManager.isLdapEnabled(domainId) || _ldapManager.isLdapEnabled()) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("LDAP is enabled in the ldapManager"); + } + final UserAccount user = _userAccountDao.getUserAccount(username, domainId); + if (user != null && ! User.Source.LDAP.equals(user.getSource())) { + return rc; + } + List ldapTrustMapVOs = getLdapTrustMapVOS(domainId); + if(ldapTrustMapVOs != null && ldapTrustMapVOs.size() > 0) { + if(ldapTrustMapVOs.size() == 1 && ldapTrustMapVOs.get(0).getAccountId() == 0) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("We have a single mapping of a domain to an ldap group or ou"); + } + rc = authenticate(username, password, domainId, user, ldapTrustMapVOs.get(0)); + } else { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("we are dealing with mapping of accounts in a domain to ldap groups"); + } + rc = authenticate(username, password, domainId, user, ldapTrustMapVOs); + } + } else { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace(String.format("'this' domain (%d) is not linked to ldap follow normal authentication", domainId)); + } + rc = authenticate(username, password, domainId, user); } - } else { - //domain is not linked to ldap follow normal authentication - return authenticate(username, password, domainId, user); } + } else { + LOGGER.debug("Username or Password cannot be empty"); } return rc; } + private List getLdapTrustMapVOS(Long domainId) { + return _ldapManager.getDomainLinkage(domainId); + } + /** * checks if the user exists in ldap and create in cloudstack if needed. * @@ -97,13 +116,16 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator * @param ldapTrustMapVOs the trust mappings of accounts in the domain to ldap groups * @return false if the ldap user object does not exist, is not mapped to an account, is mapped to multiple accounts or if authenitication fails */ - private Pair authenticate(String username, String password, Long domainId, UserAccount userAccount, List ldapTrustMapVOs) { + Pair authenticate(String username, String password, Long domainId, UserAccount userAccount, List ldapTrustMapVOs) { Pair rc = new Pair(false, null); try { LdapUser ldapUser = _ldapManager.getUser(username, domainId); List memberships = ldapUser.getMemberships(); + tracelist("memberships for " + username, memberships); List mappedGroups = getMappedGroups(ldapTrustMapVOs); + tracelist("mappedgroups for " + username, mappedGroups); mappedGroups.retainAll(memberships); + tracelist("actual groups for " + username, mappedGroups); // check membership, there must be only one match in this domain if(ldapUser.isDisabled()) { logAndDisable(userAccount, "attempt to log on using disabled ldap user " + userAccount.getUsername(), false); @@ -115,9 +137,16 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator // a valid ldap configured user exists LdapTrustMapVO mapping = _ldapManager.getLinkedLdapGroup(domainId,mappedGroups.get(0)); // we could now assert that ldapTrustMapVOs.contains(mapping); - // createUser in Account can only be done by account name not by account id - String accountName = _accountManager.getAccount(mapping.getAccountId()).getAccountName(); + // createUser in Account can only be done by account name not by account id; + Account account = _accountManager.getAccount(mapping.getAccountId()); + if(null == account) { + throw new CloudRuntimeException(String.format("account for user (%s) not found by id %d", username, mapping.getAccountId())); + } + String accountName = account.getAccountName(); rc.first(_ldapManager.canAuthenticate(ldapUser.getPrincipal(), password, domainId)); + if (! rc.first()) { + rc.second(ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); + } // for security reasons we keep processing on faulty login attempt to not give a way information on userid existence if (userAccount == null) { // new user that is in ldap; authenticate and create @@ -146,16 +175,29 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator } } } catch (NoLdapUserMatchingQueryException e) { - s_logger.debug(e.getMessage()); + LOGGER.debug(e.getMessage()); disableUserInCloudStack(userAccount); } return rc; } + private void tracelist(String msg, List listToTrace) { + if (LOGGER.isTraceEnabled()) { + StringBuilder logMsg = new StringBuilder(); + logMsg.append(msg); + logMsg.append(':'); + for (String listMember : listToTrace) { + logMsg.append(' '); + logMsg.append(listMember); + } + LOGGER.trace(logMsg.toString()); + } + } + private void logAndDisable(UserAccount userAccount, String msg, boolean remove) { - if (s_logger.isInfoEnabled()) { - s_logger.info(msg); + if (LOGGER.isInfoEnabled()) { + LOGGER.info(msg); } if(remove) { removeUserInCloudStack(userAccount); @@ -164,7 +206,7 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator } } - private List getMappedGroups(List ldapTrustMapVOs) { + List getMappedGroups(List ldapTrustMapVOs) { List groups = new ArrayList<>(); for (LdapTrustMapVO vo : ldapTrustMapVOs) { groups.add(vo.getName()); @@ -188,7 +230,9 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator final short accountType = ldapTrustMapVO.getAccountType(); processLdapUser(password, domainId, user, rc, ldapUser, accountType); } catch (NoLdapUserMatchingQueryException e) { - s_logger.debug(e.getMessage()); + LOGGER.debug(e.getMessage()); + // no user in ldap ==>> disable user in cloudstack + disableUserInCloudStack(user); } return rc; } @@ -229,12 +273,16 @@ public class LdapAuthenticator extends AdapterBase implements UserAuthenticator if(!ldapUser.isDisabled()) { result = _ldapManager.canAuthenticate(ldapUser.getPrincipal(), password, domainId); } else { - s_logger.debug("user with principal "+ ldapUser.getPrincipal() + " is disabled in ldap"); + LOGGER.debug("user with principal "+ ldapUser.getPrincipal() + " is disabled in ldap"); } } catch (NoLdapUserMatchingQueryException e) { - s_logger.debug(e.getMessage()); + LOGGER.debug(e.getMessage()); } } + return processResultAndAction(user, result); + } + + private Pair processResultAndAction(UserAccount user, boolean result) { return (!result && user != null) ? new Pair(result, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT): new Pair(result, null); diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManager.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManager.java index 2dceae1db32..fa337bc0d4d 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManager.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManager.java @@ -38,7 +38,6 @@ public interface LdapManager extends PluggableService { LdapConfigurationResponse addConfiguration(final LdapAddConfigurationCmd cmd) throws InvalidParameterValueException; - @Deprecated LdapConfigurationResponse addConfiguration(String hostname, int port, Long domainId) throws InvalidParameterValueException; boolean canAuthenticate(String principal, String password, final Long domainId); @@ -62,6 +61,8 @@ public interface LdapManager extends PluggableService { boolean isLdapEnabled(); + boolean isLdapEnabled(long domainId); + Pair, Integer> listConfigurations(LdapListConfigurationCmd cmd); List searchUsers(String query) throws NoLdapUserMatchingQueryException; diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java index 547c10b7b1d..910d06eade8 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapManagerImpl.java @@ -25,6 +25,7 @@ import javax.naming.NamingException; import javax.naming.ldap.LdapContext; import java.util.UUID; +import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.api.LdapValidator; import org.apache.cloudstack.api.command.LDAPConfigCmd; import org.apache.cloudstack.api.command.LDAPRemoveCmd; @@ -57,7 +58,7 @@ import com.cloud.utils.Pair; @Component public class LdapManagerImpl implements LdapManager, LdapValidator { - private static final Logger s_logger = Logger.getLogger(LdapManagerImpl.class.getName()); + private static final Logger LOGGER = Logger.getLogger(LdapManagerImpl.class.getName()); @Inject private LdapConfigurationDao _ldapConfigurationDao; @@ -79,14 +80,13 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { @Inject LdapTrustMapDao _ldapTrustMapDao; - public LdapManagerImpl() { super(); } public LdapManagerImpl(final LdapConfigurationDao ldapConfigurationDao, final LdapContextFactory ldapContextFactory, final LdapUserManagerFactory ldapUserManagerFactory, final LdapConfiguration ldapConfiguration) { - super(); + this(); _ldapConfigurationDao = ldapConfigurationDao; _ldapContextFactory = ldapContextFactory; _ldapUserManagerFactory = ldapUserManagerFactory; @@ -118,10 +118,10 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { context = _ldapContextFactory.createBindContext(providerUrl,domainId); configuration = new LdapConfigurationVO(hostname, port, domainId); _ldapConfigurationDao.persist(configuration); - s_logger.info("Added new ldap server with url: " + providerUrl + (domainId == null ? "": " for domain " + domainId)); + LOGGER.info("Added new ldap server with url: " + providerUrl + (domainId == null ? "": " for domain " + domainId)); return createLdapConfigurationResponse(configuration); } catch (NamingException | IOException e) { - s_logger.debug("NamingException while doing an LDAP bind", e); + LOGGER.debug("NamingException while doing an LDAP bind", e); throw new InvalidParameterValueException("Unable to bind to the given LDAP server"); } finally { closeContext(context); @@ -142,12 +142,15 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { public boolean canAuthenticate(final String principal, final String password, final Long domainId) { try { // TODO return the right account for this user - final LdapContext context = _ldapContextFactory.createUserContext(principal, password,domainId); + final LdapContext context = _ldapContextFactory.createUserContext(principal, password, domainId); closeContext(context); + if(LOGGER.isTraceEnabled()) { + LOGGER.trace(String.format("User(%s) authenticated for domain(%s)", principal, domainId)); + } return true; - } catch (NamingException | IOException e) { - s_logger.debug("Exception while doing an LDAP bind for user "+" "+principal, e); - s_logger.info("Failed to authenticate user: " + principal + ". incorrect password."); + } catch (NamingException | IOException e) {/* AuthenticationException is caught as NamingException */ + LOGGER.debug("Exception while doing an LDAP bind for user "+" "+principal, e); + LOGGER.info("Failed to authenticate user: " + principal + ". incorrect password."); return false; } } @@ -158,7 +161,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { context.close(); } } catch (final NamingException e) { - s_logger.warn(e.getMessage(), e); + LOGGER.warn(e.getMessage(), e); } } @@ -166,7 +169,10 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { public LdapConfigurationResponse createLdapConfigurationResponse(final LdapConfigurationVO configuration) { String domainUuid = null; if(configuration.getDomainId() != null) { - domainUuid = domainDao.findById(configuration.getDomainId()).getUuid(); + DomainVO domain = domainDao.findById(configuration.getDomainId()); + if (domain != null) { + domainUuid = domain.getUuid(); + } } return new LdapConfigurationResponse(configuration.getHostname(), configuration.getPort(), domainUuid); } @@ -199,7 +205,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { throw new InvalidParameterValueException("Cannot find configuration with hostname " + hostname); } else { _ldapConfigurationDao.remove(configuration.getId()); - s_logger.info("Removed ldap server with url: " + hostname + ':' + port + (domainId == null ? "" : " for domain id " + domainId)); + LOGGER.info("Removed ldap server with url: " + hostname + ':' + port + (domainId == null ? "" : " for domain id " + domainId)); return createLdapConfigurationResponse(configuration); } } @@ -231,7 +237,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { return _ldapUserManagerFactory.getInstance(_ldapConfiguration.getLdapProvider(null)).getUser(escapedUsername, context, domainId); } catch (NamingException | IOException e) { - s_logger.debug("ldap Exception: ",e); + LOGGER.debug("ldap Exception: ",e); throw new NoLdapUserMatchingQueryException("No Ldap User found for username: "+username); } finally { closeContext(context); @@ -244,9 +250,15 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { try { context = _ldapContextFactory.createBindContext(domainId); final String escapedUsername = LdapUtils.escapeLDAPSearchFilter(username); - return _ldapUserManagerFactory.getInstance(_ldapConfiguration.getLdapProvider(null)).getUser(escapedUsername, type, name, context, domainId); + LdapUserManager.Provider ldapProvider = _ldapConfiguration.getLdapProvider(domainId); + if (ldapProvider == null) { + // feeble second attempt? + ldapProvider = _ldapConfiguration.getLdapProvider(null); + } + LdapUserManager userManagerFactory = _ldapUserManagerFactory.getInstance(ldapProvider); + return userManagerFactory.getUser(escapedUsername, type, name, context, domainId); } catch (NamingException | IOException e) { - s_logger.debug("ldap Exception: ",e); + LOGGER.debug("ldap Exception: ",e); throw new NoLdapUserMatchingQueryException("No Ldap User found for username: "+username + " in group: " + name + " of type: " + type); } finally { closeContext(context); @@ -260,7 +272,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { context = _ldapContextFactory.createBindContext(domainId); return _ldapUserManagerFactory.getInstance(_ldapConfiguration.getLdapProvider(domainId)).getUsers(context, domainId); } catch (NamingException | IOException e) { - s_logger.debug("ldap Exception: ",e); + LOGGER.debug("ldap Exception: ",e); throw new NoLdapUserMatchingQueryException("*"); } finally { closeContext(context); @@ -274,7 +286,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { context = _ldapContextFactory.createBindContext(domainId); return _ldapUserManagerFactory.getInstance(_ldapConfiguration.getLdapProvider(domainId)).getUsersInGroup(groupName, context, domainId); } catch (NamingException | IOException e) { - s_logger.debug("ldap NamingException: ",e); + LOGGER.debug("ldap NamingException: ",e); throw new NoLdapUserMatchingQueryException("groupName=" + groupName); } finally { closeContext(context); @@ -286,6 +298,13 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { return listConfigurations(new LdapListConfigurationCmd(this)).second() > 0; } + @Override + public boolean isLdapEnabled(long domainId) { + LdapListConfigurationCmd cmd = new LdapListConfigurationCmd(this); + cmd.setDomainId(domainId); + return listConfigurations(cmd).second() > 0; + } + @Override public Pair, Integer> listConfigurations(final LdapListConfigurationCmd cmd) { final String hostname = cmd.getHostname(); @@ -304,7 +323,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { final String escapedUsername = LdapUtils.escapeLDAPSearchFilter(username); return _ldapUserManagerFactory.getInstance(_ldapConfiguration.getLdapProvider(null)).getUsers("*" + escapedUsername + "*", context, null); } catch (NamingException | IOException e) { - s_logger.debug("ldap Exception: ",e); + LOGGER.debug("ldap Exception: ",e); throw new NoLdapUserMatchingQueryException(username); } finally { closeContext(context); @@ -313,9 +332,13 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { @Override public LinkDomainToLdapResponse linkDomainToLdap(LinkDomainToLdapCmd cmd) { - Validate.isTrue(_ldapConfiguration.getBaseDn(cmd.getDomainId()) == null, "can not link a domain unless a basedn is configured for it."); - Validate.notEmpty(cmd.getLdapDomain(), "ldapDomain cannot be empty, please supply a GROUP or OU name"); - return linkDomainToLdap(cmd.getDomainId(),cmd.getType(),cmd.getLdapDomain(),cmd.getAccountType()); + final Long domainId = cmd.getDomainId(); + final String baseDn = _ldapConfiguration.getBaseDn(domainId); + final String ldapDomain = cmd.getLdapDomain(); + + Validate.isTrue(baseDn != null, String.format("can not link a domain (with id = %d) unless a basedn (%s) is configured for it.", domainId, baseDn)); + Validate.notEmpty(ldapDomain, "ldapDomain cannot be empty, please supply a GROUP or OU name"); + return linkDomainToLdap(cmd.getDomainId(),cmd.getType(), ldapDomain,cmd.getAccountType()); } private LinkDomainToLdapResponse linkDomainToLdap(Long domainId, String type, String name, short accountType) { @@ -329,7 +352,7 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { DomainVO domain = domainDao.findById(vo.getDomainId()); String domainUuid = ""; if (domain == null) { - s_logger.error("no domain in database for id " + vo.getDomainId()); + LOGGER.error("no domain in database for id " + vo.getDomainId()); } else { domainUuid = domain.getUuid(); } @@ -371,12 +394,14 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { account = new AccountVO(cmd.getAccountName(), cmd.getDomainId(), null, cmd.getAccountType(), UUID.randomUUID().toString()); accountDao.persist((AccountVO)account); } + Long accountId = account.getAccountId(); + clearOldAccountMapping(cmd); LdapTrustMapVO vo = _ldapTrustMapDao.persist(new LdapTrustMapVO(cmd.getDomainId(), linkType, cmd.getLdapDomain(), cmd.getAccountType(), accountId)); DomainVO domain = domainDao.findById(vo.getDomainId()); String domainUuid = ""; if (domain == null) { - s_logger.error("no domain in database for id " + vo.getDomainId()); + LOGGER.error("no domain in database for id " + vo.getDomainId()); } else { domainUuid = domain.getUuid(); } @@ -384,4 +409,29 @@ public class LdapManagerImpl implements LdapManager, LdapValidator { LinkAccountToLdapResponse response = new LinkAccountToLdapResponse(domainUuid, vo.getType().toString(), vo.getName(), vo.getAccountType(), account.getUuid(), cmd.getAccountName()); return response; } + + private void clearOldAccountMapping(LinkAccountToLdapCmd cmd) { + // first find if exists log warning and update + LdapTrustMapVO oldVo = _ldapTrustMapDao.findGroupInDomain(cmd.getDomainId(), cmd.getLdapDomain()); + if(oldVo != null) { + // deal with edge cases, i.e. check if the old account is indeed deleted etc. + if (oldVo.getAccountId() != 0l) { + AccountVO oldAcount = accountDao.findByIdIncludingRemoved(oldVo.getAccountId()); + String msg = String.format("group %s is mapped to account %d in the current domain (%s)", cmd.getLdapDomain(), oldVo.getAccountId(), cmd.getDomainId()); + if (null == oldAcount.getRemoved()) { + msg += ", delete the old map before mapping a new account to the same group."; + LOGGER.error(msg); + throw new CloudRuntimeException(msg); + } else { + msg += ", the old map is deleted."; + LOGGER.warn(msg); + _ldapTrustMapDao.expunge(oldVo.getId()); + } + } else { + String msg = String.format("group %s is mapped to the current domain (%s) for autoimport and can not be used for autosync", cmd.getLdapDomain(), cmd.getDomainId()); + LOGGER.error(msg); + throw new CloudRuntimeException(msg); + } + } + } } diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapUserManagerFactory.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapUserManagerFactory.java index f796ce23b4e..a6217dcb5cb 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapUserManagerFactory.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/LdapUserManagerFactory.java @@ -32,7 +32,7 @@ public class LdapUserManagerFactory implements ApplicationContextAware { public static final Logger s_logger = Logger.getLogger(LdapUserManagerFactory.class.getName()); - private static Map ldapUserManagerMap = new HashMap<>(); + static Map ldapUserManagerMap = new HashMap<>(); private ApplicationContext applicationCtx; diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java index cb3824a2ef0..5fe27e50d4d 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/OpenLdapUserManagerImpl.java @@ -33,16 +33,20 @@ import javax.naming.ldap.LdapContext; import javax.naming.ldap.PagedResultsControl; import javax.naming.ldap.PagedResultsResponseControl; +import org.apache.cloudstack.ldap.dao.LdapTrustMapDao; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; public class OpenLdapUserManagerImpl implements LdapUserManager { - private static final Logger s_logger = Logger.getLogger(OpenLdapUserManagerImpl.class.getName()); + private static final Logger LOGGER = Logger.getLogger(OpenLdapUserManagerImpl.class.getName()); @Inject protected LdapConfiguration _ldapConfiguration; + @Inject + LdapTrustMapDao _ldapTrustMapDao; + public OpenLdapUserManagerImpl() { } @@ -82,25 +86,62 @@ public class OpenLdapUserManagerImpl implements LdapUserManager { usernameFilter.append((username == null ? "*" : username)); usernameFilter.append(")"); - final StringBuilder memberOfFilter = new StringBuilder(); - if (_ldapConfiguration.getSearchGroupPrinciple(domainId) != null) { - if(s_logger.isDebugEnabled()) { - s_logger.debug("adding search filter for '" + _ldapConfiguration.getSearchGroupPrinciple(domainId) + - "', using " + _ldapConfiguration.getUserMemberOfAttribute(domainId)); + String memberOfAttribute = _ldapConfiguration.getUserMemberOfAttribute(domainId); + StringBuilder ldapGroupsFilter = new StringBuilder(); + // this should get the trustmaps for this domain + List ldapGroups = getMappedLdapGroups(domainId); + if (null != ldapGroups && ldapGroups.size() > 0) { + ldapGroupsFilter.append("(|"); + for (String ldapGroup : ldapGroups) { + ldapGroupsFilter.append(getMemberOfGroupString(ldapGroup, memberOfAttribute)); } - memberOfFilter.append("(" + _ldapConfiguration.getUserMemberOfAttribute(domainId) + "="); - memberOfFilter.append(_ldapConfiguration.getSearchGroupPrinciple(domainId)); - memberOfFilter.append(")"); + ldapGroupsFilter.append(')'); + } + // make sure only users in the principle group are retrieved + String pricipleGroup = _ldapConfiguration.getSearchGroupPrinciple(domainId); + final StringBuilder principleGroupFilter = new StringBuilder(); + if (null != pricipleGroup) { + principleGroupFilter.append(getMemberOfGroupString(pricipleGroup, memberOfAttribute)); } - final StringBuilder result = new StringBuilder(); result.append("(&"); result.append(userObjectFilter); result.append(usernameFilter); - result.append(memberOfFilter); + result.append(ldapGroupsFilter); + result.append(principleGroupFilter); result.append(")"); - return result.toString(); + String returnString = result.toString(); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("constructed ldap query: " + returnString); + } + return returnString; + } + + private List getMappedLdapGroups(Long domainId) { + List ldapGroups = new ArrayList<>(); + // first get the trustmaps + if (null != domainId) { + for (LdapTrustMapVO trustMap : _ldapTrustMapDao.searchByDomainId(domainId)) { + // then retrieve the string from it + ldapGroups.add(trustMap.getName()); + } + } + return ldapGroups; + } + + private String getMemberOfGroupString(String group, String memberOfAttribute) { + final StringBuilder memberOfFilter = new StringBuilder(); + if (null != group) { + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("adding search filter for '" + group + + "', using '" + memberOfAttribute + "'"); + } + memberOfFilter.append("(" + memberOfAttribute + "="); + memberOfFilter.append(group); + memberOfFilter.append(")"); + } + return memberOfFilter.toString(); } private String generateGroupSearchFilter(final String groupName, Long domainId) { @@ -212,7 +253,7 @@ public class OpenLdapUserManagerImpl implements LdapUserManager { try{ users.add(getUserForDn(userdn, context, domainId)); } catch (NamingException e){ - s_logger.info("Userdn: " + userdn + " Not Found:: Exception message: " + e.getMessage()); + LOGGER.info("Userdn: " + userdn + " Not Found:: Exception message: " + e.getMessage()); } } } @@ -251,8 +292,8 @@ public class OpenLdapUserManagerImpl implements LdapUserManager { searchControls.setReturningAttributes(_ldapConfiguration.getReturnAttributes(domainId)); NamingEnumeration results = context.search(basedn, searchString, searchControls); - if(s_logger.isDebugEnabled()) { - s_logger.debug("searching user(s) with filter: \"" + searchString + "\""); + if(LOGGER.isDebugEnabled()) { + LOGGER.debug("searching user(s) with filter: \"" + searchString + "\""); } final List users = new ArrayList(); while (results.hasMoreElements()) { @@ -277,7 +318,7 @@ public class OpenLdapUserManagerImpl implements LdapUserManager { String basedn = _ldapConfiguration.getBaseDn(domainId); if (StringUtils.isBlank(basedn)) { - throw new IllegalArgumentException("ldap basedn is not configured"); + throw new IllegalArgumentException(String.format("ldap basedn is not configured (for domain: %s)", domainId)); } byte[] cookie = null; int pageSize = _ldapConfiguration.getLdapPageSize(domainId); @@ -301,7 +342,7 @@ public class OpenLdapUserManagerImpl implements LdapUserManager { } } } else { - s_logger.info("No controls were sent from the ldap server"); + LOGGER.info("No controls were sent from the ldap server"); } context.setRequestControls(new Control[] {new PagedResultsControl(pageSize, cookie, Control.CRITICAL)}); } while (cookie != null); diff --git a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java index fa4c0af236f..b591e3acbf9 100644 --- a/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java +++ b/plugins/user-authenticators/ldap/src/main/java/org/apache/cloudstack/ldap/dao/LdapConfigurationDaoImpl.java @@ -75,7 +75,7 @@ public class LdapConfigurationDaoImpl extends GenericDaoBase getSearchCriteria(String hostname, int port, Long domainId) { SearchCriteria sc; if (domainId == null) { - sc = listDomainConfigurationsSearch.create(); + sc = listGlobalConfigurationsSearch.create(); } else { sc = listDomainConfigurationsSearch.create(); sc.setParameters("domain_id", domainId); diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/api/command/LdapListUsersCmdTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/api/command/LdapListUsersCmdTest.java new file mode 100644 index 00000000000..d84e73aae47 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/api/command/LdapListUsersCmdTest.java @@ -0,0 +1,466 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command; + +import com.cloud.domain.Domain; +import com.cloud.domain.DomainVO; +import com.cloud.user.Account; +import com.cloud.user.AccountVO; +import com.cloud.user.DomainService; +import com.cloud.user.User; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.response.LdapUserResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.UserResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.ldap.LdapManager; +import org.apache.cloudstack.ldap.LdapUser; +import org.apache.cloudstack.ldap.NoLdapUserMatchingQueryException; +import org.apache.cloudstack.query.QueryService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.doThrow; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(CallContext.class) +public class LdapListUsersCmdTest implements LdapConfigurationChanger { + + public static final String LOCAL_DOMAIN_ID = "12345678-90ab-cdef-fedc-ba0987654321"; + public static final String LOCAL_DOMAIN_NAME = "engineering"; + @Mock + LdapManager ldapManager; + @Mock + QueryService queryService; + @Mock + DomainService domainService; + + LdapListUsersCmd ldapListUsersCmd; + LdapListUsersCmd cmdSpy; + + Domain localDomain; + + @Before + public void setUp() throws NoSuchFieldException, IllegalAccessException { + ldapListUsersCmd = new LdapListUsersCmd(ldapManager, queryService); + cmdSpy = spy(ldapListUsersCmd); + + PowerMockito.mockStatic(CallContext.class); + CallContext callContextMock = PowerMockito.mock(CallContext.class); + PowerMockito.when(CallContext.current()).thenReturn(callContextMock); + Account accountMock = PowerMockito.mock(Account.class); + PowerMockito.when(accountMock.getDomainId()).thenReturn(1l); + PowerMockito.when(callContextMock.getCallingAccount()).thenReturn(accountMock); + + ldapListUsersCmd._domainService = domainService; + +// no need to setHiddenField(ldapListUsersCmd, .... ); + } + + /** + * given: "We have an LdapManager, QueryService and LdapListUsersCmd" + * when: "Get entity owner id is called" + * then: "a 1 should be returned" + * + */ + @Test + public void getEntityOwnerIdisOne() { + long ownerId = ldapListUsersCmd.getEntityOwnerId(); + assertEquals(ownerId, 1); + } + + /** + * given: "We have an LdapManager with no users, QueryService and a LdapListUsersCmd" + * when: "LdapListUsersCmd is executed" + * then: "An array of size 0 is returned" + * + * @throws NoLdapUserMatchingQueryException + */ + @Test + public void successfulEmptyResponseFromExecute() throws NoLdapUserMatchingQueryException { + doThrow(new NoLdapUserMatchingQueryException("")).when(ldapManager).getUsers(null); + ldapListUsersCmd.execute(); + assertEquals(0, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + } + + /** + * given: "We have an LdapManager, one user, QueryService and a LdapListUsersCmd" + * when: "LdapListUsersCmd is executed" + * then: "a list of size not 0 is returned" + */ + @Test + public void successfulResponseFromExecute() throws NoLdapUserMatchingQueryException { + mockACSUserSearch(); + + mockResponseCreation(); + + useSubdomain(); + + ldapListUsersCmd.execute(); + + verify(queryService, times(1)).searchForUsers(anyLong(), anyBoolean()); + assertNotEquals(0, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + } + + /** + * given: "We have an LdapManager, QueryService and a LdapListUsersCmd" + * when: "Get command name is called" + * then: "ldapuserresponse is returned" + */ + @Test + public void successfulReturnOfCommandName() { + String commandName = ldapListUsersCmd.getCommandName(); + + assertEquals("ldapuserresponse", commandName); + } + + /** + * given: "We have an LdapUser and a CloudStack user whose username match" + * when: "isACloudstackUser is executed" + * then: "The result is true" + * + * TODO: is this really the valid behaviour? shouldn't the user also be linked to ldap and not accidentally match? + */ + @Test + public void isACloudstackUser() { + mockACSUserSearch(); + + LdapUser ldapUser = new LdapUser("rmurphy", "rmurphy@cloudstack.org", "Ryan", "Murphy", "cn=rmurphy,dc=cloudstack,dc=org", null, false, null); + + boolean result = ldapListUsersCmd.isACloudstackUser(ldapUser); + + assertTrue(result); + } + + /** + * given: "We have an LdapUser and not a matching CloudstackUser" + * when: "isACloudstackUser is executed" + * then: "The result is false" + */ + @Test + public void isNotACloudstackUser() { + doReturn(new ListResponse()).when(queryService).searchForUsers(anyLong(), anyBoolean()); + + LdapUser ldapUser = new LdapUser("rmurphy", "rmurphy@cloudstack.org", "Ryan", "Murphy", "cn=rmurphy,dc=cloudstack,dc=org", null, false, null); + + boolean result = ldapListUsersCmd.isACloudstackUser(ldapUser); + + assertFalse(result); + } + + /** + * test whether a value other than 'any' for 'listtype' leads to a good 'userfilter' value + */ + @Test + public void getListtypeOther() { + when(cmdSpy.getListTypeString()).thenReturn("otHer", "anY"); + + String userfilter = cmdSpy.getUserFilterString(); + assertEquals("AnyDomain", userfilter); + + userfilter = cmdSpy.getUserFilterString(); + assertEquals("AnyDomain", userfilter); + } + + /** + * test whether a value of 'any' for 'listtype' leads to a good 'userfilter' value + */ + @Test + public void getListtypeAny() { + when(cmdSpy.getListTypeString()).thenReturn("all"); + String userfilter = cmdSpy.getUserFilterString(); + assertEquals("NoFilter", userfilter); + } + + /** + * test whether values for 'userfilter' yield the right filter + */ + @Test + public void getUserFilter() throws NoSuchFieldException, IllegalAccessException { + when(cmdSpy.getListTypeString()).thenReturn("otHer"); + LdapListUsersCmd.UserFilter userfilter = cmdSpy.getUserFilter(); + + assertEquals(LdapListUsersCmd.UserFilter.ANY_DOMAIN, userfilter); + + when(cmdSpy.getListTypeString()).thenReturn("anY"); + userfilter = cmdSpy.getUserFilter(); + assertEquals(LdapListUsersCmd.UserFilter.ANY_DOMAIN, userfilter); + } + + /** + * test if the right exception is thrown on invalid input. + */ + @Test(expected = IllegalArgumentException.class) + public void getInvalidUserFilterValues() throws NoSuchFieldException, IllegalAccessException { + setHiddenField(ldapListUsersCmd, "userFilter", "flase"); +// unused output: LdapListUsersCmd.UserFilter userfilter = + ldapListUsersCmd.getUserFilter(); + } + + @Test + public void getUserFilterValues() { + assertEquals("PotentialImport", LdapListUsersCmd.UserFilter.POTENTIAL_IMPORT.toString()); + assertEquals(LdapListUsersCmd.UserFilter.POTENTIAL_IMPORT, LdapListUsersCmd.UserFilter.fromString("PotentialImport")); + } + + @Test(expected = IllegalArgumentException.class) + public void getInvalidUserFilterStringValue() { + LdapListUsersCmd.UserFilter.fromString("PotentImport"); + } + + /** + * apply no filter + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyNoFilter() throws NoSuchFieldException, IllegalAccessException, NoLdapUserMatchingQueryException { + mockACSUserSearch(); + mockResponseCreation(); + + useSubdomain(); + + setHiddenField(ldapListUsersCmd, "userFilter", "NoFilter"); + ldapListUsersCmd.execute(); + + assertEquals(3, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + } + + /** + * filter all acs users + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyAnyDomain() throws NoSuchFieldException, IllegalAccessException, NoLdapUserMatchingQueryException { + mockACSUserSearch(); + mockResponseCreation(); + + useSubdomain(); + + setHiddenField(ldapListUsersCmd, "userFilter", "AnyDomain"); + setHiddenField(ldapListUsersCmd, "domainId", 2l /* not root */); + ldapListUsersCmd.execute(); + + // 'rmurphy' annotated with native + // 'bob' still in + // 'abhi' is filtered out + List responses = ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses(); + assertEquals(2, responses.size()); + for(ResponseObject response : responses) { + if(!(response instanceof LdapUserResponse)) { + fail("unexpected return-type from API backend method"); + } else { + LdapUserResponse userResponse = (LdapUserResponse)response; + // further validate this user + if ("rmurphy".equals(userResponse.getUsername()) && + ! User.Source.NATIVE.toString().equalsIgnoreCase(userResponse.getUserSource())) { + fail("expected murphy from ldap"); + } + if ("bob".equals(userResponse.getUsername()) && + ! "".equals(userResponse.getUserSource())) { + fail("expected bob from without usersource"); + } + } + } + } + + /** + * filter out acs users for the requested domain + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyLocalDomainForASubDomain() throws NoSuchFieldException, IllegalAccessException, NoLdapUserMatchingQueryException { + mockACSUserSearch(); + mockResponseCreation(); + + setHiddenField(ldapListUsersCmd, "userFilter", "LocalDomain"); + setHiddenField(ldapListUsersCmd, "domainId", 2l /* not root */); + + localDomain = useSubdomain(); + + ldapListUsersCmd.execute(); + + // 'rmurphy' filtered out 'bob' still in + assertEquals(2, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + // todo: assert user sources + } + + /** + * filter out acs users for the default domain + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyLocalDomainForTheCallersDomain() throws NoSuchFieldException, IllegalAccessException, NoLdapUserMatchingQueryException { + mockACSUserSearch(); + mockResponseCreation(); + + setHiddenField(ldapListUsersCmd, "userFilter", "LocalDomain"); + + AccountVO account = new AccountVO(); + setHiddenField(account, "accountName", "admin"); + setHiddenField(account, "domainId", 1l); + final CallContext callContext = CallContext.current(); + setHiddenField(callContext, "account", account); + DomainVO domainVO = useDomain("ROOT", 1l); + localDomain = domainVO; + + ldapListUsersCmd.execute(); + + // 'rmurphy' filtered out 'bob' still in + assertEquals(2, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + // todo: assert usersources + } + + /** + * todo generate an extensive configuration and check with an extensive user list + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyPotentialImport() throws NoSuchFieldException, IllegalAccessException, NoLdapUserMatchingQueryException { + mockACSUserSearch(); + mockResponseCreation(); + + useSubdomain(); + + setHiddenField(ldapListUsersCmd, "userFilter", "PotentialImport"); + ldapListUsersCmd.execute(); + + assertEquals(2, ((ListResponse)ldapListUsersCmd.getResponseObject()).getResponses().size()); + } + + /** + * unknown filter + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test(expected = IllegalArgumentException.class) + public void applyUnknownFilter() throws NoSuchFieldException, IllegalAccessException { + setHiddenField(ldapListUsersCmd, "userFilter", "UnknownFilter"); + ldapListUsersCmd.execute(); + } + + /** + * make sure there are no unimplemented filters + * + * This was created to deal with the possible {code}NoSuchMethodException{code} that won't be dealt with in regular coverage + * + * @throws NoSuchFieldException + * @throws IllegalAccessException + */ + @Test + public void applyUnimplementedFilter() throws NoSuchFieldException, IllegalAccessException { + useSubdomain(); + for (LdapListUsersCmd.UserFilter UNIMPLEMENTED_FILTER : LdapListUsersCmd.UserFilter.values()) { + setHiddenField(ldapListUsersCmd, "userFilter", UNIMPLEMENTED_FILTER.toString()); + ldapListUsersCmd.getUserFilter().filter(ldapListUsersCmd,new ArrayList()); + } + } + + // helper methods // + //////////////////// + private DomainVO useSubdomain() { + DomainVO domainVO = useDomain(LOCAL_DOMAIN_NAME, 2l); + return domainVO; + } + + private DomainVO useDomain(String domainName, long domainId) { + DomainVO domainVO = new DomainVO(); + domainVO.setName(domainName); + domainVO.setId(domainId); + domainVO.setUuid(LOCAL_DOMAIN_ID); + when(domainService.getDomain(anyLong())).thenReturn(domainVO); + return domainVO; + } + + private void mockACSUserSearch() { + UserResponse rmurphy = createMockUserResponse("rmurphy", User.Source.NATIVE); + UserResponse rohit = createMockUserResponse("rohit", User.Source.SAML2); + UserResponse abhi = createMockUserResponse("abhi", User.Source.LDAP); + + ArrayList responses = new ArrayList<>(); + responses.add(rmurphy); + responses.add(rohit); + responses.add(abhi); + + ListResponse queryServiceResponse = new ListResponse<>(); + queryServiceResponse.setResponses(responses); + + doReturn(queryServiceResponse).when(queryService).searchForUsers(anyLong(), anyBoolean()); + } + + private UserResponse createMockUserResponse(String uid, User.Source source) { + UserResponse userResponse = new UserResponse(); + userResponse.setUsername(uid); + userResponse.setUserSource(source); + + // for now: + userResponse.setDomainId(LOCAL_DOMAIN_ID); + userResponse.setDomainName(LOCAL_DOMAIN_NAME); + + return userResponse; + } + + private void mockResponseCreation() throws NoLdapUserMatchingQueryException { + List users = new ArrayList(); + LdapUser murphy = new LdapUser("rmurphy", "rmurphy@test.com", "Ryan", "Murphy", "cn=rmurphy,dc=cloudstack,dc=org", "mythical", false, null); + LdapUser bob = new LdapUser("bob", "bob@test.com", "Robert", "Young", "cn=bob,ou=engineering,dc=cloudstack,dc=org", LOCAL_DOMAIN_NAME, false, null); + LdapUser abhi = new LdapUser("abhi", "abhi@test.com", "Abhi", "YoungOrOld", "cn=abhi,ou=engineering,dc=cloudstack,dc=org", LOCAL_DOMAIN_NAME, false, null); + users.add(murphy); + users.add(bob); + users.add(abhi); + + doReturn(users).when(ldapManager).getUsers(any()); + + LdapUserResponse response = new LdapUserResponse("rmurphy", "rmurphy@test.com", "Ryan", "Murphy", "cn=rmurphy,dc=cloudstack,dc=org", null); + doReturn(response).when(ldapManager).createLdapUserResponse(murphy); + LdapUserResponse bobResponse = new LdapUserResponse("bob", "bob@test.com", "Robert", "Young", "cn=bob,ou=engineering,dc=cloudstack,dc=org", LOCAL_DOMAIN_NAME); + doReturn(bobResponse).when(ldapManager).createLdapUserResponse(bob); + LdapUserResponse abhiResponse = new LdapUserResponse("abhi", "abhi@test.com", "Abhi", "YoungOrOld", "cn=abhi,ou=engineering,dc=cloudstack,dc=org", LOCAL_DOMAIN_NAME); + doReturn(abhiResponse).when(ldapManager).createLdapUserResponse(abhi); + } +} diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/EmbeddedLdapServer.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/EmbeddedLdapServer.java new file mode 100644 index 00000000000..2b719855282 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/EmbeddedLdapServer.java @@ -0,0 +1,326 @@ +/*- + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.ldap; + +import org.apache.commons.io.FileUtils; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.schema.registries.DefaultSchema; +import org.apache.directory.api.ldap.model.schema.registries.Schema; +import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader; +import org.apache.directory.api.ldap.schema.loader.LdifSchemaLoader; +import org.apache.directory.server.core.api.CoreSession; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory; +import org.apache.directory.server.core.factory.JdbmPartitionFactory; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex; +import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; +import org.apache.directory.server.xdbm.Index; +import org.apache.directory.server.xdbm.IndexNotFoundException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Call init() to start the server and destroy() to shut it down. + */ +public class EmbeddedLdapServer { + // API References: + // http://directory.apache.org/apacheds/gen-docs/latest/apidocs/ + // http://directory.apache.org/api/gen-docs/latest/apidocs/ + + private static final String BASE_PARTITION_NAME = "mydomain"; + private static final String BASE_DOMAIN = "org"; + private static final String BASE_STRUCTURE = "dc=" + BASE_PARTITION_NAME + ",dc=" + BASE_DOMAIN; + + private static final int LDAP_SERVER_PORT = 10389; + private static final int BASE_CACHE_SIZE = 1000; + private static final List ATTR_NAMES_TO_INDEX = new ArrayList(Arrays.asList("uid")); + + private DirectoryService _directoryService; + private LdapServer _ldapServer; + private JdbmPartition _basePartition; + private boolean _deleteInstanceDirectoryOnStartup = true; + private boolean _deleteInstanceDirectoryOnShutdown = true; + + public String getBasePartitionName() { + return BASE_PARTITION_NAME; + } + + public String getBaseStructure() { + return BASE_STRUCTURE; + } + + public int getBaseCacheSize() { + return BASE_CACHE_SIZE; + } + + public int getLdapServerPort() { + return LDAP_SERVER_PORT; + } + + public List getAttrNamesToIndex() { + return ATTR_NAMES_TO_INDEX; + } + + protected void addSchemaExtensions() throws LdapException, IOException { + // override to add custom attributes to the schema + } + + public void init() throws Exception { + if (getDirectoryService() == null) { + if (getDeleteInstanceDirectoryOnStartup()) { + deleteDirectory(getGuessedInstanceDirectory()); + } + + DefaultDirectoryServiceFactory serviceFactory = new DefaultDirectoryServiceFactory(); + serviceFactory.init(getDirectoryServiceName()); + setDirectoryService(serviceFactory.getDirectoryService()); + + getDirectoryService().getChangeLog().setEnabled(false); + getDirectoryService().setDenormalizeOpAttrsEnabled(true); + + createBasePartition(); + + getDirectoryService().startup(); + + createRootEntry(); + } + + if (getLdapServer() == null) { + setLdapServer(new LdapServer()); + getLdapServer().setDirectoryService(getDirectoryService()); + getLdapServer().setTransports(new TcpTransport(getLdapServerPort())); + getLdapServer().start(); + } + } + + public void destroy() throws Exception { + File instanceDirectory = getDirectoryService().getInstanceLayout().getInstanceDirectory(); + getLdapServer().stop(); + getDirectoryService().shutdown(); + setLdapServer(null); + setDirectoryService(null); + if (getDeleteInstanceDirectoryOnShutdown()) { + deleteDirectory(instanceDirectory); + } + } + + public String getDirectoryServiceName() { + return getBasePartitionName() + "DirectoryService"; + } + + private static void deleteDirectory(File path) throws IOException { + FileUtils.deleteDirectory(path); + } + + protected void createBasePartition() throws Exception { + JdbmPartitionFactory jdbmPartitionFactory = new JdbmPartitionFactory(); + setBasePartition(jdbmPartitionFactory.createPartition(getDirectoryService().getSchemaManager(), getDirectoryService().getDnFactory(), getBasePartitionName(), getBaseStructure(), getBaseCacheSize(), getBasePartitionPath())); + addSchemaExtensions(); + createBaseIndices(); + getDirectoryService().addPartition(getBasePartition()); + } + + protected void createBaseIndices() throws Exception { + // + // Default indices, that can be seen with getSystemIndexMap() and + // getUserIndexMap(), are minimal. There are no user indices by + // default and the default system indices are: + // + // apacheOneAlias, entryCSN, apacheSubAlias, apacheAlias, + // objectClass, apachePresence, apacheRdn, administrativeRole + // + for (String attrName : getAttrNamesToIndex()) { + getBasePartition().addIndex(createIndexObjectForAttr(attrName)); + } + } + + protected JdbmIndex createIndexObjectForAttr(String attrName, boolean withReverse) throws LdapException { + String oid = getOidByAttributeName(attrName); + if (oid == null) { + throw new RuntimeException("OID could not be found for attr " + attrName); + } + return new JdbmIndex(oid, withReverse); + } + + protected JdbmIndex createIndexObjectForAttr(String attrName) throws LdapException { + return createIndexObjectForAttr(attrName, false); + } + + protected void createRootEntry() throws LdapException { + Entry entry = getDirectoryService().newEntry(getDirectoryService().getDnFactory().create(getBaseStructure())); + entry.add("objectClass", "top", "domain", "extensibleObject"); + entry.add("dc", getBasePartitionName()); + CoreSession session = getDirectoryService().getAdminSession(); + try { + session.add(entry); + } finally { + session.unbind(); + } + } + + /** + * @return A map where the key is the attribute name the value is the + * oid. + */ + public Map getSystemIndexMap() throws IndexNotFoundException { + Map result = new LinkedHashMap<>(); + Iterator it = getBasePartition().getSystemIndices(); + while (it.hasNext()) { + String oid = it.next(); + Index index = getBasePartition().getSystemIndex(getDirectoryService().getSchemaManager().getAttributeType(oid)); + result.put(index.getAttribute().getName(), index.getAttributeId()); + } + return result; + } + + /** + * @return A map where the key is the attribute name the value is the + * oid. + */ + public Map getUserIndexMap() throws IndexNotFoundException { + Map result = new LinkedHashMap<>(); + Iterator it = getBasePartition().getUserIndices(); + while (it.hasNext()) { + String oid = it.next(); + Index index = getBasePartition().getUserIndex(getDirectoryService().getSchemaManager().getAttributeType(oid)); + result.put(index.getAttribute().getName(), index.getAttributeId()); + } + return result; + } + + public File getPartitionsDirectory() { + return getDirectoryService().getInstanceLayout().getPartitionsDirectory(); + } + + public File getBasePartitionPath() { + return new File(getPartitionsDirectory(), getBasePartitionName()); + } + + /** + * Used at init time to clear out the likely instance directory before + * anything is created. + */ + public File getGuessedInstanceDirectory() { + // See source code for DefaultDirectoryServiceFactory + // buildInstanceDirectory. ApacheDS looks at the workingDirectory + // system property first and then defers to the java.io.tmpdir + // system property. + final String property = System.getProperty("workingDirectory"); + return new File(property != null ? property : System.getProperty("java.io.tmpdir") + File.separator + "server-work-" + getDirectoryServiceName()); + } + + public String getOidByAttributeName(String attrName) throws LdapException { + return getDirectoryService().getSchemaManager().getAttributeTypeRegistry().getOidByName(attrName); + } + + /** + * Add additional schemas to the directory server. This takes a path to + * the schema directory and uses the LdifSchemaLoader. + * + * @param schemaLocation The path to the directory containing the + * "ou=schema" directory for an additional schema + * @param schemaName The name of the schema + * @return true if the schemas have been loaded and the registries is + * consistent + */ + public boolean addSchemaFromPath(File schemaLocation, String schemaName) throws LdapException, IOException { + LdifSchemaLoader schemaLoader = new LdifSchemaLoader(schemaLocation); + DefaultSchema schema = new DefaultSchema(schemaLoader, schemaName); + return getDirectoryService().getSchemaManager().load(schema); + } + + /** + * Add additional schemas to the directory server. This uses + * JarLdifSchemaLoader, which will search for the "ou=schema" directory + * within "/schema" on the classpath. If packaging the schema as part of + * a jar using Gradle or Maven, you'd probably want to put your + * "ou=schema" directory in src/main/resources/schema. + *

+ * It's also required that a META-INF/apacheds-schema.index be present in + * your classpath that lists each LDIF file in your schema directory. + * + * @param schemaName The name of the schema + * @return true if the schemas have been loaded and the registries is + * consistent + */ + public boolean addSchemaFromClasspath(String schemaName) throws LdapException, IOException { + // To debug if your apacheds-schema.index isn't found: + // Enumeration indexes = getClass().getClassLoader().getResources("META-INF/apacheds-schema.index"); + JarLdifSchemaLoader schemaLoader = new JarLdifSchemaLoader(); + Schema schema = schemaLoader.getSchema(schemaName); + return schema != null && getDirectoryService().getSchemaManager().load(schema); + } + + public DirectoryService getDirectoryService() { + return _directoryService; + } + + public void setDirectoryService(DirectoryService directoryService) { + this._directoryService = directoryService; + } + + public LdapServer getLdapServer() { + return _ldapServer; + } + + public void setLdapServer(LdapServer ldapServer) { + this._ldapServer = ldapServer; + } + + public JdbmPartition getBasePartition() { + return _basePartition; + } + + public void setBasePartition(JdbmPartition basePartition) { + this._basePartition = basePartition; + } + + public boolean getDeleteInstanceDirectoryOnStartup() { + return _deleteInstanceDirectoryOnStartup; + } + + public void setDeleteInstanceDirectoryOnStartup(boolean deleteInstanceDirectoryOnStartup) { + this._deleteInstanceDirectoryOnStartup = deleteInstanceDirectoryOnStartup; + } + + public boolean getDeleteInstanceDirectoryOnShutdown() { + return _deleteInstanceDirectoryOnShutdown; + } + + public void setDeleteInstanceDirectoryOnShutdown(boolean deleteInstanceDirectoryOnShutdown) { + this._deleteInstanceDirectoryOnShutdown = deleteInstanceDirectoryOnShutdown; + } + + public static void main (String[] args) { + EmbeddedLdapServer embeddedLdapServer = new EmbeddedLdapServer(); + try { + embeddedLdapServer.init(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapAuthenticatorTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapAuthenticatorTest.java index 85fd01a0cae..4c0519de256 100644 --- a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapAuthenticatorTest.java +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapAuthenticatorTest.java @@ -18,6 +18,9 @@ package org.apache.cloudstack.ldap; import com.cloud.server.auth.UserAuthenticator; +import com.cloud.user.AccountManager; +import com.cloud.user.AccountVO; +import com.cloud.user.User; import com.cloud.user.UserAccount; import com.cloud.user.UserAccountVO; import com.cloud.user.dao.UserAccountDao; @@ -25,13 +28,20 @@ import com.cloud.utils.Pair; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -43,9 +53,12 @@ public class LdapAuthenticatorTest { @Mock UserAccountDao userAccountDao; @Mock + AccountManager accountManager; + @Mock UserAccount user = new UserAccountVO(); - LdapAuthenticator ldapAuthenticator; + @InjectMocks + LdapAuthenticator ldapAuthenticator = new LdapAuthenticator(); private String username = "bbanner"; private String principal = "cd=bbanner"; private String hardcoded = "password"; @@ -53,7 +66,18 @@ public class LdapAuthenticatorTest { @Before public void setUp() throws Exception { - ldapAuthenticator = new LdapAuthenticator(ldapManager, userAccountDao); + } + + @Test + public void authenticateAsNativeUser() throws Exception { + final UserAccountVO user = new UserAccountVO(); + user.setSource(User.Source.NATIVE); + + when(userAccountDao.getUserAccount(username, domainId)).thenReturn(user); + Pair rc; + rc = ldapAuthenticator.authenticate(username, "password", domainId, (Map)null); + assertFalse("authentication succeeded when it should have failed", rc.first()); + assertEquals("We should not have tried to authenticate", null,rc.second()); } @Test @@ -62,9 +86,39 @@ public class LdapAuthenticatorTest { Pair rc; when(ldapManager.getUser(username, domainId)).thenReturn(ldapUser); rc = ldapAuthenticator.authenticate(username, "password", domainId, user); - assertFalse("authentication succeded when it should have failed", rc.first()); + assertFalse("authentication succeeded when it should have failed", rc.first()); assertEquals("", UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT,rc.second()); } + + @Test + public void authenticateFailingOnSyncedAccount() throws Exception { + Pair rc; + + List memberships = new ArrayList<>(); + memberships.add("g1"); + List mappedGroups = new ArrayList<>(); + mappedGroups.add("g1"); + mappedGroups.add("g2"); + + LdapUser ldapUser = new LdapUser(username,"a@b","b","banner",principal,"",false,null); + LdapUser userSpy = spy(ldapUser); + when(userSpy.getMemberships()).thenReturn(memberships); + + List maps = new ArrayList<>(); + LdapAuthenticator auth = spy(ldapAuthenticator); + when(auth.getMappedGroups(maps)).thenReturn(mappedGroups); + + LdapTrustMapVO trustMap = new LdapTrustMapVO(domainId, LdapManager.LinkType.GROUP, "cn=name", (short)2, 1l); + + AccountVO account = new AccountVO("accountName" , domainId, "domain.net", (short)2, "final String uuid"); + when(accountManager.getAccount(anyLong())).thenReturn(account); + when(ldapManager.getUser(username, domainId)).thenReturn(userSpy); + when(ldapManager.getLinkedLdapGroup(domainId, "g1")).thenReturn(trustMap); + rc = auth.authenticate(username, "password", domainId, user, maps); + assertFalse("authentication succeeded when it should have failed", rc.first()); + assertEquals("", UserAuthenticator.ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT,rc.second()); + } + @Test public void authenticate() throws Exception { LdapUser ldapUser = new LdapUser(username, "a@b", "b", "banner", principal, "", false, null); diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapConfigurationTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapConfigurationTest.java index 52c70ac0d19..2af20e79e36 100644 --- a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapConfigurationTest.java +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapConfigurationTest.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.ldap; -import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.ldap.dao.LdapConfigurationDao; import org.apache.cloudstack.ldap.dao.LdapConfigurationDaoImpl; import org.junit.Before; @@ -24,118 +23,98 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @RunWith(MockitoJUnitRunner.class) public class LdapConfigurationTest { + private final LdapTestConfigTool ldapTestConfigTool = new LdapTestConfigTool(); LdapConfigurationDao ldapConfigurationDao; LdapConfiguration ldapConfiguration; - private void overrideConfigValue(final String configKeyName, final Object o) throws IllegalAccessException, NoSuchFieldException { - Field configKey = LdapConfiguration.class.getDeclaredField(configKeyName); - configKey.setAccessible(true); - - ConfigKey key = (ConfigKey)configKey.get(ldapConfiguration); - - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(configKey, configKey.getModifiers() & ~Modifier.FINAL); - - Field f = ConfigKey.class.getDeclaredField("_value"); - f.setAccessible(true); - modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); - f.set(key, o); - - Field dynamic = ConfigKey.class.getDeclaredField("_isDynamic"); - dynamic.setAccessible(true); - modifiersField.setInt(dynamic, dynamic.getModifiers() & ~Modifier.FINAL); - dynamic.setBoolean(key, false); + private void overrideConfigValue(LdapConfiguration ldapConfiguration, final String configKeyName, final Object o) throws IllegalAccessException, NoSuchFieldException + { + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, configKeyName, o); } - @Before - public void init() throws Exception { - ldapConfigurationDao = new LdapConfigurationDaoImpl(); - ldapConfiguration = new LdapConfiguration(ldapConfigurationDao);; + @Before public void init() throws Exception { + ldapConfigurationDao = new LdapConfigurationDaoImpl(); + ldapConfiguration = new LdapConfiguration(ldapConfigurationDao); + ; } - @Test - public void getAuthenticationReturnsSimple() throws Exception { - overrideConfigValue("ldapBindPrincipal", "cn=bla"); - overrideConfigValue("ldapBindPassword", "pw"); + @Test public void getAuthenticationReturnsSimple() throws Exception { + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapBindPrincipal", "cn=bla"); + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapBindPassword", "pw"); String authentication = ldapConfiguration.getAuthentication(null); assertEquals("authentication should be set to simple", "simple", authentication); } - - @Test - public void getBaseDnReturnsABaseDn() throws Exception { - overrideConfigValue("ldapBaseDn", "dc=cloudstack,dc=org"); + @Test public void getBaseDnReturnsABaseDn() throws Exception { + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapBaseDn", "dc=cloudstack,dc=org"); String baseDn = ldapConfiguration.getBaseDn(null); - assertEquals("The set baseDn should be returned","dc=cloudstack,dc=org", baseDn); + assertEquals("The set baseDn should be returned", "dc=cloudstack,dc=org", baseDn); } - @Test - public void getGroupUniqueMemberAttribute() throws Exception { - String [] groupNames = {"bla", "uniquemember", "memberuid", "", null}; - for (String groupObject: groupNames) { - overrideConfigValue("ldapGroupUniqueMemberAttribute", groupObject); + @Test public void getGroupUniqueMemberAttribute() throws Exception { + String[] groupNames = {"bla", "uniquemember", "memberuid", "", null}; + for (String groupObject : groupNames) { + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapGroupUniqueMemberAttribute", groupObject); String expectedResult = null; - if(groupObject == null) { + if (groupObject == null) { expectedResult = "uniquemember"; } else { expectedResult = groupObject; - }; + } + ; String result = ldapConfiguration.getGroupUniqueMemberAttribute(null); assertEquals("testing for " + groupObject, expectedResult, result); } } - @Test - public void getSSLStatusCanBeTrue() throws Exception { + @Test public void getSSLStatusCanBeTrue() throws Exception { // given: "We have a ConfigDao with values for truststore and truststore password set" - overrideConfigValue("ldapTrustStore", "/tmp/ldap.ts"); - overrideConfigValue("ldapTrustStorePassword", "password"); + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapTrustStore", "/tmp/ldap.ts"); + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapTrustStorePassword", "password"); assertTrue("A request is made to get the status of SSL should result in true", ldapConfiguration.getSSLStatus()); } - @Test - public void getSearchGroupPrincipleReturnsSuccessfully() throws Exception { + + @Test public void getSearchGroupPrincipleReturnsSuccessfully() throws Exception { // We have a ConfigDao with a value for ldap.search.group.principle and an LdapConfiguration - overrideConfigValue("ldapSearchGroupPrinciple", "cn=cloudstack,cn=users,dc=cloudstack,dc=org"); + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapSearchGroupPrinciple", "cn=cloudstack,cn=users,dc=cloudstack,dc=org"); String result = ldapConfiguration.getSearchGroupPrinciple(null); - assertEquals("The result holds the same value configDao did", "cn=cloudstack,cn=users,dc=cloudstack,dc=org",result); + assertEquals("The result holds the same value configDao did", "cn=cloudstack,cn=users,dc=cloudstack,dc=org", result); } - @Test - public void getTrustStorePasswordResopnds() throws Exception { + @Test public void getTrustStorePasswordResopnds() throws Exception { // We have a ConfigDao with a value for truststore password - overrideConfigValue("ldapTrustStorePassword", "password"); + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapTrustStorePassword", "password"); String result = ldapConfiguration.getTrustStorePassword(); assertEquals("The result is password", "password", result); } - - @Test - public void getGroupObject() throws Exception { - String [] groupNames = {"bla", "groupOfUniqueNames", "groupOfNames", "", null}; - for (String groupObject: groupNames) { - overrideConfigValue("ldapGroupObject", groupObject); + @Test public void getGroupObject() throws Exception { + String[] groupNames = {"bla", "groupOfUniqueNames", "groupOfNames", "", null}; + for (String groupObject : groupNames) { + ldapTestConfigTool.overrideConfigValue(ldapConfiguration, "ldapGroupObject", groupObject); String expectedResult = null; - if(groupObject == null) { + if (groupObject == null) { expectedResult = "groupOfUniqueNames"; } else { expectedResult = groupObject; - }; + } + ; String result = ldapConfiguration.getGroupObject(null); assertEquals("testing for " + groupObject, expectedResult, result); } } + + @Test public void getNullLdapProvider() { + assertEquals(LdapUserManager.Provider.OPENLDAP, ldapConfiguration.getLdapProvider(null)); + } } \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapDirectoryServerConnectionTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapDirectoryServerConnectionTest.java new file mode 100644 index 00000000000..f3a17fae36c --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapDirectoryServerConnectionTest.java @@ -0,0 +1,210 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.ldap; + +import com.cloud.utils.Pair; +import org.apache.cloudstack.ldap.dao.LdapConfigurationDao; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.message.AddRequest; +import org.apache.directory.api.ldap.model.message.AddRequestImpl; +import org.apache.directory.api.ldap.model.message.AddResponse; +import org.apache.directory.ldap.client.api.LdapConnection; +import org.apache.directory.ldap.client.api.LdapNetworkConnection; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.changelog.ChangeLog; +import org.apache.directory.server.ldap.LdapServer; +import org.apache.directory.server.xdbm.IndexNotFoundException; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +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 static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class LdapDirectoryServerConnectionTest { + + static EmbeddedLdapServer embeddedLdapServer; + + @Mock + LdapConfigurationDao configurationDao; + + LdapContextFactory contextFactory; + + @Mock + LdapUserManagerFactory userManagerFactory; + + @InjectMocks + LdapConfiguration configuration; + + @InjectMocks + private LdapManagerImpl ldapManager; + + private final LdapTestConfigTool ldapTestConfigTool = new LdapTestConfigTool(); + + @BeforeClass + public static void start() throws Exception { + embeddedLdapServer = new EmbeddedLdapServer(); + embeddedLdapServer.init(); + } + @Before + public void setup() throws Exception { + LdapConfigurationVO configurationVO = new LdapConfigurationVO("localhost",10389,null); + when(configurationDao.find("localhost",10389,null)).thenReturn(configurationVO); + ldapTestConfigTool.overrideConfigValue(configuration, "ldapBaseDn", "ou=system"); + ldapTestConfigTool.overrideConfigValue(configuration, "ldapBindPassword", "secret"); + ldapTestConfigTool.overrideConfigValue(configuration, "ldapBindPrincipal", "uid=admin,ou=system"); + ldapTestConfigTool.overrideConfigValue(configuration, "ldapMemberOfAttribute", "memberOf"); + when(userManagerFactory.getInstance(LdapUserManager.Provider.OPENLDAP)).thenReturn(new OpenLdapUserManagerImpl(configuration)); + // construct an ellaborate structure around a single object + Pair, Integer> vos = new Pair, Integer>( Collections.singletonList(configurationVO),1); + when(configurationDao.searchConfigurations(null, 0, 1L)).thenReturn(vos); + + contextFactory = new LdapContextFactory(configuration); + ldapManager = new LdapManagerImpl(configurationDao, contextFactory, userManagerFactory, configuration); + } + + @After + public void cleanup() throws Exception { + contextFactory = null; + ldapManager = null; + } + + @AfterClass + public static void stop() throws Exception { + embeddedLdapServer.destroy(); + } + + @Test + public void testEmbeddedLdapServerInitialization() throws IndexNotFoundException { + LdapServer ldapServer = embeddedLdapServer.getLdapServer(); + assertNotNull(ldapServer); + + DirectoryService directoryService = embeddedLdapServer.getDirectoryService(); + assertNotNull(directoryService); + assertNotNull(directoryService.getSchemaPartition()); + assertNotNull(directoryService.getSystemPartition()); + assertNotNull(directoryService.getSchemaManager()); + assertNotNull(directoryService.getDnFactory()); + + assertNotNull(directoryService.isDenormalizeOpAttrsEnabled()); + + ChangeLog changeLog = directoryService.getChangeLog(); + + assertNotNull(changeLog); + assertFalse(changeLog.isEnabled()); + + assertNotNull(directoryService.isStarted()); + assertNotNull(ldapServer.isStarted()); + + List userList = new ArrayList(embeddedLdapServer.getUserIndexMap().keySet()); + java.util.Collections.sort(userList); + List checkList = Arrays.asList("uid"); + assertEquals(userList, checkList); + } + +// @Test + public void testEmbeddedLdapAvailable() { + try { + List usahs = ldapManager.getUsers(1L); + assertFalse("should find at least the admin user", usahs.isEmpty()); + } catch (NoLdapUserMatchingQueryException e) { + fail(e.getLocalizedMessage()); + } + } + + @Test + public void testSchemaLoading() { + try { + assertTrue("standard not loaded", embeddedLdapServer.addSchemaFromClasspath("other")); +// we need member of in ACS nowadays (backwards comptability broken): +// assertTrue("memberOf schema not loaded", embeddedLdapServer.addSchemaFromPath(new File("src/test/resources/memberOf"), "microsoft")); + } catch (LdapException | IOException e) { + fail(e.getLocalizedMessage()); + } + } + +// @Test + public void testUserCreation() { + LdapConnection connection = new LdapNetworkConnection( "localhost", 10389 ); + try { + connection.bind( "uid=admin,ou=system", "secret" ); + + connection.add(new DefaultEntry( + "ou=acsadmins,ou=users,ou=system", + "objectClass: organizationalUnit", +// might also need to be objectClass: top + "ou: acsadmins" + )); + connection.add(new DefaultEntry( + "uid=dahn,ou=acsadmins,ou=users,ou=system", + "objectClass: inetOrgPerson", + "objectClass: top", + "cn: dahn", + "sn: Hoogland", + "givenName: Daan", + "mail: d@b.c", + "uid: dahn" + )); + + connection.add( + new DefaultEntry( + "cn=JuniorAdmins,ou=groups,ou=system", // The Dn + "objectClass: groupOfUniqueNames", + "ObjectClass: top", + "cn: JuniorAdmins", + "uniqueMember: uid=dahn,ou=acsadmins,ou=system,ou=users") ); + + assertTrue( connection.exists( "cn=JuniorAdmins,ou=groups,ou=system" ) ); + assertTrue( connection.exists( "uid=dahn,ou=acsadmins,ou=users,ou=system" ) ); + + Entry ourUser = connection.lookup("uid=dahn,ou=acsadmins,ou=users,ou=system"); + ourUser.add("memberOf", "cn=JuniorAdmins,ou=groups,ou=system"); + AddRequest addRequest = new AddRequestImpl(); + addRequest.setEntry( ourUser ); + AddResponse response = connection.add( addRequest ); + assertNotNull( response ); + // We would need to either +// assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() ); + // or have the automatic virtual attribute + + List usahs = ldapManager.getUsers(1L); + assertEquals("now an admin and a normal user should be present",2, usahs.size()); + + } catch (LdapException | NoLdapUserMatchingQueryException e) { + fail(e.getLocalizedMessage()); + } + } +} diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapTestConfigTool.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapTestConfigTool.java new file mode 100644 index 00000000000..0507a01bc58 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapTestConfigTool.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.ldap; + +import org.apache.cloudstack.framework.config.ConfigKey; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class LdapTestConfigTool { + public LdapTestConfigTool() { + } + + void overrideConfigValue(LdapConfiguration ldapConfiguration, final String configKeyName, final Object o) throws IllegalAccessException, NoSuchFieldException { + Field configKey = LdapConfiguration.class.getDeclaredField(configKeyName); + configKey.setAccessible(true); + + ConfigKey key = (ConfigKey)configKey.get(ldapConfiguration); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(configKey, configKey.getModifiers() & ~Modifier.FINAL); + + Field f = ConfigKey.class.getDeclaredField("_value"); + f.setAccessible(true); + modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); + f.set(key, o); + + Field dynamic = ConfigKey.class.getDeclaredField("_isDynamic"); + dynamic.setAccessible(true); + modifiersField.setInt(dynamic, dynamic.getModifiers() & ~Modifier.FINAL); + dynamic.setBoolean(key, false); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnboundidZapdotConnectionTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnboundidZapdotConnectionTest.java new file mode 100644 index 00000000000..3acc7c5474e --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnboundidZapdotConnectionTest.java @@ -0,0 +1,89 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.ldap; + +import com.google.common.collect.Iterators; +import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPInterface; +import com.unboundid.ldap.sdk.SearchResult; +import com.unboundid.ldap.sdk.SearchScope; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.zapodot.junit.ldap.EmbeddedLdapRule; +import org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(MockitoJUnitRunner.class) +public class LdapUnboundidZapdotConnectionTest { + private static final String DOMAIN_DSN; + + static { + DOMAIN_DSN = "dc=cloudstack,dc=org"; + } + + @Rule + public EmbeddedLdapRule embeddedLdapRule = EmbeddedLdapRuleBuilder + .newInstance() + .usingDomainDsn(DOMAIN_DSN) + .importingLdifs("unboundid.ldif") + .build(); + + @Test + public void testLdapInteface() throws Exception { + // Test using the UnboundID LDAP SDK directly + final LDAPInterface ldapConnection = embeddedLdapRule.ldapConnection(); + final SearchResult searchResult = ldapConnection.search(DOMAIN_DSN, SearchScope.SUB, "(objectClass=person)"); + assertEquals(24, searchResult.getEntryCount()); + } + + @Test + public void testUnsharedLdapConnection() throws Exception { + // Test using the UnboundID LDAP SDK directly by using the UnboundID LDAPConnection type + final LDAPConnection ldapConnection = embeddedLdapRule.unsharedLdapConnection(); + final SearchResult searchResult = ldapConnection.search(DOMAIN_DSN, SearchScope.SUB, "(objectClass=person)"); + assertEquals(24, searchResult.getEntryCount()); + } + + @Test + public void testDirContext() throws Exception { + + // Test using the good ol' JDNI-LDAP integration + final DirContext dirContext = embeddedLdapRule.dirContext(); + final SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + final NamingEnumeration resultNamingEnumeration = + dirContext.search(DOMAIN_DSN, "(objectClass=person)", searchControls); + assertEquals(24, Iterators.size(Iterators.forEnumeration(resultNamingEnumeration))); + } + @Test + public void testContext() throws Exception { + + // Another test using the good ol' JDNI-LDAP integration, this time with the Context interface + final Context context = embeddedLdapRule.context(); + final Object user = context.lookup("cn=Cammy Petri,dc=cloudstack,dc=org"); + assertNotNull(user); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnitConnectionTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnitConnectionTest.java new file mode 100644 index 00000000000..667d14e3dd2 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUnitConnectionTest.java @@ -0,0 +1,62 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.ldap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import com.btmatthews.ldapunit.DirectoryTester; +import com.btmatthews.ldapunit.DirectoryServerConfiguration; +import com.btmatthews.ldapunit.DirectoryServerRule; + +@RunWith(MockitoJUnitRunner.class) +@DirectoryServerConfiguration(ldifFiles = {LdapUnitConnectionTest.LDIF_FILE_NAME}, + baseDN = LdapUnitConnectionTest.DOMAIN_DSN, + port = LdapUnitConnectionTest.PORT, + authDN = LdapUnitConnectionTest.BIND_DN, +authPassword = LdapUnitConnectionTest.SECRET) +public class LdapUnitConnectionTest { + static final String LDIF_FILE_NAME = "ldapunit.ldif"; + static final String DOMAIN_DSN = "dc=am,dc=echt,dc=net"; + static final String BIND_DN = "uid=admin,ou=cloudstack"; + static final String SECRET = "secretzz"; + static final int PORT =11389; + + @Rule + public DirectoryServerRule directoryServerRule = new DirectoryServerRule(); + + private DirectoryTester directoryTester; + + @Before + public void setUp() { + directoryTester = new DirectoryTester("localhost", PORT, BIND_DN, SECRET); + } + + @After + public void tearDown() { + directoryTester.disconnect(); + } + + @Test + public void testLdapInteface() throws Exception { + directoryTester.assertDNExists("dc=am,dc=echt,dc=net"); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUserManagerFactoryTest.java b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUserManagerFactoryTest.java new file mode 100644 index 00000000000..a3ece8dd88f --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/java/org/apache/cloudstack/ldap/LdapUserManagerFactoryTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package org.apache.cloudstack.ldap; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; + +import static org.junit.Assert.assertTrue; + +@RunWith(MockitoJUnitRunner.class) +public class LdapUserManagerFactoryTest { + + @Mock + ApplicationContext applicationCtx; + + @Mock + AutowireCapableBeanFactory autowireCapableBeanFactory; + + @Mock + protected LdapConfiguration _ldapConfiguration; + + @Spy + @InjectMocks + static LdapUserManagerFactory ldapUserManagerFactory = new LdapUserManagerFactory(); + + /** + * circumvent springframework for these {code ManagerImpl} + */ + @BeforeClass + public static void init() + { + ldapUserManagerFactory.ldapUserManagerMap.put(LdapUserManager.Provider.MICROSOFTAD, new ADLdapUserManagerImpl()); + ldapUserManagerFactory.ldapUserManagerMap.put(LdapUserManager.Provider.OPENLDAP, new OpenLdapUserManagerImpl()); + } + + @Before + public void setup() { + + } + @Test + public void getOpenLdapInstance() { + LdapUserManager userManager = ldapUserManagerFactory.getInstance(LdapUserManager.Provider.OPENLDAP); + assertTrue("x dude", userManager instanceof OpenLdapUserManagerImpl); + } + + @Test + public void getMSADInstance() { + LdapUserManager userManager = ldapUserManagerFactory.getInstance(LdapUserManager.Provider.MICROSOFTAD); + assertTrue("wrong dude", userManager instanceof ADLdapUserManagerImpl); + } +} \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/resources/ldapunit.ldif b/plugins/user-authenticators/ldap/src/test/resources/ldapunit.ldif new file mode 100644 index 00000000000..a6c1da15411 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/resources/ldapunit.ldif @@ -0,0 +1,151 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +version: 1 + +dn: ou=groups,dc=am,dc=echt,dc=net +objectClass: organizationalUnit +objectClass: top +ou: Groups + +dn: cn=JuniorAdmins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: JuniorAdmins +uniqueMember: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: organizationalUnit +objectClass: top +ou: acsadmins + +dn: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: dahn +sn: Hoogland +givenName: Daan +mail: d@b.c +uid: dahn + +dn: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: demo + +dn: cn=SeniorAdmins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: SeniorAdmins +uniqueMember: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: cn=admins,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: groupOfNames +objectClass: top +cn: admins +member: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalperson +objectClass: top +objectClass: inetorgperson +cn: Paul Angus +sn: angus +givenName: paul +mail: paul.angus@shapeblue.com +uid: pga + +dn: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: demo2 + +dn: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: demo3 + +dn: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: demo4 + +dn: cn=Admins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: Admins +uniqueMember: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=noadmin,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: double + +dn: uid=noadmin,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +cn: demo +sn: User +givenName: demo +mail: d@b.c +uid: noadmin \ No newline at end of file diff --git a/plugins/user-authenticators/ldap/src/test/resources/log4j.xml b/plugins/user-authenticators/ldap/src/test/resources/log4j.xml new file mode 100755 index 00000000000..031d2283580 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/resources/log4j.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/user-authenticators/ldap/src/test/resources/minimal.ldif b/plugins/user-authenticators/ldap/src/test/resources/minimal.ldif new file mode 100644 index 00000000000..46e87c29ab8 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/resources/minimal.ldif @@ -0,0 +1,243 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +version: 1 + +dn: dc=am,dc=echt,dc=net +objectClass: domain +objectClass: top +dc: am + +dn: ou=groups,dc=am,dc=echt,dc=net +objectClass: organizationalUnit +objectClass: top +ou: Groups + +dn: cn=JuniorAdmins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: JuniorAdmins +uniqueMember: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: organizationalUnit +objectClass: top +ou: acsadmins + +dn: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: person +objectClass: organizationalPerson +objectClass: top +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +cn: dahn +sn: Hoogland +givenName: Daan +inetUserStatus: Active +mail: d@b.c +uid: dahn + +dn: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: demo + +dn: cn=SeniorAdmins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: SeniorAdmins +uniqueMember: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: cn=admins,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: groupOfNames +objectClass: top +cn: admins +member: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +member: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: uid=pga,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalperson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetorgperson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: Paul Angus +sn: angus +givenName: paul +inetUserStatus: Active +mail: paul.angus@shapeblue.com +uid: pga + +dn: uid=demo2,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: demo2 + +dn: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: demo3 + +dn: uid=demo4,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: demo4 + +dn: cn=Admins,ou=groups,dc=am,dc=echt,dc=net +objectClass: groupOfUniqueNames +objectClass: top +cn: Admins +uniqueMember: uid=dahn,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=demo3,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net +uniqueMember: uid=noadmin,ou=acsadmins,dc=am,dc=echt,dc=net + +dn: uid=double,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: double + +dn: uid=noadmin,ou=acsadmins,dc=am,dc=echt,dc=net +objectClass: iplanet-am-user-service +objectClass: person +objectClass: organizationalPerson +objectClass: sunAMAuthAccountLockout +objectClass: iPlanetPreferences +objectClass: top +objectClass: sunIdentityServerLibertyPPService +objectClass: sunFMSAML2NameIdentifier +objectClass: forgerock-am-dashboard-service +objectClass: inetOrgPerson +objectClass: sunFederationManagerDataStore +objectClass: devicePrintProfilesContainer +objectClass: iplanet-am-auth-configuration-service +objectClass: iplanet-am-managed-person +objectClass: inetuser +cn: demo +sn: User +givenName: demo +inetUserStatus: Active +mail: d@b.c +uid: noadmin + diff --git a/plugins/user-authenticators/ldap/src/test/resources/testContext.xml b/plugins/user-authenticators/ldap/src/test/resources/testContext.xml new file mode 100644 index 00000000000..357a14f6146 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/resources/testContext.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/plugins/user-authenticators/ldap/src/test/resources/unboundid.ldif b/plugins/user-authenticators/ldap/src/test/resources/unboundid.ldif new file mode 100644 index 00000000000..407837256c9 --- /dev/null +++ b/plugins/user-authenticators/ldap/src/test/resources/unboundid.ldif @@ -0,0 +1,311 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +version: 1 + +dn: dc=cloudstack,dc=org +objectClass: dcObject +objectClass: organization +dc: cloudstack +o: cloudstack + +dn: cn=Ryan Murphy,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Ryan Murphy +sn: Murphy +givenName: Ryan +mail: rmurphy@cloudstack.org +uid: rmurphy +userpassword:: cGFzc3dvcmQ= + +dn: cn=Barbara Brewer,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Barbara Brewer +sn: Brewer +mail: bbrewer@cloudstack.org +uid: bbrewer +userpassword:: cGFzc3dvcmQ= + +dn: cn=Zak Wilkinson,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Zak Wilkinson +givenname: Zak +sn: Wilkinson +uid: zwilkinson +userpassword:: cGFzc3dvcmQ= + +dn: cn=Archie Shingleton,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Archie Shingleton +sn: Shingleton +givenName: Archie +mail: ashingleton@cloudstack.org +uid: ashingleton +userpassword:: cGFzc3dvcmQ= + +dn: cn=Cletus Pears,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Cletus Pears +sn: Pears +givenName: Cletus +mail: cpears@cloudstack.org +uid: cpears +userpassword:: cGFzc3dvcmQ= + +dn: cn=Teisha Milewski,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Teisha Milewski +sn: Milewski +givenName: Teisha +mail: tmilewski@cloudstack.org +uid: tmilewski +userpassword:: cGFzc3dvcmQ= + +dn: cn=Eloy Para,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Eloy Para +sn: Para +givenName: Eloy +mail: epara@cloudstack.org +uid: epara +userpassword:: cGFzc3dvcmQ= + +dn: cn=Elaine Lamb,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Elaine Lamb +sn: Lamb +givenName: Elaine +mail: elamb@cloudstack.org +uid: elamb +userpassword:: cGFzc3dvcmQ= + +dn: cn=Soon Griffen,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Soon Griffen +sn: Griffen +givenName: Soon +mail: sgriffen@cloudstack.org +uid: sgriffen +userpassword:: cGFzc3dvcmQ= + +dn: cn=Tran Neisler,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Tran Neisler +sn: Neisler +givenName: Tran +mail: tneisler@cloudstack.org +uid: tneisler +userpassword:: cGFzc3dvcmQ= + +dn: cn=Mirella Zeck,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Mirella Zeck +sn: Zeck +givenName: Mirella +mail: mzeck@cloudstack.org +uid: mzeck +userpassword:: cGFzc3dvcmQ= + +dn: cn=Greg Hoskin,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Greg Hoskin +sn: Hoskin +givenName: Greg +mail: ghoskin@cloudstack.org +uid: ghoskin +userpassword:: cGFzc3dvcmQ= + +dn: cn=Johanne Runyon,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Johanne Runyon +sn: Runyon +givenName: Johanne +mail: jrunyon@cloudstack.org +uid: jrunyon +userpassword:: cGFzc3dvcmQ= + +dn: cn=Mabelle Waiters,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Mabelle Waiters +sn: Waiters +givenName: Mabelle +mail: mwaiters@cloudstack.org +uid: mwaiters +userpassword:: cGFzc3dvcmQ= + +dn: cn=Phillip Fruge,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Phillip Fruge +sn: Fruge +givenName: Phillip +mail: pfruge@cloudstack.org +uid: pfruge +userpassword:: cGFzc3dvcmQ= + +dn: cn=Jayna Ridenhour,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Jayna Ridenhour +sn: Ridenhour +givenName: Jayna +mail: jridenhour@cloudstack.org +uid: jridenhour +userpassword:: cGFzc3dvcmQ= + +dn: cn=Marlyn Mandujano,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Marlyn Mandujano +sn: Mandujano +givenName: Marlyn +mail: mmandujano@cloudstack.org +uid: mmandujano +userpassword:: cGFzc3dvcmQ= + +dn: cn=Shaunna Scherer,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Shaunna Scherer +sn: Scherer +givenName: Shaunna +mail: sscherer@cloudstack.org +uid: sscherer +userpassword:: cGFzc3dvcmQ= + +dn: cn=Adriana Bozek,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Adriana Bozek +sn: Bozek +givenName: Adriana +mail: abozek@cloudstack.org +uid: abozek +userpassword:: cGFzc3dvcmQ= + +dn: cn=Silvana Chipman,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Silvana Chipman +sn: Chipman +givenName: Silvana +mail: schipman@cloudstack.org +uid: schipman +userpassword:: cGFzc3dvcmQ= + +dn: cn=Marion Wasden,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Marion Wasden +sn: Wasden +givenName: Marion +mail: mwasden@cloudstack.org +uid: mwasden +userpassword:: cGFzc3dvcmQ= + +dn: cn=Anisa Casson,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Anisa Casson +sn: Casson +givenName: Anisa +mail: acasson@cloudstack.org +uid: acasson +userpassword:: cGFzc3dvcmQ= + +dn: cn=Noel King,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Noel King +sn: King +givenName: Noel +mail: nking@cloudstack.org +uid: nking +userpassword:: cGFzc3dvcmQ= + + +dn: cn=Cammy Petri,dc=cloudstack,dc=org +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Cammy Petri +sn: Petri +givenName: Cammy +mail: cpetri@cloudstack.org +uid: cpetri +userpassword:: cGFzc3dvcmQ= + diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 1ba083f88ac..81a0ba66941 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -413,6 +413,36 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q return response; } + public ListResponse searchForUsers(Long domainId, boolean recursive) throws PermissionDeniedException { + Account caller = CallContext.current().getCallingAccount(); + + List permittedAccounts = new ArrayList(); + + boolean listAll = true; + Long id = null; + + if (caller.getType() == Account.ACCOUNT_TYPE_NORMAL) { + long currentId = CallContext.current().getCallingUser().getId(); + if (id != null && currentId != id.longValue()) { + throw new PermissionDeniedException("Calling user is not authorized to see the user requested by id"); + } + id = currentId; + } + Object username = null; + Object type = null; + String accountName = null; + Object state = null; + Object keyword = null; + + Pair, Integer> result = getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, + null); + ListResponse response = new ListResponse(); + List userResponses = ViewResponseHelper.createUserResponse(CallContext.current().getCallingAccount().getDomainId(), + result.first().toArray(new UserAccountJoinVO[result.first().size()])); + response.setResponses(userResponses, result.second()); + return response; + } + private Pair, Integer> searchForUsersInternal(ListUsersCmd cmd) throws PermissionDeniedException { Account caller = CallContext.current().getCallingAccount(); @@ -427,42 +457,52 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q } id = currentId; } - Ternary domainIdRecursiveListProject = new Ternary(cmd.getDomainId(), cmd.isRecursive(), null); - _accountMgr.buildACLSearchParameters(caller, id, cmd.getAccountName(), null, permittedAccounts, domainIdRecursiveListProject, listAll, false); - Long domainId = domainIdRecursiveListProject.first(); - Boolean isRecursive = domainIdRecursiveListProject.second(); - ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); - - Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal()); - Object username = cmd.getUsername(); Object type = cmd.getAccountType(); - Object accountName = cmd.getAccountName(); + String accountName = cmd.getAccountName(); Object state = cmd.getState(); Object keyword = cmd.getKeyword(); + Long domainId = cmd.getDomainId(); + boolean recursive = cmd.isRecursive(); + Long pageSizeVal = cmd.getPageSizeVal(); + Long startIndex = cmd.getStartIndex(); + + Filter searchFilter = new Filter(UserAccountJoinVO.class, "id", true, startIndex, pageSizeVal); + + return getUserListInternal(caller, permittedAccounts, listAll, id, username, type, accountName, state, keyword, domainId, recursive, searchFilter); + } + + private Pair, Integer> getUserListInternal(Account caller, List permittedAccounts, boolean listAll, Long id, Object username, Object type, + String accountName, Object state, Object keyword, Long domainId, boolean recursive, Filter searchFilter) { + Ternary domainIdRecursiveListProject = new Ternary(domainId, recursive, null); + _accountMgr.buildACLSearchParameters(caller, id, accountName, null, permittedAccounts, domainIdRecursiveListProject, listAll, false); + domainId = domainIdRecursiveListProject.first(); + Boolean isRecursive = domainIdRecursiveListProject.second(); + ListProjectResourcesCriteria listProjectResourcesCriteria = domainIdRecursiveListProject.third(); + SearchBuilder sb = _userAccountJoinDao.createSearchBuilder(); _accountMgr.buildACLViewSearchBuilder(sb, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria); - sb.and("username", sb.entity().getUsername(), SearchCriteria.Op.LIKE); + sb.and("username", sb.entity().getUsername(), Op.LIKE); if (id != null && id == 1) { // system user should NOT be searchable List emptyList = new ArrayList(); return new Pair, Integer>(emptyList, 0); } else if (id != null) { - sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + sb.and("id", sb.entity().getId(), Op.EQ); } else { // this condition is used to exclude system user from the search // results - sb.and("id", sb.entity().getId(), SearchCriteria.Op.NEQ); + sb.and("id", sb.entity().getId(), Op.NEQ); } - sb.and("type", sb.entity().getAccountType(), SearchCriteria.Op.EQ); - sb.and("domainId", sb.entity().getDomainId(), SearchCriteria.Op.EQ); - sb.and("accountName", sb.entity().getAccountName(), SearchCriteria.Op.EQ); - sb.and("state", sb.entity().getState(), SearchCriteria.Op.EQ); + sb.and("type", sb.entity().getAccountType(), Op.EQ); + sb.and("domainId", sb.entity().getDomainId(), Op.EQ); + sb.and("accountName", sb.entity().getAccountName(), Op.EQ); + sb.and("state", sb.entity().getState(), Op.EQ); if ((accountName == null) && (domainId != null)) { - sb.and("domainPath", sb.entity().getDomainPath(), SearchCriteria.Op.LIKE); + sb.and("domainPath", sb.entity().getDomainPath(), Op.LIKE); } SearchCriteria sc = sb.create(); @@ -472,15 +512,15 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q if (keyword != null) { SearchCriteria ssc = _userAccountJoinDao.createSearchCriteria(); - ssc.addOr("username", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("firstname", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("lastname", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("email", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("state", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("accountName", SearchCriteria.Op.LIKE, "%" + keyword + "%"); - ssc.addOr("accountType", SearchCriteria.Op.LIKE, "%" + keyword + "%"); + ssc.addOr("username", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("firstname", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("lastname", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("email", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("state", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("accountName", Op.LIKE, "%" + keyword + "%"); + ssc.addOr("accountType", Op.LIKE, "%" + keyword + "%"); - sc.addAnd("username", SearchCriteria.Op.SC, ssc); + sc.addAnd("username", Op.SC, ssc); } if (username != null) { diff --git a/test/integration/plugins/ldap/ldap_test_data.py b/test/integration/plugins/ldap/ldap_test_data.py new file mode 100644 index 00000000000..bfb89d58ae1 --- /dev/null +++ b/test/integration/plugins/ldap/ldap_test_data.py @@ -0,0 +1,189 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +class LdapTestData: + #constants + configuration = "ldap_configuration" + syncAccounts = "accountsToSync" + parentDomain = "LDAP" + manualDomain = "manual" + importDomain = "import" + syncDomain = "sync" + name = "name" + id = "id" + notAvailable = "N/A" + groups = "groups" + group = "group" + seniorAccount = "seniors" + juniorAccount = "juniors" + + ldap_ip_address = "localhost" + ldap_port = 389 + hostname = "hostname" + port = "port" + dn = "dn" + ou = "ou" + cn = "cn" + member = "uniqueMember" + basedn = "basedn" + basednConfig = "ldap.basedn" + ldapPw = "ldapPassword" + ldapPwConfig = "ldap.bind.password" + principal = "ldapUsername" + principalConfig = "ldap.bind.principal" + users = "users" + objectClass = "objectClass" + sn = "sn" + givenName = "givenName" + uid = "uid" + domains = "domains" + type = "accounttype" + password = "userPassword" + mail = "email" + groupPrinciple = "ldap.search.group.principle" + + basednValue = "dc=echt,dc=net" + people_dn = "ou=people,"+basednValue + groups_dn = "ou=groups,"+basednValue + admins = "ou=admins,"+groups_dn + juniors = "ou=juniors,"+groups_dn + seniors = "ou=seniors,"+groups_dn + userObject = "userObject" + usernameAttribute = "usernameAttribute" + memberAttribute = "memberAttribute" + mailAttribute = "emailAttribute" + + def __init__(self): + self.testdata = { + LdapTestData.configuration: { + LdapTestData.mailAttribute: "mail", + LdapTestData.userObject: "person", + LdapTestData.usernameAttribute: LdapTestData.uid, + LdapTestData.memberAttribute: LdapTestData.member, + # global values for use in all domains + LdapTestData.hostname: LdapTestData.ldap_ip_address, + LdapTestData.port: LdapTestData.ldap_port, + LdapTestData.basedn: LdapTestData.basednValue, + LdapTestData.ldapPw: "secret", + LdapTestData.principal: "cn=willie,"+LdapTestData.basednValue, + }, + LdapTestData.groups: [ + { + LdapTestData.dn : LdapTestData.people_dn, + LdapTestData.objectClass: ["organizationalUnit", "top"], + LdapTestData.ou : "People" + }, + { + LdapTestData.dn : LdapTestData.groups_dn, + LdapTestData.objectClass: ["organizationalUnit", "top"], + LdapTestData.ou : "Groups" + }, + { + LdapTestData.dn : LdapTestData.seniors, + LdapTestData.objectClass: ["groupOfUniqueNames", "top"], + LdapTestData.ou : "seniors", + LdapTestData.cn : "seniors", + LdapTestData.member : ["uid=bobby,ou=people,"+LdapTestData.basednValue, "uid=rohit,ou=people,"+LdapTestData.basednValue] + }, + { + LdapTestData.dn : LdapTestData.juniors, + LdapTestData.objectClass : ["groupOfUniqueNames", "top"], + LdapTestData.ou : "juniors", + LdapTestData.cn : "juniors", + LdapTestData.member : ["uid=dahn,ou=people,"+LdapTestData.basednValue, "uid=paul,ou=people,"+LdapTestData.basednValue] + } + ], + LdapTestData.users: [ + { + LdapTestData.dn : "uid=bobby,ou=people,"+LdapTestData.basednValue, + LdapTestData.objectClass : ["inetOrgPerson", "top", "person"], + LdapTestData.cn : "bobby", + LdapTestData.sn: "Stoyanov", + LdapTestData.givenName : "Boris", + LdapTestData.uid : "bobby", + LdapTestData.mail: "bobby@echt.net" + }, + { + LdapTestData.dn : "uid=dahn,ou=people,"+LdapTestData.basednValue, + LdapTestData.objectClass : ["inetOrgPerson", "top", "person"], + LdapTestData.cn : "dahn", + LdapTestData.sn: "Hoogland", + LdapTestData.givenName : "Daan", + LdapTestData.uid : "dahn", + LdapTestData.mail: "dahn@echt.net" + }, + { + LdapTestData.dn : "uid=paul,ou=people,"+LdapTestData.basednValue, + LdapTestData.objectClass : ["inetOrgPerson", "top", "person"], + LdapTestData.cn : "Paul", + LdapTestData.sn: "Angus", + LdapTestData.givenName : "Paul", + LdapTestData.uid : "paul", + LdapTestData.mail: "paul@echt.net" + }, + { + LdapTestData.dn : "uid=rohit,ou=people,"+LdapTestData.basednValue, + LdapTestData.objectClass : ["inetOrgPerson", "top", "person"], + LdapTestData.cn : "rhtyd", + LdapTestData.sn: "Yadav", + LdapTestData.givenName : "Rohit", + LdapTestData.uid : "rohit", + LdapTestData.mail: "rhtyd@echt.net" + }, + # extra test user (just in case) + # { + # LdapTestData.dn : "uid=noone,ou=people,"+LdapTestData.basednValue, + # LdapTestData.objectClass : ["inetOrgPerson", "person"], + # LdapTestData.cn : "noone", + # LdapTestData.sn: "a User", + # LdapTestData.givenName : "Not", + # LdapTestData.uid : "noone", + # LdapTestData.mail: "noone@echt.net", + # LdapTestData.password: 'password' + # }, + ], + LdapTestData.domains : [ + { + LdapTestData.name : LdapTestData.parentDomain, + LdapTestData.id : LdapTestData.notAvailable + }, + { + LdapTestData.name : LdapTestData.manualDomain, + LdapTestData.id : LdapTestData.notAvailable + }, + { + LdapTestData.name : LdapTestData.importDomain, + LdapTestData.id : LdapTestData.notAvailable + }, + { + LdapTestData.name : LdapTestData.syncDomain, + LdapTestData.id : LdapTestData.notAvailable + }, + ], + LdapTestData.syncAccounts : [ + { + LdapTestData.name : LdapTestData.juniorAccount, + LdapTestData.type : 0, + LdapTestData.group : LdapTestData.juniors + }, + { + LdapTestData.name : LdapTestData.seniorAccount, + LdapTestData.type : 2, + LdapTestData.group : LdapTestData.seniors + } + ], + } \ No newline at end of file diff --git a/test/integration/plugins/ldap/test_ldap.py b/test/integration/plugins/ldap/test_ldap.py new file mode 100644 index 00000000000..b017957b936 --- /dev/null +++ b/test/integration/plugins/ldap/test_ldap.py @@ -0,0 +1,476 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#Import Local Modules +from .ldap_test_data import LdapTestData +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.utils import (cleanup_resources) +from marvin.lib.base import (listLdapUsers, + ldapCreateAccount, + importLdapUsers, + User, + Domain, + Account, + addLdapConfiguration, + deleteLdapConfiguration, + linkAccountToLdap, + linkDomainToLdap, + updateConfiguration) +from marvin.lib.common import (get_domain, + get_zone) +from nose.plugins.attrib import attr + +# for login validation +import requests + +import logging + +class TestLDAP(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + ''' + needs to + - create the applicable ldap accounts in the directory server + - create three domains: + -- LDAP/manual + -- LDAP/import + -- LDAP/sync + ''' + cls.logger = logging.getLogger(__name__) + stream_handler = logging.StreamHandler() + logger_formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + stream_handler.setFormatter(logger_formatter) + cls.logger.setLevel(logging.DEBUG) + cls.logger.addHandler(stream_handler) + + cls.logger.info("Setting up Class") + testClient = super(TestLDAP, cls).getClsTestClient() + cls.apiclient = testClient.getApiClient() + + try: + # Setup test data + cls.testdata = LdapTestData() + if cls.config.TestData and cls.config.TestData.Path: + cls.logger.debug("reading extra config from '" + cls.config.TestData.Path + "'") + cls.testdata.update(cls.config.TestData.Path) + cls.logger.debug(cls.testdata) + + cls.services = testClient.getParsedTestDataConfig() + cls.services["configurableData"]["ldap_configuration"] = cls.testdata.testdata["ldap_configuration"] + cls.logger.debug(cls.services["configurableData"]["ldap_configuration"]) + + # Get Zone, Domain + cls.domain = get_domain(cls.apiclient) + cls.logger.debug("standard domain: %s" % cls.domain.id) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + + cls._cleanup = [] + + # Build the test env + cls.create_domains(cls.testdata) + cls.configure_ldap_for_domains(cls.testdata) + + cls.test_user = [ + cls.testdata.testdata[LdapTestData.users][0][LdapTestData.uid], + cls.testdata.testdata[LdapTestData.users][1][LdapTestData.uid], + cls.testdata.testdata[LdapTestData.users][2][LdapTestData.uid] + ] + except Exception as e: + cls.logger.debug("Exception in setUpClass(cls): %s" % e) + cls.tearDownClass() + raise Exception("setup failed due to %s", e) + + return + + @classmethod + def tearDownClass(cls): + cls.logger.info("Tearing Down Class") + try: + cleanup_resources(cls.apiclient, reversed(cls._cleanup)) + cls.remove_ldap_configuration_for_domains() + cls.logger.debug("done cleaning up resources in tearDownClass(cls) %s") + except Exception as e: + cls.logger.debug("Exception in tearDownClass(cls): %s" % e) + + def setUp(self): + self.cleanup = [] + + self.server_details = self.config.__dict__["mgtSvr"][0].__dict__ + self.server_url = "http://%s:8080/client/api" % self.server_details['mgtSvrIp'] + + return + + def tearDown(self): + try: + cleanup_resources(self.apiclient, self.cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + @attr(tags=["smoke", "advanced"], required_hardware="false") + def test_01_manual(self): + ''' + test if an account can be imported + + prerequisite + a ldap host is configured + a domain is linked to cloudstack + ''' + cmd = listLdapUsers.listLdapUsersCmd() + cmd.domainid = self.manualDomain.id + cmd.userfilter = "LocalDomain" + response = self.apiclient.listLdapUsers(cmd) + self.logger.info("users found for linked domain %s" % response) + self.assertEqual(len(response), len(self.testdata.testdata[LdapTestData.users]), "unexpected number (%d) of ldap users" % len(self.testdata.testdata[LdapTestData.users])) + + cmd = ldapCreateAccount.ldapCreateAccountCmd() + cmd.domainid = self.manualDomain.id + cmd.accounttype = 0 + cmd.username = self.test_user[1] + create_response = self.apiclient.ldapCreateAccount(cmd) + + # cleanup + # last results id should be the account + list_response = Account.list(self.apiclient, id=create_response.id) + account_created = Account(list_response[0].__dict__) + self.cleanup.append(account_created) + + self.assertEqual(len(create_response.user), 1, "only one user %s should be present" % self.test_user[1]) + + self.assertEqual(len(list_response), + 1, + "only one account (for user %s) should be present" % self.test_user[1]) + + return + + @attr(tags=["smoke", "advanced"], required_hardware="false") + def test_02_import(self): + ''' + test if components are synced + + prerequisite + a ldap host is configured + a domain is linked to cloudstack + ''' + domainid = self.importDomain.id + + cmd = importLdapUsers.importLdapUsersCmd() + cmd.domainid = domainid + cmd.accounttype = 0 + import_response = self.apiclient.importLdapUsers(cmd) + + # this is needed purely for cleanup: + # cleanup + list_response = Account.list(self.apiclient, domainid=domainid) + for account in list_response: + account_created = Account(account.__dict__) + self.logger.debug("account to clean: %s (id: %s)" % (account_created.name, account_created.id)) + self.cleanup.append(account_created) + + self.assertEqual(len(import_response), len(self.testdata.testdata[LdapTestData.users]), "unexpected number of ldap users") + + self.assertEqual(len(list_response), len(self.testdata.testdata[LdapTestData.users]), "only one account (for user %s) should be present" % self.test_user[1]) + + return + + @attr(tags=["smoke", "advanced"], required_hardware="false") + def test_03_sync(self): + ''' + test if components are synced + + prerequisite + a ldap host is configured + a domain is linked to cloudstack + some accounts in that domain are linked to groups in ldap + ''' + domainid = self.syncDomain.id + username = self.test_user[1] + + # validate the user doesn't exist + response = User.list(self.apiclient,domainid=domainid,username=username) + self.assertEqual(response, None, "user should not exist yet") + + self.logon_test_user(username) + + # now validate the user exists in domain + response = User.list(self.apiclient,domainid=domainid,username=username) + for user in response: + user_created = User(user.__dict__) + self.logger.debug("user to clean: %s (id: %s)" % (user_created.username, user_created.id)) + self.cleanup.append(user_created) + + # now verify the creation of the user + self.assertEqual(len(response), 1, "user should exist by now") + + return + + @attr(tags=["smoke", "advanced"], required_hardware="false") + def test_04_filtered_list_of_users(self): + ''' + test if we can get a filtered list of ldap users + + prerequisite + a ldap host is configured + a couple of ldapdomains are linked to cloudstack domains + some accounts in those domain are linked to groups in ldap + some ldap accounts are linked and present with the same uid + some ldap accounts are not yet linked but present at other locations in cloudstack + + NOTE 1: if this test is run last only the explicitely imported test user from test_03_sync + is in the system. The accounts from test_01_manual and test_02_import should have been cleared + by the test tearDown(). We can not depend on test_03_sync having run so the test must avoid + depending on it either being available or not. + + NOTE 2: this test will not work if the ldap users UIDs are already present in the ACS instance + against which is being tested + ''' + cmd = listLdapUsers.listLdapUsersCmd() + cmd.userfilter = "NoFilter" + cmd.domainid = self.manualDomain.id + response = self.apiclient.listLdapUsers(cmd) + self.logger.debug(cmd.userfilter + " : " + str(response)) + self.assertEqual(len(response), len(self.testdata.testdata[LdapTestData.users]), "unexpected number of ldap users") + + # create a non ldap user with the uid of cls.test_user[0] in parentDomain + # create a manual import of a cls.test_user[1] in manualDomain + # log on with test_user[2] in an syncDomain + + # we can now test all four filtertypes in syncDomain and inspect the respective outcomes for validity + + self.logon_test_user(self.test_user[2]) + + cmd.userfilter = "LocalDomain" + cmd.domainid = self.syncDomain.id + response = self.apiclient.listLdapUsers(cmd) + self.logger.debug(cmd.userfilter + " : " + str(response)) + self.assertEqual(len(response), + len(self.testdata.testdata[LdapTestData.users]) - 1, + "unexpected number of ldap users") + + @attr(tags=["smoke", "advanced"], required_hardware="false") + def test_05_relink_account_and_reuse_user(self): + ''' + test if an account and thus a user can be removed and re-added + + test if components still are synced + + prerequisite + a ldap host is configured + a domain is linked to cloudstack + some accounts in that domain are linked to groups in ldap + ''' + domainid = self.syncDomain.id + username = self.test_user[1] + + # validate the user doesn't exist + response = User.list(self.apiclient,domainid=domainid,username=username) + self.assertEqual(response, None, "user should not exist yet") + + self.logon_test_user(username) + + # now validate the user exists in domain + response = User.list(self.apiclient,domainid=domainid,username=username) + # for user in response: + # user_created = User(user.__dict__) + # self.debug("user to clean: %s (id: %s)" % (user_created.username, user_created.id)) + # # we don't cleanup to test if re-adding fails + # self.cleanup.append(user_created) + + # now verify the creation of the user + self.assertEqual(len(response), 1, "user should exist by now") + + # delete the account - quick implementation: user[1] happens to be a junior + self.junior_account.delete(self.apiclient) + + # add the account with the same ldap group + self.bind_account_to_ldap( + account=self.testdata.testdata[LdapTestData.syncAccounts][0]["name"], + ldapdomain=self.testdata.testdata[LdapTestData.syncAccounts][0]["group"], + accounttype=self.testdata.testdata[LdapTestData.syncAccounts][0]["accounttype"]) + + # logon the user - should succeed - reported to fail + self.logon_test_user(username) + + # now verify the creation of the user + response = User.list(self.apiclient,domainid=domainid,username=username) + for user in response: + user_created = User(user.__dict__) + self.debug("user to clean: %s (id: %s)" % (user_created.username, user_created.id)) + # we don't cleanup to test if re-adding fails + # self.cleanup.append(user_created) + self.assertEqual(len(response), 1, "user should exist again") + return + + + def logon_test_user(self, username, domain = None): + # login of dahn should create a user in account juniors + args = {} + args["command"] = 'login' + args["username"] = username + args["password"] = 'password' + if domain == None: + args["domain"] = "/" + self.parentDomain.name + "/" + self.syncDomain.name + else: + args["domain"] = domain + args["response"] = "json" + session = requests.Session() + try: + resp = session.post(self.server_url, params=args, verify=False) + except requests.exceptions.ConnectionError as e: + self.fail("Failed to attempt login request to mgmt server") + + + @classmethod + def create_domains(cls, td): + # create a parent domain + cls.parentDomain = cls.create_domain(td.testdata["domains"][0], parent_domain=cls.domain.id) + cls.manualDomain = cls.create_domain(td.testdata["domains"][1], parent_domain=cls.parentDomain.id) + cls.importDomain = cls.create_domain(td.testdata["domains"][2], parent_domain=cls.parentDomain.id) + cls.syncDomain = cls.create_domain(td.testdata["domains"][3], parent_domain=cls.parentDomain.id) + + @classmethod + def create_domain(cls, domain_to_create, parent_domain = None): + cls.logger.debug("Creating domain: %s under %s" % (domain_to_create[LdapTestData.name], parent_domain)) + if parent_domain: + domain_to_create["parentdomainid"] = parent_domain + tmpDomain = Domain.create(cls.apiclient, domain_to_create) + cls.logger.debug("Created domain %s with id %s " % (tmpDomain.name, tmpDomain.id)) + cls._cleanup.append(tmpDomain) + return tmpDomain + + @classmethod + def configure_ldap_for_domains(cls, td) : + cmd = addLdapConfiguration.addLdapConfigurationCmd() + cmd.hostname = td.testdata[LdapTestData.configuration][LdapTestData.hostname] + cmd.port = td.testdata[LdapTestData.configuration][LdapTestData.port] + + cls.logger.debug("configuring ldap server for domain %s" % LdapTestData.manualDomain) + cmd.domainid = cls.manualDomain.id + response = cls.apiclient.addLdapConfiguration(cmd) + cls.manualLdap = response + + cls.logger.debug("configuring ldap server for domain %s" % LdapTestData.importDomain) + cmd.domainid = cls.importDomain.id + response = cls.apiclient.addLdapConfiguration(cmd) + cls.importLdap = response + + cls.logger.debug("configuring ldap server for domain %s" % LdapTestData.syncDomain) + cmd.domainid = cls.syncDomain.id + response = cls.apiclient.addLdapConfiguration(cmd) + cls.syncLdap = response + + cls.set_ldap_settings_on_domain(domainid=cls.manualDomain.id) + cls.bind_domain_to_ldap(domainid=cls.manualDomain.id, ldapdomain=cls.testdata.admins) + + cls.set_ldap_settings_on_domain(domainid=cls.importDomain.id) + cls.bind_domain_to_ldap(domainid=cls.importDomain.id, ldapdomain=cls.testdata.admins, + accounttype=2, type="Group") # just to be testing different types + + cls.set_ldap_settings_on_domain(domainid=cls.syncDomain.id) + cls.create_sync_accounts() + + @classmethod + def remove_ldap_configuration_for_domains(cls) : + cls.logger.debug("deleting configurations for ldap server") + cmd = deleteLdapConfiguration.deleteLdapConfigurationCmd() + + cmd.hostname = cls.manualLdap.hostname + cmd.port = cls.manualLdap.port + cmd.domainid = cls.manualLdap.domainid + response = cls.apiclient.deleteLdapConfiguration(cmd) + cls.logger.debug("configuration deleted for %s" % response) + + cmd.hostname = cls.importLdap.hostname + cmd.port = cls.importLdap.port + cmd.domainid = cls.importLdap.domainid + response = cls.apiclient.deleteLdapConfiguration(cmd) + cls.logger.debug("configuration deleted for %s" % response) + + cmd.hostname = cls.syncLdap.hostname + cmd.port = cls.syncLdap.port + cmd.domainid = cls.syncLdap.domainid + cls.logger.debug("deleting configuration %s" % cmd) + response = cls.apiclient.deleteLdapConfiguration(cmd) + cls.logger.debug("configuration deleted for %s" % response) + + + @classmethod + def create_sync_accounts(cls): + cls.logger.debug("creating account: %s" % LdapTestData.seniors) + cls.senior_account = cls.bind_account_to_ldap( + account=cls.testdata.testdata[LdapTestData.syncAccounts][1]["name"], + ldapdomain=cls.testdata.testdata[LdapTestData.syncAccounts][1]["group"], + accounttype=cls.testdata.testdata[LdapTestData.syncAccounts][1]["accounttype"]) + cls.junior_account = cls.bind_account_to_ldap( + account=cls.testdata.testdata[LdapTestData.syncAccounts][0]["name"], + ldapdomain=cls.testdata.testdata[LdapTestData.syncAccounts][0]["group"], + accounttype=cls.testdata.testdata[LdapTestData.syncAccounts][0]["accounttype"]) + + @classmethod + def bind_account_to_ldap(cls, account, ldapdomain, type="Group", accounttype=0): + cmd = linkAccountToLdap.linkAccountToLdapCmd() + + cmd.domainid = cls.syncDomain.id + cmd.account = account + cmd.ldapdomain = ldapdomain + cmd.type = type + cmd.accounttype = accounttype + + response = cls.apiclient.linkAccountToLdap(cmd) + cls.logger.info("account linked to ladp %s" % response) + + # this is needed purely for cleanup: + response = Account.list(cls.apiclient, id=response.accountid) + account_created = Account(response[0].__dict__) + cls._cleanup.append(account_created) + return account_created + + @classmethod + def bind_domain_to_ldap(cls, domainid, ldapdomain, type="OU", accounttype=0): + cmd = linkDomainToLdap.linkDomainToLdapCmd() + cmd.domainid = domainid + cmd.type = type + cmd.accounttype = accounttype + cmd.ldapdomain = ldapdomain + response = cls.apiclient.linkDomainToLdap(cmd) + cls.logger.info("domain linked to ladp %s" % response) + + @classmethod + def set_ldap_settings_on_domain(cls, domainid): + cmd = updateConfiguration.updateConfigurationCmd() + cmd.domainid = domainid + cmd.name = LdapTestData.basednConfig + cmd.value = cls.testdata.testdata[LdapTestData.configuration][LdapTestData.basedn] + response = cls.apiclient.updateConfiguration(cmd) + cls.logger.debug("set the basedn: %s" % response) + cmd.name = LdapTestData.ldapPwConfig + cmd.value = cls.testdata.testdata[LdapTestData.configuration][LdapTestData.ldapPw] + response = cls.apiclient.updateConfiguration(cmd) + cls.logger.debug("set the pw: %s" % response) + cmd.name = LdapTestData.principalConfig + cmd.value = cls.testdata.testdata[LdapTestData.configuration][LdapTestData.principal] + response = cls.apiclient.updateConfiguration(cmd) + cls.logger.debug("set the id: %s" % response) + if cls.testdata.testdata[LdapTestData.configuration].has_key(LdapTestData.groupPrinciple) : + cmd.name = LdapTestData.groupPrinciple + cmd.value = cls.testdata.testdata[LdapTestData.configuration][LdapTestData.groupPrinciple] + response = cls.apiclient.updateConfiguration(cmd) + cls.logger.debug("set the id: %s" % response) + + +## python ldap utility functions diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 409fd325c3e..3bd1064271f 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -181,7 +181,7 @@ class Account: self.__dict__.update(items) @classmethod - def create(cls, apiclient, services, admin=False, domainid=None, roleid=None): + def create(cls, apiclient, services, admin=False, domainid=None, roleid=None, account=None): """Creates an account""" cmd = createAccount.createAccountCmd() @@ -213,6 +213,9 @@ class Account: if roleid: cmd.roleid = roleid + if account: + cmd.account = account + account = apiclient.createAccount(cmd) return Account(account.__dict__) diff --git a/ui/index.html b/ui/index.html index 7c8aec3dced..982a159d2d9 100644 --- a/ui/index.html +++ b/ui/index.html @@ -639,6 +639,7 @@ + diff --git a/ui/l10n/en.js b/ui/l10n/en.js index 87deba8142b..66f45ea19d0 100644 --- a/ui/l10n/en.js +++ b/ui/l10n/en.js @@ -1759,8 +1759,10 @@ var dictionary = { "label.use.vm.ips":"Use VM IPs", "label.used":"Used", "label.user":"User", +"label.user.conflict":"Conflict", "label.user.data":"User Data", "label.user.details":"User details", +"label.user.source":"source", "label.user.vm":"User VM", "label.username":"Username", "label.username.lower":"username", diff --git a/ui/scripts/accountsWizard.js b/ui/scripts/accountsWizard.js index 5b8e9a62d54..3f87ea88682 100644 --- a/ui/scripts/accountsWizard.js +++ b/ui/scripts/accountsWizard.js @@ -68,10 +68,43 @@ required: true }, docID: 'helpAccountLastName' - } + }, + conflictingusersource: { + label: 'label.user.conflict', + validation: { + required: true + }, + docID: 'helpConflictSource' + } }, informationNotInLdap: { + filter: { + label: 'label.filterBy', + docID: 'helpLdapUserFilter', + select: function(args) { + var items = []; + items.push({ + id: "NoFilter", + description: "No filter" + }); + items.push({ + id: "LocalDomain", + description: "Local domain" + }); + items.push({ + id: "AnyDomain", + description: "Any domain" + }); + items.push({ + id: "PotentialImport", + description: "Potential import" + }); + args.response.success({ + data: items + }); + } + }, domainid: { label: 'label.domain', docID: 'helpAccountDomain', diff --git a/ui/scripts/docs.js b/ui/scripts/docs.js index 4d00c835a20..7f29f2b3ac2 100755 --- a/ui/scripts/docs.js +++ b/ui/scripts/docs.js @@ -110,6 +110,11 @@ cloudStack.docs = { }, //Ldap + helpLdapUserFilter: { + desc: 'Filter to apply to listing of ldap accounts\n\t"NoFilter": no filtering is done\n\t"LocalDomain": shows only users not in the current or requested domain\n\t"AnyDomain": shows only users not currently known to cloudstack (in any domain)\n\t"PotentialImport": shows all users that (would be) automatically imported to cloudstack with their current usersource', + externalLink: '' + }, + helpLdapQueryFilter: { desc: 'Query filter is used to find a mapped user in the external LDAP server.Cloudstack provides some wildchars to represent the unique attributes in its database . Example - If Cloudstack account-name is same as the LDAP uid, then following will be a valid filter: Queryfilter : (&(uid=%u) , Queryfilter: .incase of Active Directory , Email _ID :(&(mail=%e)) , displayName :(&(displayName=%u)', @@ -127,7 +132,7 @@ cloudStack.docs = { helpIPReservationNetworkCidr: { desc: 'The CIDR of the entire network when IP reservation is configured', - externalLink: ' ' + externalLink: '' }, diff --git a/ui/scripts/ui-custom/accountsWizard.js b/ui/scripts/ui-custom/accountsWizard.js index a9328870c8c..dfaad0544aa 100644 --- a/ui/scripts/ui-custom/accountsWizard.js +++ b/ui/scripts/ui-custom/accountsWizard.js @@ -111,6 +111,14 @@ }); if (ldapStatus) { + var userFilter = $wizard.find('#label_filterBy').val(); + if (userFilter == null) { + userFilter = 'AnyDomain'; + } + var domainId = $wizard.find('#label_domain').val(); + if (domainId == null) { + domainId = $.cookie('domainid'); + } var $table = $wizard.find('.ldap-account-choice tbody'); $("#label_ldap_group_name").on("keypress", function(event) { if ($table.find("#tr-groupname-message").length === 0) { @@ -125,94 +133,13 @@ $table.find("#tr-groupname-message").hide(); } }); - $.ajax({ - url: createURL("listLdapUsers&listtype=new"), + loadList = function() { $.ajax({ + url: createURL("listLdapUsers&listtype=new&domainid=" + domainId + "&userfilter=" + userFilter), dataType: "json", async: false, success: function(json) { - //for testing only (begin) - /* - json = { - "ldapuserresponse": { - "count": 11, - "LdapUser": [ - { - "email": "test@test.com", - "principal": "CN=Administrator,CN=Users,DC=hyd-qa,DC=com", - "username": "Administrator", - "domain": "CN=Administrator" - }, - { - "email": "test@test.com", - "principal": "CN=Guest,CN=Users,DC=hyd-qa,DC=com", - "username": "Guest", - "domain": "CN=Guest" - }, - { - "email": "test@test.com", - "principal": "CN=IUSR_HYD-QA12,CN=Users,DC=hyd-qa,DC=com", - "username": "IUSR_HYD-QA12", - "domain": "CN=IUSR_HYD-QA12" - }, - { - "email": "test@test.com", - "principal": "CN=IWAM_HYD-QA12,CN=Users,DC=hyd-qa,DC=com", - "username": "IWAM_HYD-QA12", - "domain": "CN=IWAM_HYD-QA12" - }, - { - "email": "test@test.com", - "principal": "CN=SUPPORT_388945a0,CN=Users,DC=hyd-qa,DC=com", - "username": "SUPPORT_388945a0", - "domain": "CN=SUPPORT_388945a0" - }, - { - "principal": "CN=jessica j,CN=Users,DC=hyd-qa,DC=com", - "firstname": "jessica", - "lastname": "j", - "username": "jessica", - "domain": "CN=jessica j" - }, - { - "principal": "CN=krbtgt,CN=Users,DC=hyd-qa,DC=com", - "username": "krbtgt", - "domain": "CN=krbtgt" - }, - { - "email": "sadhu@sadhu.com", - "principal": "CN=sadhu,CN=Users,DC=hyd-qa,DC=com", - "firstname": "sadhu", - "username": "sadhu", - "domain": "CN=sadhu" - }, - { - "email": "test@test.com", - "principal": "CN=sangee1 hariharan,CN=Users,DC=hyd-qa,DC=com", - "firstname": "sangee1", - "lastname": "hariharan", - "username": "sangee1", - "domain": "CN=sangee1 hariharan" - }, - { - "email": "test@test.com", - "principal": "CN=sanjeev n.,CN=Users,DC=hyd-qa,DC=com", - "firstname": "sanjeev", - "username": "sanjeev", - "domain": "CN=sanjeev n." - }, - { - "email": "test@test.com", - "principal": "CN=test1dddd,CN=Users,DC=hyd-qa,DC=com", - "firstname": "test1", - "username": "test1dddd", - "domain": "CN=test1dddd" - } - ] - } - }; - */ - //for testing only (end) + $table.find('tr').remove(); if (json.ldapuserresponse.count > 0) { $(json.ldapuserresponse.LdapUser).each(function() { var $result = $(''); @@ -228,7 +155,9 @@ $('').addClass('username').html(_s(this.username)) .attr('title', this.username), $('').addClass('email').html(_s(this.email)) - .attr('title', _s(this.email)) + .attr('title', _s(this.email)), + $('').addClass('email').html(_s(this.conflictingusersource)) + .attr('title', _s(this.conflictingusersource)) ) $table.append($result); @@ -243,14 +172,20 @@ $table.append($result); } } - }); + }) }; + loadList(); + } else { + var informationWithinLdapFields = $.extend(true,{},args.informationWithinLdap); + // we are not in ldap so + delete informationWithinLdapFields.conflictingusersource; + var informationWithinLdap = cloudStack.dialog.createForm({ context: context, noDialog: true, form: { title: '', - fields: args.informationWithinLdap + fields: informationWithinLdapFields } }); @@ -267,13 +202,16 @@ $wizard.removeClass('multi-wizard'); } + var informationNotInLdap = $.extend(true,{},args.informationNotInLdap); + if (!ldapStatus) { - delete args.informationNotInLdap.ldapGroupName; + delete informationNotInLdap.filter; + delete informationNotInLdap.ldapGroupName; } if (g_idpList == null) { - delete args.informationNotInLdap.samlEnable; - delete args.informationNotInLdap.samlEntity; + delete informationNotInLdap.samlEnable; + delete informationNotInLdap.samlEntity; } var informationNotInLdap = cloudStack.dialog.createForm({ @@ -281,12 +219,21 @@ noDialog: true, form: { title: '', - fields: args.informationNotInLdap + fields: informationNotInLdap } }); var informationNotInLdapForm = informationNotInLdap.$formContainer.find('form .form-item'); + informationNotInLdapForm.find('.value #label_filterBy').addClass('required'); + informationNotInLdapForm.find('.value #label_filterBy').change(function() { + userFilter = $wizard.find('#label_filterBy').val(); + loadList(); + }); informationNotInLdapForm.find('.value #label_domain').addClass('required'); + informationNotInLdapForm.find('.value #label_domain').change(function() { + domainId = $wizard.find('#label_domain').val(); + loadList(); + }); informationNotInLdapForm.find('.value #label_type').addClass('required'); if (!ldapStatus) { informationNotInLdapForm.css('background', 'none');