-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Cloudstack:8647 LDAP Trust AD and Autoimport #755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0680648
e3ddde8
7109689
0dc9ccd
5929186
2825c07
6cb5d3a
dd6d6d1
6572abc
36340d9
c2b36cb
1c836a8
ca8b375
26fea34
6177bae
53a441f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| 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 |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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()); | ||
|
|
@@ -33,6 +37,8 @@ public class LdapAuthenticator extends DefaultUserAuthenticator { | |
| private LdapManager _ldapManager; | ||
| @Inject | ||
| private UserAccountDao _userAccountDao; | ||
| @Inject | ||
| private AccountManager _accountManager; | ||
|
|
||
| public LdapAuthenticator() { | ||
| super(); | ||
|
|
@@ -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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't 'user' be assigned the output of this call?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. user isnt required after this.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. authentication wont be successful. authentication will fail as createuseraccount will throw exception.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.