Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
import org.apache.cloudstack.framework.security.keystore.KeystoreVO;
import org.apache.cloudstack.resourcedetail.UserDetailVO;
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
import org.apache.cloudstack.utils.security.CertUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
Expand Down Expand Up @@ -92,6 +94,10 @@
@Component
public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManager, Configurable {

/** Remembers the user's Source (e.g. LDAP) from before SAML was authorized, so disabling
* SAML can fall back to it instead of always defaulting to {@link User.Source#UNKNOWN}. */
private static final String PRE_SAML_SOURCE_DETAIL_KEY = "PreSamlSource";

private SAMLProviderMetadata _spMetadata = new SAMLProviderMetadata();
private Map<String, SAMLProviderMetadata> _idpMetadataMap = new HashMap<String, SAMLProviderMetadata>();

Expand All @@ -115,7 +121,10 @@
@Inject
private UserDao _userDao;

@Inject
private UserDetailsDao userDetailsDao;

@Inject

Check warning on line 127 in plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this field injection and use constructor injection instead.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaArJ31krd49U0dPV8fp&open=AaArJ31krd49U0dPV8fp&pullRequest=13953
DomainManager _domainMgr;

@Override
Expand Down Expand Up @@ -446,26 +455,52 @@
@Override
public boolean authorizeUser(Long userId, String entityId, boolean enable) {
UserVO user = _userDao.getUser(userId);
if (user != null) {
if (enable) {
user.setExternalEntity(entityId);
user.setSource(User.Source.SAML2);
} else {
boolean enableLoginAfterSAMLDisable = SAML2AuthManager.EnableLoginAfterSAMLDisable.value();
if (user.getSource().equals(User.Source.SAML2)) {
if(enableLoginAfterSAMLDisable) {
user.setSource(User.Source.UNKNOWN);
} else {
user.setSource(User.Source.SAML2DISABLED);
}
} else {
return false;
}
if (user == null) {
return false;
}
if (enable) {
enableSAMLForUser(user, entityId);
} else if (!disableSAMLForUser(user)) {
return false;
}
_userDao.update(user.getId(), user);
return true;
}

private void enableSAMLForUser(UserVO user, String entityId) {
if (user.getSource() != null && !User.Source.SAML2.equals(user.getSource()) && !User.Source.SAML2DISABLED.equals(user.getSource())) {
userDetailsDao.addDetail(user.getId(), PRE_SAML_SOURCE_DETAIL_KEY, user.getSource().toString(), false);
}
user.setExternalEntity(entityId);
user.setSource(User.Source.SAML2);
}

private boolean disableSAMLForUser(UserVO user) {
if (!user.getSource().equals(User.Source.SAML2)) {
return false;
}
if (SAML2AuthManager.EnableLoginAfterSAMLDisable.value()) {

Check warning on line 482 in plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a primitive boolean expression here.

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaA4nZlvUxr6NS6VP-Se&open=AaA4nZlvUxr6NS6VP-Se&pullRequest=13953
user.setSource(getPreSamlSource(user.getId()));
} else {
user.setSource(User.Source.SAML2DISABLED);
}
return true;
}

/**
* The Source (e.g. LDAP) the user had before SAML was authorized for them, so disabling
* SAML can restore it instead of always falling back to {@link User.Source#UNKNOWN}.
*/
private User.Source getPreSamlSource(long userId) {
UserDetailVO preSamlSource = userDetailsDao.findDetail(userId, PRE_SAML_SOURCE_DETAIL_KEY);
if (preSamlSource != null) {
try {
return User.Source.valueOf(preSamlSource.getValue());
} catch (IllegalArgumentException e) {
logger.warn("Unrecognized pre-SAML source '{}' stored for user {}; falling back to UNKNOWN", preSamlSource.getValue(), userId);
}
_userDao.update(user.getId(), user);
return true;
}
return false;
return User.Source.UNKNOWN;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@

package org.apache.cloudstack;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.apache.cloudstack.framework.security.keystore.KeystoreDao;
import org.apache.cloudstack.resourcedetail.UserDetailVO;
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
import org.apache.cloudstack.saml.SAML2AuthManagerImpl;
import org.apache.cloudstack.saml.SAMLTokenDao;
import org.apache.cloudstack.saml.SAMLTokenVO;
Expand Down Expand Up @@ -50,6 +57,9 @@ public class SAML2AuthManagerImplTest extends TestCase {
@Mock
private UserDao userDao;

@Mock
private UserDetailsDao userDetailsDao;

@Mock
DomainManager domainMgr;

Expand All @@ -72,6 +82,10 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException {
userDaoField.setAccessible(true);
userDaoField.set(saml2AuthManager, userDao);

Field userDetailsDaoField = SAML2AuthManagerImpl.class.getDeclaredField("userDetailsDao");
userDetailsDaoField.setAccessible(true);
userDetailsDaoField.set(saml2AuthManager, userDetailsDao);

Field domainMgrField = SAML2AuthManagerImpl.class.getDeclaredField("_domainMgr");
domainMgrField.setAccessible(true);
domainMgrField.set(saml2AuthManager, domainMgr);
Expand Down Expand Up @@ -117,7 +131,57 @@ public void testAuthorizeUser() {
Mockito.verify(userDao, Mockito.atLeastOnce()).update(Mockito.anyLong(), Mockito.any(user.getClass()));
}

@Test
public void testAuthorizeUserStoresPreSamlSourceOnEnable() {
UserVO user = new UserVO(200L);
user.setUsername("someuser");
user.setSource(User.Source.LDAP);
when(userDao.getUser(Mockito.anyLong())).thenReturn(user);

saml2AuthManager.authorizeUser(200L, "someID", true);

verify(userDetailsDao).addDetail(200L, "PreSamlSource", "LDAP", false);
assertEquals(User.Source.SAML2, user.getSource());
}

@Test
public void testAuthorizeUserDoesNotRestorePreSamlSourceWhenAlreadyAuthorized() {
UserVO user = new UserVO(200L);
user.setUsername("someuser");
user.setSource(User.Source.SAML2);
when(userDao.getUser(Mockito.anyLong())).thenReturn(user);

saml2AuthManager.authorizeUser(200L, "someID", true);

verify(userDetailsDao, never()).addDetail(Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyBoolean());
}

@Test
public void testGetPreSamlSourceRestoresStoredSource() throws Exception {
when(userDetailsDao.findDetail(200L, "PreSamlSource")).thenReturn(new UserDetailVO(200L, "PreSamlSource", "LDAP"));

assertEquals(User.Source.LDAP, invokeGetPreSamlSource(200L));
}

@Test
public void testGetPreSamlSourceDefaultsToUnknownWhenNothingStored() throws Exception {
when(userDetailsDao.findDetail(200L, "PreSamlSource")).thenReturn(null);

assertEquals(User.Source.UNKNOWN, invokeGetPreSamlSource(200L));
}

@Test
public void testGetPreSamlSourceDefaultsToUnknownOnGarbageValue() throws Exception {
when(userDetailsDao.findDetail(200L, "PreSamlSource")).thenReturn(new UserDetailVO(200L, "PreSamlSource", "not-a-real-source"));

assertEquals(User.Source.UNKNOWN, invokeGetPreSamlSource(200L));
}

private User.Source invokeGetPreSamlSource(long userId) throws Exception {
Method method = SAML2AuthManagerImpl.class.getDeclaredMethod("getPreSamlSource", long.class);
method.setAccessible(true);
return (User.Source) method.invoke(saml2AuthManager, userId);
}

@Test
public void testSaveToken() {
Expand Down
Loading