Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/org/apache/cloudstack/api/ApiConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,8 @@ public class ApiConstants {
public static final String OVM3_CLUSTER = "ovm3cluster";
public static final String OVM3_VIP = "ovm3vip";

public static final String ADMIN = "admin";

public enum HostDetails {
all, capacity, events, stats, min;
}
Expand Down
4 changes: 4 additions & 0 deletions client/WEB-INF/classes/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2133,6 +2133,10 @@ label.every=Every
label.day=Day
label.of.month=of month
label.add.private.gateway=Add Private Gateway
label.link.domain.to.ldap=Link Domain to LDAP
message.link.domain.to.ldap=Enable autosync for this domain in LDAP
label.ldap.link.type=Type
label.account.type=Account Type
message.desc.created.ssh.key.pair=Created a SSH Key Pair.
message.please.confirm.remove.ssh.key.pair=Please confirm that you want to remove this SSH Key Pair
message.password.has.been.reset.to=Password has been reset to
Expand Down
1 change: 1 addition & 0 deletions client/tomcatconf/commands.properties.in
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,7 @@ deleteLdapConfiguration=3
listLdapUsers=3
ldapCreateAccount=3
importLdapUsers=3
linkDomainToLdap=3


#### juniper-contrail commands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@
<bean id="LdapConfigurationDao"
class="org.apache.cloudstack.ldap.dao.LdapConfigurationDaoImpl" />
<bean id="LdapConfiguration" class="org.apache.cloudstack.ldap.LdapConfiguration" />
<bean id="LdapTrustMapDao" class="org.apache.cloudstack.ldap.dao.LdapTrustMapDaoImpl" />

</beans>
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* 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 javax.inject.Inject;

import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.LinkDomainToLdapResponse;
import org.apache.cloudstack.ldap.LdapManager;
import org.apache.cloudstack.ldap.LdapUser;
import org.apache.cloudstack.ldap.NoLdapUserMatchingQueryException;
import org.apache.log4j.Logger;

import com.cloud.user.Account;

import java.util.UUID;

@APICommand(name = "linkDomainToLdap", description = "link an existing cloudstack domain to group or OU in ldap", responseObject = LinkDomainToLdapResponse.class, since = "4.6.0",
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class LinkDomainToLdapCmd extends BaseCmd {
public static final Logger s_logger = Logger.getLogger(LinkDomainToLdapCmd.class.getName());
private static final String s_name = "linkdomaintoldapresponse";

@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "The id of the domain which has to be "
+ "linked to LDAP.")
private Long domainId;

@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING, required = true, description = "type of the ldap name. GROUP or OU")
private String type;

@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "name of the group or OU in LDAP")
private String name;

@Parameter(name = ApiConstants.ADMIN, type = CommandType.STRING, required = false, description = "domain admin username in LDAP ")
private String admin;

@Parameter(name = ApiConstants.ACCOUNT_TYPE, type = CommandType.SHORT, required = true, description = "Type of the account to auto import. Specify 0 for user and 2 for " +
"domain admin")
private short accountType;

@Inject
private LdapManager _ldapManager;

@Override
public void execute() throws ServerApiException {
try {
LinkDomainToLdapResponse response = _ldapManager.linkDomainToLdap(domainId, type, name, accountType);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linkDomainToAdmin() and createUserAccount() involves DB updates. Shouldn't it be in a tx? What if the first call succeeds and the second call fails?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is ok for the first call to succeed and the second to fail. for example if the username already exists, it will fail. I will check if tx is required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if user doesn't exist but still createUserAccount() fails?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createUserAccount can fail and that doesnt mean api failure. When it is successful, the new id is returned in response. check fs for sample request response in different scenarios.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The api semantics doesn't seem right here. It is ok for createUserAccount to fail if the user exists but in that case the response should have the UUID of the existing account. Also if the user doesn't exist but still the call fails then the entire API should fail, a tx would help in this case. The semantics should be if optional admin user parameter is specified then the admin UUID should always be present in the response in case of success otherwise the API call should return failure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will see if I can change that later. For now, it will as documented in FS which is, it returns created account id when the account can be created. Otherwise, the information on why it failed to create the account will be in logs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the semantic aspect of the API will be revisited as part of CLOUDSTACK-8796, for now the changes are ok.

if(admin!=null) {
LdapUser ldapUser = null;
try {
ldapUser = _ldapManager.getUser(admin, type, name);
} catch (NoLdapUserMatchingQueryException e) {
s_logger.debug("no ldap user matching username " + admin + " in the given group/ou", e);
}
if (ldapUser != null && !ldapUser.isDisabled()) {
Account account = _accountService.getActiveAccountByName(admin, domainId);
if (account == null) {
try {
UserAccount userAccount = _accountService.createUserAccount(admin, "", ldapUser.getFirstname(), ldapUser.getLastname(), ldapUser.getEmail(), null,
admin, Account.ACCOUNT_TYPE_DOMAIN_ADMIN, domainId, admin, null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), User.Source.LDAP);
response.setAdminId(String.valueOf(userAccount.getAccountId()));
s_logger.info("created an account with name " + admin + " in the given domain " + domainId);
} catch (Exception e) {
s_logger.info("an exception occurred while creating account with name " + admin +" in domain " + domainId, e);
}
} else {
s_logger.debug("an account with name " + admin + " already exists in the domain " + domainId);
}
} else {
s_logger.debug("ldap user with username "+admin+" is disabled in the given group/ou");
}
}
response.setObjectName("LinkDomainToLdap");
response.setResponseName(getCommandName());
setResponseObject(response);
} catch (final InvalidParameterValueException e) {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.toString());
}
}

@Override
public String getCommandName() {
return s_name;
}

@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.response;

import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;

public class LinkDomainToLdapResponse extends BaseResponse {

@SerializedName(ApiConstants.DOMAIN_ID)
@Param(description = "id of the Domain which is linked to LDAP")
private long domainId;

@SerializedName(ApiConstants.NAME)
@Param(description = "name of the group or OU in LDAP which is linked to the domain")
private String name;

@SerializedName(ApiConstants.TYPE)
@Param(description = "type of the name in LDAP which is linke to the domain")
private String type;

@SerializedName(ApiConstants.ACCOUNT_TYPE)
@Param(description = "Type of the account to auto import")
private short accountType;

@SerializedName(ApiConstants.ACCOUNT_ID)
@Param(description = "Domain Admin accountId that is created")
private String adminId;

public LinkDomainToLdapResponse(long domainId, String type, String name, short accountType) {
this.domainId = domainId;
this.name = name;
this.type = type;
this.accountType = accountType;
}

public long getDomainId() {
return domainId;
}

public String getName() {
return name;
}

public String getType() {
return type;
}

public short getAccountType() {
return accountType;
}

public String getAdminId() {
return adminId;
}

public void setAdminId(String adminId) {
this.adminId = adminId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@

public class ADLdapUserManagerImpl extends OpenLdapUserManagerImpl implements LdapUserManager {
public static final Logger s_logger = Logger.getLogger(ADLdapUserManagerImpl.class.getName());
private static final String MICROSOFT_AD_NESTED_MEMBERS_FILTER = "memberOf:1.2.840.113556.1.4.1941";
private static final String MICROSOFT_AD_NESTED_MEMBERS_FILTER = "memberOf:1.2.840.113556.1.4.1941:";
private static final String MICROSOFT_AD_MEMBERS_FILTER = "memberOf";

@Override
public List<LdapUser> getUsersInGroup(String groupName, LdapContext context) throws NamingException {
Expand Down Expand Up @@ -66,7 +67,7 @@ private String generateADGroupSearchFilter(String groupName) {

final StringBuilder memberOfFilter = new StringBuilder();
String groupCnName = _ldapConfiguration.getCommonNameAttribute() + "=" +groupName + "," + _ldapConfiguration.getBaseDn();
memberOfFilter.append("(" + MICROSOFT_AD_NESTED_MEMBERS_FILTER + ":=");
memberOfFilter.append("(").append(getMemberOfAttribute()).append("=");
memberOfFilter.append(groupCnName);
memberOfFilter.append(")");

Expand All @@ -79,4 +80,25 @@ private String generateADGroupSearchFilter(String groupName) {
s_logger.debug("group search filter = " + result);
return result.toString();
}

protected boolean isUserDisabled(SearchResult result) throws NamingException {
boolean isDisabledUser = false;
String userAccountControl = LdapUtils.getAttributeValue(result.getAttributes(), _ldapConfiguration.getUserAccountControlAttribute());
if (userAccountControl != null) {
int control = Integer.valueOf(userAccountControl);
// second bit represents disabled user flag in AD
if ((control & 2) > 0) {
isDisabledUser = true;
}
}
return isDisabledUser;
}

protected String getMemberOfAttribute() {
if(_ldapConfiguration.isNestedGroupsEnabled()) {
return MICROSOFT_AD_NESTED_MEMBERS_FILTER;
} else {
return MICROSOFT_AD_MEMBERS_FILTER;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package org.apache.cloudstack.ldap;

import com.cloud.server.auth.DefaultUserAuthenticator;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.utils.Pair;
Expand All @@ -25,6 +28,7 @@

import javax.inject.Inject;
import java.util.Map;
import java.util.UUID;

public class LdapAuthenticator extends DefaultUserAuthenticator {
private static final Logger s_logger = Logger.getLogger(LdapAuthenticator.class.getName());
Expand All @@ -33,6 +37,8 @@ public class LdapAuthenticator extends DefaultUserAuthenticator {
private LdapManager _ldapManager;
@Inject
private UserAccountDao _userAccountDao;
@Inject
private AccountManager _accountManager;

public LdapAuthenticator() {
super();
Expand All @@ -52,21 +58,71 @@ public Pair<Boolean, ActionOnFailedAuthentication> authenticate(final String use
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
}

final UserAccount user = _userAccountDao.getUserAccount(username, domainId);
boolean result = false;
ActionOnFailedAuthentication action = null;

if (user == null) {
s_logger.debug("Unable to find user with " + username + " in domain " + domainId);
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
} else if (_ldapManager.isLdapEnabled()) {
boolean result = _ldapManager.canAuthenticate(username, password);
ActionOnFailedAuthentication action = null;
if (result == false) {
if (_ldapManager.isLdapEnabled()) {
final UserAccount user = _userAccountDao.getUserAccount(username, domainId);
LdapTrustMapVO ldapTrustMapVO = _ldapManager.getDomainLinkedToLdap(domainId);
if(ldapTrustMapVO != null) {
try {
LdapUser ldapUser = _ldapManager.getUser(username, ldapTrustMapVO.getType().toString(), ldapTrustMapVO.getName());
if(!ldapUser.isDisabled()) {
result = _ldapManager.canAuthenticate(ldapUser.getPrincipal(), password);
if(result) {
if(user == null) {
// import user to cloudstack
createCloudStackUserAccount(ldapUser, domainId, ldapTrustMapVO.getAccountType());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't 'user' be assigned the output of this call?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user isnt required after this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if more than one thread simultaneously calls authenticate for same user for the first time (i.e. both see user is not created)? Will one of the call to createCSUserAccount() fail? Ideally first call should create the account and 2nd should reuse.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second call to createUserAccount will fail and authentication will fail in the second attempt.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case as well, the authn is successful but user sees failure as account creation has failed. The behaviour is not intuitive. If it is difficult to address it in implementation then should be documented as limitation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case as well, the authn is successful but user sees failure as account creation has failed. The behaviour is not intuitive. If it is difficult to address it in implementation then should be documented as limitation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authentication wont be successful. authentication will fail as createuseraccount will throw exception.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By authn I mean the call to LdapManager.canAuthenticate(). This will be successful in both cases but CS will report authn failure in once case due to account creation failure which is not intuitive. Account creation failure will be passed as authn failure.
But this will be a corner case scenario.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please open a tracking bug to see if this can be improved to make the outcome more intuitive.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no bug here. Its how the current authenticators work. the api output will be improved based on the outcome of CLOUDSTACK-8796

} else {
enableUserInCloudStack(user);
}
}
} else {
//disable user in cloudstack
disableUserInCloudStack(user);
}
} catch (NoLdapUserMatchingQueryException e) {
s_logger.debug(e.getMessage());
}

} else {
//domain is not linked to ldap follow normal authentication
if(user != null ) {
try {
LdapUser ldapUser = _ldapManager.getUser(username);
if(!ldapUser.isDisabled()) {
result = _ldapManager.canAuthenticate(ldapUser.getPrincipal(), password);
} else {
s_logger.debug("user with principal "+ ldapUser.getPrincipal() + " is disabled in ldap");
}
} catch (NoLdapUserMatchingQueryException e) {
s_logger.debug(e.getMessage());
}
}
}
if (!result && user != null) {
action = ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT;
}
return new Pair<Boolean, ActionOnFailedAuthentication>(result, action);
}

return new Pair<Boolean, ActionOnFailedAuthentication>(result, action);
}

private void enableUserInCloudStack(UserAccount user) {
if(user != null && (user.getState().equalsIgnoreCase(Account.State.disabled.toString()))) {
_accountManager.enableUser(user.getId());
}
}

private void createCloudStackUserAccount(LdapUser user, long domainId, short accountType) {
String username = user.getUsername();
_accountManager.createUserAccount(username, "", user.getFirstname(), user.getLastname(), user.getEmail(), null, username, accountType, domainId, username, null,
UUID.randomUUID().toString(), UUID.randomUUID().toString(), User.Source.LDAP);
}

} else {
return new Pair<Boolean, ActionOnFailedAuthentication>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT);
private void disableUserInCloudStack(UserAccount user) {
if (user != null) {
_accountManager.disableUser(user.getId());
}
}

Expand Down
Loading