package org.carlspring.strongbox.users.security;
import org.carlspring.strongbox.configuration.ConfigurationException;
import org.carlspring.strongbox.resource.ConfigurationResourceResolver;
import org.carlspring.strongbox.security.Role;
import org.carlspring.strongbox.users.domain.Roles;
import org.carlspring.strongbox.users.service.AuthorizationConfigService;
import org.carlspring.strongbox.xml.parsers.GenericParser;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.validation.constraints.NotNull;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import com.google.common.collect.Sets;
import com.orientechnologies.orient.core.entity.OEntityManager;
import com.orientechnologies.orient.core.exception.OSerializationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Responsible for load, validate and save to the persistent storage {@link AuthorizationConfig} from configuration
* sources.
*
* @author Alex Oreshkevich
* @see {@linkplain=https://dev.carlspring.org/youtrack/issue/SB-126}
*/
@Component
public class AuthorizationConfigProvider
{
private static final Logger logger = LoggerFactory.getLogger(AuthorizationConfigProvider.class);
@Inject
private AuthorizationConfigService configService;
private GenericParser<AuthorizationConfig> parser;
@Inject
private OEntityManager oEntityManager;
private AuthorizationConfig config;
@Inject
private TransactionTemplate transactionTemplate;
private static void throwIfNotEmpty(Sets.SetView<String> intersectionView,
String message)
{
if (!intersectionView.isEmpty())
{
throw new ConfigurationException(message + intersectionView);
}
}
private static Set<String> collect(@NotNull Iterable<?> it,
@NotNull NameFunction nameFunction)
{
Set<String> names = new HashSet<>();
for (Object o : it)
{
names.add(nameFunction.name(o));
}
return names;
}
@PostConstruct
public void init()
throws IOException,
JAXBException
{
// update schema in any case
registerEntities();
// check database for any configuration source, if something is already in place
// reuse it and skip reading configuration from XML
transactionTemplate.execute((s) -> {
try
{
doInit();
}
catch (Exception e)
{
throw new BeanInitializationException(String.format("Failed to initialize: msg-[%s]", e.getMessage()),
e);
}
return null;
});
}
protected void doInit()
throws IOException,
JAXBException
{
long configCount = configService.count();
if (configCount > 0)
{
logger.debug("Reuse existing authorization config from database...");
try
{
// get first of the available configs into work
configService.findAll()
.ifPresent(
authorizationConfigs -> config = authorizationConfigs.get(0));
// process the case when for some reason we have more than one config
if (configCount > 1)
{
logger.warn("Taking first of the total of " + configCount + " authorization configs...");
}
}
catch (OSerializationException e)
{
config = null;
logger.error("Unable to reuse existing authorization config", e);
throw new BeanInitializationException("Unable to reuse existing authorization config", e);
}
}
if (config == null)
{
logger.debug("Load authorization config from XML file...");
parser = new GenericParser<>(AuthorizationConfig.class);
config = parser.parse(getConfigurationResource().getURL());
// when Configuration retrieved from XML file validation process appears
// if we found in XML file privilege or role that already defined as build-in
// (based on role/privilege name) we will throw runtime exception
validateConfig(config);
}
saveConfig();
}
@Transactional
public synchronized void saveConfig()
{
configService.deleteAll();
config.setObjectId(null);
try
{
configService.save(config);
}
catch (Exception e)
{
logger.error("Unable to save configuration: ", e);
}
}
private synchronized void registerEntities()
{
// full class names used for clarity and to avoid conflicts with domain package
// that contains the same class names
oEntityManager.registerEntityClass(AuthorizationConfig.class);
oEntityManager.registerEntityClass(org.carlspring.strongbox.security.Roles.class);
oEntityManager.registerEntityClass(Role.class);
}
private void validateConfig(@NotNull AuthorizationConfig config)
throws ConfigurationException
{
// check that embedded roles was not overridden
throwIfNotEmpty(toIntersection(config.getRoles()
.getRoles(),
Arrays.asList(Roles.values()),
o -> ((Role) o).getName()
.toUpperCase(),
o -> ((Roles) o).name()
.toUpperCase()),
"Embedded roles overriding is forbidden: ");
}
/**
* Calculates intersection of two sets that was created from two iterable sources with help of two name functions
* respectively.
*/
private Sets.SetView<String> toIntersection(@NotNull Iterable<?> first,
@NotNull Iterable<?> second,
@NotNull NameFunction firstNameFunction,
@NotNull NameFunction secondNameFunction)
{
return Sets.intersection(collect(first, firstNameFunction), collect(second, secondNameFunction));
}
public synchronized Optional<AuthorizationConfig> getConfig()
{
logger.debug("Get config -> " + config);
return Optional.ofNullable(config);
}
public synchronized void updateConfig(AuthorizationConfig config)
{
validateConfig(config);
// this is enough because we will execute actual saving before this bean died
// see method annotated with @PreDestroy
this.config = config;
logger.debug("Update config -> " + this.config);
saveConfig();
}
private Resource getConfigurationResource()
throws IOException
{
return ConfigurationResourceResolver.getConfigurationResource("authorization.config.xml",
"etc/conf/security-authorization.xml");
}
// used to receive String representation of any object to execute future comparisons based on that
private interface NameFunction
{
String name(Object o);
}
}