From 2d97b34959dbe8fbb938fef7b9ac646bd328dfd5 Mon Sep 17 00:00:00 2001
From: MohinishSah <88024811+MohinishSah@users.noreply.github.com>
Date: Fri, 29 Nov 2024 11:03:37 +0530
Subject: [PATCH] Update ZipDownloadExporter.java
---
.../repo/download/ZipDownloadExporter.java | 941 +++++++++++++-----
1 file changed, 705 insertions(+), 236 deletions(-)
diff --git a/repository/src/main/java/org/alfresco/repo/download/ZipDownloadExporter.java b/repository/src/main/java/org/alfresco/repo/download/ZipDownloadExporter.java
index 8bab018fbd..104db2818b 100644
--- a/repository/src/main/java/org/alfresco/repo/download/ZipDownloadExporter.java
+++ b/repository/src/main/java/org/alfresco/repo/download/ZipDownloadExporter.java
@@ -23,319 +23,788 @@
* along with Alfresco. If not, see .
* #L%
*/
-package org.alfresco.repo.download;
+package org.alfresco.repo.security.authentication.identityservice;
+import static java.util.Objects.requireNonNull;
+import static java.util.Optional.ofNullable;
+import static java.util.function.Predicate.not;
+
+import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.AUDIENCE;
+import static org.alfresco.repo.security.authentication.identityservice.IdentityServiceMetadataKey.SCOPES_SUPPORTED;
+
+import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.attribute.FileTime;
-import java.util.Date;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.LinkedList;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
-import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
-import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
-import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream.UnicodeExtraFieldPolicy;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.oauth2.sdk.Scope;
+import com.nimbusds.oauth2.sdk.id.Identifier;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
+import org.apache.hc.client5.http.ssl.TrustAllStrategy;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.security.converter.RsaKeyConverters;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
+import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+import org.springframework.security.oauth2.core.converter.ClaimTypeConverter;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
+import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtClaimNames;
+import org.springframework.security.oauth2.jwt.JwtClaimValidator;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
-import org.alfresco.model.ContentModel;
-import org.alfresco.repo.transaction.RetryingTransactionHelper;
-import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
-import org.alfresco.service.cmr.coci.CheckOutCheckInService;
-import org.alfresco.service.cmr.dictionary.DictionaryService;
-import org.alfresco.service.cmr.download.DownloadStatus;
-import org.alfresco.service.cmr.download.DownloadStatus.Status;
-import org.alfresco.service.cmr.repository.ContentData;
-import org.alfresco.service.cmr.repository.NodeRef;
-import org.alfresco.service.cmr.repository.NodeService;
-import org.alfresco.service.cmr.view.ExporterContext;
-import org.alfresco.service.cmr.view.ExporterException;
-import org.alfresco.service.namespace.QName;
-import org.alfresco.util.Pair;
+import org.alfresco.repo.security.authentication.identityservice.IdentityServiceFacade.IdentityServiceFacadeException;
/**
- * Handler for exporting node content to a ZIP file
- *
- * @author Alex Miller
+ * Creates an instance of {@link IdentityServiceFacade}.
+ * This factory can return a null if it is disabled.
*/
-public class ZipDownloadExporter extends BaseExporter
+public class IdentityServiceFacadeFactoryBean implements FactoryBean
{
- private static Logger log = LoggerFactory.getLogger(ZipDownloadExporter.class);
+ private static final Log LOGGER = LogFactory.getLog(IdentityServiceFacadeFactoryBean.class);
- private static final String PATH_SEPARATOR = "/";
+ private static final JOSEObjectType AT_JWT = new JOSEObjectType("at+jwt");
- protected ZipArchiveOutputStream zipStream;
+ private boolean enabled;
+ private SpringBasedIdentityServiceFacadeFactory factory;
- private NodeRef downloadNodeRef;
- private int sequenceNumber = 1;
- private long total;
- private long done;
- private long totalFileCount;
- private long filesAddedCount;
-
- private RetryingTransactionHelper transactionHelper;
- private DownloadStorage downloadStorage;
- private DictionaryService dictionaryService;
- private DownloadStatusUpdateService updateService;
-
- private Deque> path = new LinkedList>();
- private String currentName;
-
- private OutputStream outputStream;
- private Date zipTimestampCreated;
- private Date zipTimestampModified;
-
- /**
- * Construct
- *
- * @param zipFile
- * File
- * @param checkOutCheckInService
- * CheckOutCheckInService
- * @param nodeService
- * NodeService
- * @param transactionHelper
- * RetryingTransactionHelper
- * @param updateService
- * DownloadStatusUpdateService
- * @param downloadStorage
- * DownloadStorage
- * @param dictionaryService
- * DictionaryService
- * @param downloadNodeRef
- * NodeRef
- * @param total
- * long
- * @param totalFileCount
- * long
- */
- public ZipDownloadExporter(File zipFile, CheckOutCheckInService checkOutCheckInService, NodeService nodeService, RetryingTransactionHelper transactionHelper, DownloadStatusUpdateService updateService, DownloadStorage downloadStorage, DictionaryService dictionaryService, NodeRef downloadNodeRef, long total, long totalFileCount)
+ public void setEnabled(boolean enabled)
{
- super(checkOutCheckInService, nodeService);
- try
- {
- this.outputStream = new FileOutputStream(zipFile);
- this.updateService = updateService;
- this.transactionHelper = transactionHelper;
- this.downloadStorage = downloadStorage;
- this.dictionaryService = dictionaryService;
+ this.enabled = enabled;
+ }
- this.downloadNodeRef = downloadNodeRef;
- this.total = total;
- this.totalFileCount = totalFileCount;
- }
- catch (FileNotFoundException e)
- {
- throw new ExporterException("Failed to create zip file", e);
- }
+ public void setIdentityServiceConfig(IdentityServiceConfig identityServiceConfig)
+ {
+ factory = new SpringBasedIdentityServiceFacadeFactory(
+ new HttpClientProvider(identityServiceConfig)::createHttpClient,
+ new ClientRegistrationProvider(identityServiceConfig)::createClientRegistration,
+ new JwtDecoderProvider(identityServiceConfig)::createJwtDecoder);
}
@Override
- public void start(final ExporterContext context)
+ public IdentityServiceFacade getObject() throws Exception
{
- zipStream = new ZipArchiveOutputStream(outputStream);
- // NOTE: This encoding allows us to workaround bug...
- // http://bugs.sun.com/bugdatabase/view_bug.do;:WuuT?bug_id=4820807
- zipStream.setEncoding("UTF-8");
- zipStream.setCreateUnicodeExtraFields(UnicodeExtraFieldPolicy.ALWAYS);
- zipStream.setUseLanguageEncodingFlag(true);
- zipStream.setFallbackToUTF8(true);
+ // The creation of the client can be disabled for testing or when the username/password authentication is not required,
+ // for instance when Identity Service is configured for 'bearer only' authentication or Direct Access Grants are disabled.
+ if (!enabled)
+ {
+ return null;
+ }
+
+ return new LazyInstantiatingIdentityServiceFacade(factory::createIdentityServiceFacade);
}
@Override
- public void startNode(NodeRef nodeRef)
+ public Class> getObjectType()
{
- this.currentName = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME);
- this.zipTimestampCreated = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_CREATED);
- this.zipTimestampModified = (Date) nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED);
- path.push(new Pair(currentName, nodeRef));
- if (dictionaryService.isSubClass(nodeService.getType(nodeRef), ContentModel.TYPE_FOLDER))
+ return IdentityServiceFacade.class;
+ }
+
+ @Override
+ public boolean isSingleton()
+ {
+ return true;
+ }
+
+ private static IdentityServiceFacadeException authorizationServerCantBeUsedException(RuntimeException cause)
+ {
+ return new IdentityServiceFacadeException("Unable to use the Authorization Server.", cause);
+ }
+
+ // The target facade is created lazily to improve resiliency on Identity Service
+ // (Keycloak/Authorization Server) failures when Spring Context is starting up.
+ static class LazyInstantiatingIdentityServiceFacade implements IdentityServiceFacade
+ {
+ private final AtomicReference targetFacade = new AtomicReference<>();
+ private final Supplier targetFacadeCreator;
+
+ LazyInstantiatingIdentityServiceFacade(Supplier targetFacadeCreator)
+ {
+ this.targetFacadeCreator = requireNonNull(targetFacadeCreator);
+ }
+
+ @Override
+ public AccessTokenAuthorization authorize(AuthorizationGrant grant) throws AuthorizationException
+ {
+ return getTargetFacade().authorize(grant);
+ }
+
+ @Override
+ public DecodedAccessToken decodeToken(String token) throws TokenDecodingException
+ {
+ return getTargetFacade().decodeToken(token);
+ }
+
+ @Override
+ public Optional getUserInfo(String token, String principalAttribute)
+ {
+ return getTargetFacade().getUserInfo(token, principalAttribute);
+ }
+
+ @Override
+ public ClientRegistration getClientRegistration()
+ {
+ return getTargetFacade().getClientRegistration();
+ }
+
+ private IdentityServiceFacade getTargetFacade()
+ {
+ return ofNullable(targetFacade.get())
+ .orElseGet(() -> targetFacade.updateAndGet(prev -> ofNullable(prev).orElseGet(this::createTargetFacade)));
+ }
+
+ private IdentityServiceFacade createTargetFacade()
{
- String path = getPath() + PATH_SEPARATOR;
- ZipArchiveEntry archiveEntry = new ZipArchiveEntry(path);
try
{
- archiveEntry.setTime(zipTimestampCreated.getTime());
- archiveEntry.setCreationTime(FileTime.fromMillis(zipTimestampCreated.getTime()));
- archiveEntry.setLastModifiedTime(FileTime.fromMillis(zipTimestampModified.getTime()));
- zipStream.putArchiveEntry(archiveEntry);
- zipStream.closeArchiveEntry();
+ return targetFacadeCreator.get();
}
- catch (IOException e)
+ catch (IdentityServiceFacadeException e)
{
- throw new ExporterException("Unexpected IOException adding folder entry", e);
+ throw e;
}
- }
- }
-
- @Override
- public void contentImpl(NodeRef nodeRef, QName property, InputStream content, ContentData contentData, int index)
- {
- // if the content stream to output is empty, then just return content descriptor as is
- if (content == null)
- {
- log.info("Archiving content has been removed or modified for the specified NodeReference: " + nodeRef
- + ", and the size of the content is " + contentData.getSize());
- return;
- }
-
- try
- {
- if (log.isDebugEnabled())
+ catch (RuntimeException e)
{
- log.debug("Archiving content for nodeRef: " + nodeRef + " with contentURL: " + contentData.getContentUrl());
+ LOGGER.warn("Failed to instantiate IdentityServiceFacade.", e);
+ throw authorizationServerCantBeUsedException(e);
}
- // ALF-2016
- ZipArchiveEntry zipEntry = new ZipArchiveEntry(getPath());
- zipEntry.setTime(zipTimestampCreated.getTime());
- zipEntry.setCreationTime(FileTime.fromMillis(zipTimestampCreated.getTime()));
- zipEntry.setLastModifiedTime(FileTime.fromMillis(zipTimestampModified.getTime()));
- zipStream.putArchiveEntry(zipEntry);
-
- // copy export stream to zip
- copyStream(zipStream, content);
-
- zipStream.closeArchiveEntry();
- filesAddedCount = filesAddedCount + 1;
- }
- catch (IOException e)
- {
- throw new ExporterException("Failed to zip export stream", e);
}
}
- @Override
- public void endNode(NodeRef nodeRef)
+ private static class SpringBasedIdentityServiceFacadeFactory
{
- path.pop();
- }
+ private final Supplier httpClientProvider;
+ private final Function clientRegistrationProvider;
+ private final BiFunction jwtDecoderProvider;
- @Override
- public void end()
- {
- try
+ SpringBasedIdentityServiceFacadeFactory(
+ Supplier httpClientProvider,
+ Function clientRegistrationProvider,
+ BiFunction jwtDecoderProvider)
{
- zipStream.close();
+ this.httpClientProvider = requireNonNull(httpClientProvider);
+ this.clientRegistrationProvider = requireNonNull(clientRegistrationProvider);
+ this.jwtDecoderProvider = requireNonNull(jwtDecoderProvider);
}
- catch (IOException error)
+
+ private IdentityServiceFacade createIdentityServiceFacade()
{
- throw new ExporterException("Unexpected error closing zip stream!", error);
+ // Here we preserve the behaviour of previously used Keycloak Adapter
+ // * Client is authenticating itself using basic auth
+ // * Resource Owner Password Credentials Flow is used to authenticate Resource Owner
+
+ final ClientHttpRequestFactory httpRequestFactory = new CustomClientHttpRequestFactory(
+ httpClientProvider.get());
+ final RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
+ final ClientRegistration clientRegistration = clientRegistrationProvider.apply(restTemplate);
+ final JwtDecoder jwtDecoder = jwtDecoderProvider.apply(restTemplate,
+ clientRegistration.getProviderDetails());
+
+ return new SpringBasedIdentityServiceFacade(createOAuth2RestTemplate(httpRequestFactory),
+ clientRegistration, jwtDecoder);
+ }
+
+ private RestTemplate createOAuth2RestTemplate(ClientHttpRequestFactory requestFactory)
+ {
+ final RestTemplate restTemplate = new RestTemplate(
+ Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
+ restTemplate.setRequestFactory(requestFactory);
+ restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
+
+ return restTemplate;
}
}
- private String getPath()
+ private static class HttpClientProvider
{
- if (path.size() < 1)
+ private final IdentityServiceConfig config;
+
+ private HttpClientProvider(IdentityServiceConfig config)
{
- throw new IllegalStateException("No elements in path!");
+ this.config = requireNonNull(config);
}
- Iterator> iter = path.descendingIterator();
- StringBuilder pathBuilder = new StringBuilder();
-
- while (iter.hasNext())
+ private HttpClient createHttpClient()
{
- Pair element = iter.next();
-
- pathBuilder.append(element.getFirst());
- if (iter.hasNext())
+ try
{
- pathBuilder.append(PATH_SEPARATOR);
+ HttpClientBuilder clientBuilder = HttpClients.custom();
+ applyConfiguration(clientBuilder);
+ return clientBuilder.build();
+ }
+ catch (Exception e)
+ {
+ throw new IllegalStateException("Failed to create ClientHttpRequestFactory. " + e.getMessage(), e);
}
}
- return pathBuilder.toString();
- }
-
- /**
- * Copy input stream to output stream
- *
- * @param output
- * output stream
- * @param in
- * input stream
- * @throws IOException
- */
- private void copyStream(OutputStream output, InputStream in)
- throws IOException
- {
- byte[] buffer = new byte[2048 * 10];
- int read = in.read(buffer, 0, 2048 * 10);
- int i = 0;
- while (read != -1)
+ private void applyConfiguration(HttpClientBuilder builder) throws Exception
{
- output.write(buffer, 0, read);
- done = done + read;
+ final PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create();
- // ALF-16289 - only update the status every 10MB
- if (i++ % 500 == 0)
- {
- updateStatus();
- checkCancelled();
- }
+ applyConnectionConfiguration(connectionManagerBuilder);
+ applySSLConfiguration(connectionManagerBuilder);
- read = in.read(buffer, 0, 2048 * 10);
+ builder.setConnectionManager(connectionManagerBuilder.build());
}
- }
- private void checkCancelled()
- {
- boolean downloadCancelled = transactionHelper.doInTransaction(new RetryingTransactionCallback() {
- @Override
- public Boolean execute() throws Throwable
- {
- return downloadStorage.isCancelled(downloadNodeRef);
- }
- }, true, true);
-
- if (downloadCancelled == true)
+ private void applyConnectionConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
{
- log.debug("Download cancelled");
- throw new DownloadCancelledException();
+ final ConnectionConfig connectionConfig = ConnectionConfig.custom()
+ .setConnectTimeout(config.getClientConnectionTimeout(), TimeUnit.MILLISECONDS)
+ .setSocketTimeout(config.getClientSocketTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ connectionManagerBuilder.setMaxConnTotal(config.getConnectionPoolSize());
+ connectionManagerBuilder.setDefaultConnectionConfig(connectionConfig);
+ }
+
+ private void applySSLConfiguration(PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder)
+ throws Exception
+ {
+ SSLContextBuilder sslContextBuilder = null;
+ if (config.isDisableTrustManager())
+ {
+ sslContextBuilder = SSLContexts.custom()
+ .loadTrustMaterial(TrustAllStrategy.INSTANCE);
+
+ }
+ else if (isDefined(config.getTruststore()))
+ {
+ final char[] truststorePassword = asCharArray(config.getTruststorePassword(), null);
+ sslContextBuilder = SSLContexts.custom()
+ .loadTrustMaterial(new File(config.getTruststore()), truststorePassword);
+ }
+
+ if (isDefined(config.getClientKeystore()))
+ {
+ if (sslContextBuilder == null)
+ {
+ sslContextBuilder = SSLContexts.custom();
+ }
+ final char[] keystorePassword = asCharArray(config.getClientKeystorePassword(), null);
+ final char[] keyPassword = asCharArray(config.getClientKeyPassword(), keystorePassword);
+ sslContextBuilder.loadKeyMaterial(new File(config.getClientKeystore()), keystorePassword, keyPassword);
+ }
+
+ final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder.create();
+
+ if (sslContextBuilder != null)
+ {
+ sslConnectionSocketFactoryBuilder.setSslContext(sslContextBuilder.build());
+ }
+
+ if (config.isDisableTrustManager() || config.isAllowAnyHostname())
+ {
+ sslConnectionSocketFactoryBuilder.setHostnameVerifier(NoopHostnameVerifier.INSTANCE);
+ }
+ final SSLConnectionSocketFactory sslConnectionSocketFactory = sslConnectionSocketFactoryBuilder.build();
+ connectionManagerBuilder.setSSLSocketFactory(sslConnectionSocketFactory);
+ }
+
+ private char[] asCharArray(String value, char... nullValue)
+ {
+ return ofNullable(value)
+ .filter(not(String::isBlank))
+ .map(String::toCharArray)
+ .orElse(nullValue);
}
}
- private void updateStatus()
+ static class ClientRegistrationProvider
{
- transactionHelper.doInTransaction(new RetryingTransactionCallback