Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ protected Set<AuthenticationConfiguration<?>> createCollection()

private AuthenticationConfigurationCollections()
{
boolean acceptOnlyFicamProviders = AuthenticationManager.isAcceptOnlyFicamProviders();

// Select the configurations stored in the core.AuthenticationConfigurations table, add the database
// authentication configuration, map each to the appropriate AuthenticationConfiguration, and add to the maps.

Expand All @@ -99,29 +97,32 @@ private AuthenticationConfigurationCollections()
configs
.map(this::getAuthenticationConfiguration)
.filter(Objects::nonNull)
.filter(c->!acceptOnlyFicamProviders || c.getAuthenticationProvider().isFicamApproved())
.forEach(this::addConfiguration);

// MultiValuedMap of domains to AuthenticationConfigurations that claim them
_activeDomainMap = getActive(PrimaryAuthenticationConfiguration.class).stream()
.filter(config -> null != config.getDomain())
.filter(config -> !AuthenticationManager.ALL_DOMAINS.equals(config.getDomain()))
.map(config -> (AuthenticationConfiguration<?>) config)
.collect(LabKeyCollectors.toMultiValuedMap(AuthenticationConfiguration::getDomain, config -> config));

List<String> activeDomains = new ArrayList<>(_activeDomainMap.keySet());
Collections.sort(activeDomains);
_activeDomains = Collections.unmodifiableCollection(activeDomains);
}

// Little helper method simplifies the stream handling above
// Little helper method simplifies the stream handling above. Filters out configurations based on FICAM-only setting.
private @Nullable AuthenticationConfiguration<?> getAuthenticationConfiguration(Map<String, Object> map)
{
String providerName = (String)map.get("Provider");
AuthenticationProvider provider = AuthenticationProviderCache.getProvider(AuthenticationProvider.class, providerName);
if (null == provider)
{
String description = (String)map.get("Description");
LOG.warn("A saved authentication configuration requires the \"{}\" authentication provider, but that provider is not present in this deployment. Authentication via {} will not be available.", providerName, null != description ? "\"" + description + "\"" : "this mechanism");
if (!AuthenticationManager.isAcceptOnlyFicamProviders()) // Don't warn if FICAM-only is checked
{
String description = (String)map.get("Description");
LOG.warn("A saved authentication configuration requires the \"{}\" authentication provider, but that provider is not present in this deployment. Authentication via {} will not be available.", providerName, null != description ? "\"" + description + "\"" : "this mechanism");
}
return null;
}

Expand Down Expand Up @@ -162,7 +163,7 @@ private void addToMap(SetValuedMap<Class<? extends AuthenticationConfiguration>,
return null != configurations ? configurations : Collections.emptyList();
}

private @NotNull Collection<AuthenticationConfiguration> getActiveConfigurationsForDomain(String domain)
private @NotNull Collection<AuthenticationConfiguration<?>> getActiveConfigurationsForDomain(String domain)
{
return new ArrayList<>(_activeDomainMap.get(domain));
}
Expand Down Expand Up @@ -233,7 +234,7 @@ public static void clear()
/**
* Return a collection of authentication configurations that claim the specified domain
*/
public static @NotNull Collection<AuthenticationConfiguration> getActiveConfigurationsForDomain(String domain)
public static @NotNull Collection<AuthenticationConfiguration<?>> getActiveConfigurationsForDomain(String domain)
{
return CACHE.get(CACHE_KEY).getActiveConfigurationsForDomain(domain);
}
Expand Down
7 changes: 7 additions & 0 deletions api/src/org/labkey/api/security/AuthenticationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,12 @@ public static void deleteConfiguration(User user, int rowId)
{
// Delete any logos attached to the configuration
AuthenticationConfiguration<?> configuration = AuthenticationConfigurationCache.getConfiguration(AuthenticationConfiguration.class, rowId);

if (null == configuration)
{
throw new NotFoundException("Unable to delete authentication configuration");
}

AttachmentService.get().deleteAttachments(configuration);

// Delete configuration
Expand Down Expand Up @@ -746,6 +752,7 @@ public static void setAcceptOnlyFicamProviders(User user, boolean enable)
if (isAcceptOnlyFicamProviders() != enable)
{
saveAuthSetting(user, ACCEPT_ONLY_FICAM_PROVIDERS_KEY, enable);
AuthenticationProviderCache.clear();
AuthenticationConfigurationCache.clear();
}
}
Expand Down
18 changes: 18 additions & 0 deletions api/src/org/labkey/api/security/AuthenticationProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ interface ResetPasswordProvider extends AuthenticationProvider
* @param isAdminCopy true for sending admin a copy of reset password email
*/
@Nullable SecurityMessage getAPIResetPasswordMessage(User user, boolean isAdminCopy);

@Override
default boolean isFicamApproved()
{
return true;
}
}

interface SecondaryAuthenticationProvider<SAC extends SecondaryAuthenticationConfiguration<?>> extends ConfigurableAuthenticationProvider<SAC>
Expand Down Expand Up @@ -286,11 +292,23 @@ interface DisableLoginProvider extends AuthenticationProvider
void addUserDelay(HttpServletRequest request, String id, int addCount);

void resetUserDelay(String id);

@Override
default boolean isFicamApproved()
{
return true;
}
}

interface ExpireAccountProvider extends AuthenticationProvider
{
boolean isEnabled();

@Override
default boolean isFicamApproved()
{
return true;
}
}

class AuthenticationResponse
Expand Down
15 changes: 7 additions & 8 deletions api/src/org/labkey/api/security/AuthenticationProviderCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@
import java.util.LinkedHashSet;
import java.util.Set;

/**
* Created by adam on 5/20/2016.
*/
public class AuthenticationProviderCache
{
// We have just a single object to cache (a global AuthenticationProviderCollection), but use standard cache (blocking cache wrapping the
Expand All @@ -56,13 +53,15 @@ protected Set<AuthenticationProvider> createCollection()

private AuthenticationProviderCollections()
{
for (AuthenticationProvider provider : AuthenticationManager.getAllProviders())
{
AuthenticationProvider.ALL_PROVIDER_INTERFACES
boolean acceptOnlyFicamProviders = AuthenticationManager.isAcceptOnlyFicamProviders();

AuthenticationManager.getAllProviders().stream()
.filter(provider -> !acceptOnlyFicamProviders || provider.isFicamApproved())
.forEach(provider -> AuthenticationProvider.ALL_PROVIDER_INTERFACES
.stream()
.filter(providerClass -> providerClass.isInstance(provider))
.forEach(providerClass -> _map.put(providerClass, provider));
}
.forEach(providerClass -> _map.put(providerClass, provider))
);
}

private <T extends AuthenticationProvider> Collection<T> get(Class<T> clazz)
Expand Down
16 changes: 14 additions & 2 deletions api/src/org/labkey/api/security/SaveConfigurationAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ public static <F extends SaveConfigurationForm> AuthenticationConfiguration<?> s
return StringUtilsLabKey.getMapDifference(
null != oldConfiguration ? oldConfiguration.getLoggingProperties() : null,
null != newConfiguration ? newConfiguration.getLoggingProperties() : null,
50);
50
);
}

protected AC getFromCache(int rowId)
Expand All @@ -136,9 +137,20 @@ protected AC getFromCache(int rowId)
return (AC)AuthenticationConfigurationCache.getConfiguration(AuthenticationConfiguration.class, rowId);
}

protected Map<String, Object> getConfigurationMap(int rowId)
protected final Map<String, Object> getConfigurationMap(int rowId)
{
AC configuration = getFromCache(rowId);

if (null == configuration)
{
throw new NotFoundException("Unable to save configuration");
}

return getConfigurationMap(configuration);
}

protected Map<String, Object> getConfigurationMap(@NotNull AC configuration)
{
return AuthenticationManager.getConfigurationMap(configuration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,8 @@ public static void logLogoAction(User user, SSOAuthenticationConfiguration<?> co
}

@Override
protected Map<String, Object> getConfigurationMap(int rowId)
protected Map<String, Object> getConfigurationMap(@NotNull AC configuration)
{
AC configuration = getFromCache(rowId);
return AuthenticationManager.getSsoConfigurationMap(configuration);
}

Expand Down
17 changes: 13 additions & 4 deletions core/src/client/components/AuthRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export default class AuthRow extends PureComponent<Props, Partial<State>> {
}));
};

closeDeleteModal = (): void => {
this.onToggleModal('deleteModalOpen', this.state.deleteModalOpen);
this.props.toggleModalOpen(false);
};

render() {
const {
authConfig,
Expand Down Expand Up @@ -141,11 +146,15 @@ export default class AuthRow extends PureComponent<Props, Partial<State>> {
<Modal
confirmText="Yes, delete"
confirmClass="labkey-button primary"
onCancel={() => {
this.onToggleModal('deleteModalOpen', this.state.deleteModalOpen);
toggleModalOpen(false);
onCancel={this.closeDeleteModal}
onConfirm={() => {
// Close the confirmation modal as soon as the user confirms, regardless of whether the
// delete succeeds. On success the row unmounts anyway; on failure (e.g. the configuration
// was already removed, or FICAM-only mode was enabled in another tab and the provider is no
// longer available) the error is surfaced in the main window and the modal should not linger.
this.closeDeleteModal();
onDelete?.();
}}
onConfirm={onDelete}
title={`Permanently delete ${authConfig.provider} configuration?`}
>
<div className="auth-row__delete-modal__textBox">
Expand Down
8 changes: 7 additions & 1 deletion core/src/org/labkey/core/login/LoginController.java
Original file line number Diff line number Diff line change
Expand Up @@ -1652,7 +1652,11 @@ public Object execute(ReturnUrlForm form, BindException errors)
PrimaryAuthenticationConfiguration<?> configuration = AuthenticationManager.getConfiguration(getViewContext().getSession());
if (configuration == null)
{
throw new NotFoundException("No configuration found");
throw new NotFoundException("Configuration was not found" + CONFIGURATION_ADVICE);
}
if (!configuration.isEnabled())
{
throw new NotFoundException("Configuration is no longer active" + CONFIGURATION_ADVICE);
}
JSONObject resp = new JSONObject();
resp.put("description", configuration.getDescription());
Expand All @@ -1665,6 +1669,8 @@ public Object execute(ReturnUrlForm form, BindException errors)
}
}

private static final String CONFIGURATION_ADVICE = ". Please Sign Out and Sign In again.";

public static final String PASSWORD1_TEXT_FIELD_NAME = "password";
public static final String PASSWORD2_TEXT_FIELD_NAME = "password2";

Expand Down
14 changes: 9 additions & 5 deletions devtools/src/org/labkey/devtools/view/testReauth.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,21 @@
success: function(response) {
const needReauth = <%=form.reauthToken() == null%>;
const data = JSON.parse(response.responseText).data;
document.getElementById("description").textContent = data.description;
document.getElementById("description").textContent = data.description; // Setting textContent HTML encodes the value
if (needReauth) {
document.getElementById("link").href = data.reauthUrl;
}
},
failure: function() {
alert('Failed to retrieve configuration!');
}
failure: LABKEY.Utils.getCallbackWrapper(function(errorInfo) {
document.getElementById("content").innerHTML = '<span>' + LABKEY.Utils.encodeHtml(errorInfo.exception ?? 'Failed to retrieve configuration') + '</span>';
}, this, true)
});
});
</script>

You authenticated with: <span id="description"></span><br/>
<div id="content">

You authenticated with: <span id="description"></span><br/>

<%
if (form.reauthToken() != null)
Expand Down Expand Up @@ -79,3 +81,5 @@ You need to re-authenticate.
<%
}
%>

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-2026 LabKey Corporation
*
* Licensed 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.labkey.test.pages.devtools;

import org.junit.Assert;
import org.labkey.test.Locator;
import org.labkey.test.WebDriverWrapper;
import org.labkey.test.WebTestHelper;
import org.labkey.test.components.html.RadioButton;
import org.labkey.test.pages.LabKeyPage;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

/**
* Wraps 'testSecondary.jsp'
*/
public class TestSecondaryPage extends LabKeyPage<TestSecondaryPage.ElementCache>
{
public TestSecondaryPage(WebDriver driver)
{
super(driver);
}

public void denyIdentity()
{
elementCache().noButton.check();
clickAndWait(elementCache().submitButton);
clearCache(); // Stays on same page
}

public void confirmIdentity()
{
elementCache().yesButton.check();
clickAndWait(elementCache().submitButton);
}

@Override
protected ElementCache newElementCache()
{
return new ElementCache();
}

protected class ElementCache extends LabKeyPage<ElementCache>.ElementCache
{
final RadioButton yesButton = RadioButton.RadioButton(Locator.radioButtonByNameAndValue("valid", "1")).findWhenNeeded(this);
final RadioButton noButton = RadioButton.RadioButton(Locator.radioButtonByNameAndValue("valid", "0")).findWhenNeeded(this);
final WebElement submitButton = Locator.name("TestSecondary").findWhenNeeded(this);
}
}
Loading