Skip to content

Commit

Permalink
OSGi support for Java HTTP Client
Browse files Browse the repository at this point in the history
This closes #3276
  • Loading branch information
kwin committed Feb 28, 2024
1 parent bc18c36 commit c56ac13
Show file tree
Hide file tree
Showing 12 changed files with 1,011 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ set ACL for anonymous
allow jcr:read on /conf restriction(rep:glob,/*/settings/redirects)
end

# user to read keystores of users as well as global truststore
create service user acs-commons-osgi-key-store-factory with path system/acs-commons
set ACL for acs-commons-osgi-key-store-factory
allow jcr:read on /home/users
allow jcr:read on /etc/truststore
end

create service user acs-commons-automatic-package-replicator-service with path system/acs-commons
create path /etc/acs-commons/automatic-package-replication(sling:OrderedFolder)
set ACL for acs-commons-automatic-package-replicator-service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ user.mapping=[ \
"com.adobe.acs.acs-aem-commons-bundle:workflowpackagemanager-service\=[acs-commons-workflowpackagemanager-service]", \
"com.adobe.acs.acs-aem-commons-bundle:redirect-manager\=[acs-commons-manage-redirects-service]", \
"com.adobe.acs.acs-aem-commons-bundle:marketo-conf\=[acs-commons-marketo-conf-service]", \
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]" \
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]", \
"com.adobe.acs.acs-aem-commons-bundle:key-store-factory\=[acs-commons-osgi-key-store-factory]"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.function.Consumer;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* Encapsulates a single Java {@link HttpClient}. Its lifetime and basic configuration is managed via OSGi (Config Admin and Declarative
* Services).
* @since 2.2.0 (Bundle Version 6.5.0)
* @see HttpClientFactory HttpClientFactory, for a similar service for the Apache Http Client
*/
public interface OsgiManagedJavaHttpClient {

/** Returns the configured HTTP client.
*
* @return the HTTP client
*/
@NotNull HttpClient getClient();

/*
* Similar to {@link #getClient()} but customizes the underlying {@link HttpClient.Builder} which is used to create the singleton HTTP
* client
*
* @param builderCustomizer a {@link Consumer} taking the {@link HttpClient.Builder} initialized with the configured basic options.
*
* @throws IllegalStateException in case {@link #getClient()} has been called already
*/
@NotNull HttpClient getClient(@Nullable Consumer<HttpClient.Builder> builderCustomizer);

/** Returns the configured HTTP request.
*
* @param uri the URI to target
* @return the request
*/
@NotNull HttpRequest createRequest(@NotNull URI uri);

/** Returns the configured HTTP request.
*
* @param uri the URI to target
* @return the request
*/
@NotNull HttpRequest createRequest(@NotNull URI uri, @Nullable Consumer<HttpRequest.Builder> builderCustomizer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http.impl;

import java.io.IOException;
import java.security.KeyStore;
import java.util.Map;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.net.ssl.X509TrustManager;

import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingIOException;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.jetbrains.annotations.NotNull;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import com.adobe.granite.crypto.CryptoException;
import com.adobe.granite.crypto.CryptoSupport;
import com.adobe.granite.keystore.KeyStoreService;

@Component(service=AemKeyStoreFactory.class)
public class AemKeyStoreFactory {

private static final String SUB_SERVICE_NAME = "key-store-factory";

private static final Map<String, Object> SERVICE_USER = Map.of(ResourceResolverFactory.SUBSERVICE,

Check warning on line 53 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L53

Added line #L53 was not covered by tests
SUB_SERVICE_NAME);

/** Defer starting the service until service user mapping is available. */
@Reference(target = "(|(" + ServiceUserMapped.SUBSERVICENAME + "=" + SUB_SERVICE_NAME + ")(!("
+ ServiceUserMapped.SUBSERVICENAME + "=*)))")
private ServiceUserMapped serviceUserMapped;

private final ResourceResolverFactory resolverFactory;
private final KeyStoreService keyStoreService;
private final CryptoSupport cryptoSupport;

@Activate()
public AemKeyStoreFactory(@Reference ResourceResolverFactory resolverFactory,
@Reference KeyStoreService keyStoreService,
@Reference CryptoSupport cryptoSupport) {
this.resolverFactory = resolverFactory;
this.keyStoreService = keyStoreService;
this.cryptoSupport = cryptoSupport;
}

Check warning on line 72 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L68-L72

Added lines #L68 - L72 were not covered by tests

/** @return the global AEM trust store
* @throws LoginException
* @see <a href=
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
* internal APIs having private certificates</a> */
public @NotNull X509TrustManager getTrustManager() throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
return (X509TrustManager) keyStoreService.getTrustManager(serviceResolver);

Check warning on line 81 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L80-L81

Added lines #L80 - L81 were not covered by tests
}
}

/** @return the global AEM trust store
* @throws LoginException
* @see <a href=
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
* internal APIs having private certificates</a> */
public @NotNull KeyStore getTrustStore() throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
var aemTrustStore = keyStoreService.getTrustStore(serviceResolver);
return aemTrustStore;

Check warning on line 93 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L91-L93

Added lines #L91 - L93 were not covered by tests
}
}

public @NotNull KeyStore getKeyStore(@NotNull final String userId) throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {

Check warning on line 98 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L98

Added line #L98 was not covered by tests
// using the password set for the user Id's keystore to decrypt the entry
return keyStoreService.getKeyStore(serviceResolver, userId);

Check warning on line 100 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L100

Added line #L100 was not covered by tests
}
}

public @NotNull char[] getKeyStorePassword(@NotNull final String userId) throws LoginException {
try (final var serviceResolver = getKeyStoreResourceResolver()) {
User user = retrieveUser(serviceResolver, userId);
String path = getKeyStorePathForUser(user, "store.p12");
return extractStorePassword(serviceResolver, path, cryptoSupport);

Check warning on line 108 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L105-L108

Added lines #L105 - L108 were not covered by tests
}
}

private @NotNull ResourceResolver getKeyStoreResourceResolver() throws LoginException {
return this.resolverFactory.getServiceResourceResolver(SERVICE_USER);

Check warning on line 113 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L113

Added line #L113 was not covered by tests
}

// the following methods are extracted from com.adobe.granite.keystore.internal.KeyStoreServiceImpl, because there is no public method
// for retrieving the keystore's password
private static User retrieveUser(ResourceResolver resolver, String userId)
throws IllegalArgumentException, SlingIOException {
UserManager userManager = (UserManager) resolver.adaptTo(UserManager.class);

Check warning on line 120 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L120

Added line #L120 was not covered by tests
if (userManager != null) {
Authorizable authorizable;
try {
authorizable = userManager.getAuthorizable(userId);
} catch (RepositoryException var6) {
throw new SlingIOException(new IOException(var6));
}

Check warning on line 127 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L124-L127

Added lines #L124 - L127 were not covered by tests

if (authorizable != null && !authorizable.isGroup()) {
User user = (User) authorizable;
return user;

Check warning on line 131 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L130-L131

Added lines #L130 - L131 were not covered by tests
} else {
throw new IllegalArgumentException("The provided userId does not identify an existing user.");

Check warning on line 133 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L133

Added line #L133 was not covered by tests
}
} else {
throw new IllegalArgumentException("Cannot obtain a UserManager for the given resource resolver.");

Check warning on line 136 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L136

Added line #L136 was not covered by tests
}
}

private static String getKeyStorePathForUser(User user, String keyStoreFileName) throws SlingIOException {
String userHome;
try {
userHome = user.getPath();
} catch (RepositoryException var4) {
throw new SlingIOException(new IOException(var4));
}
return userHome + "/" + "keystore" + "/" + keyStoreFileName;

Check warning on line 147 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L143-L147

Added lines #L143 - L147 were not covered by tests
}

private static char[] extractStorePassword(ResourceResolver resolver, String storePath, CryptoSupport cryptoSupport)
throws SecurityException {
Resource storeResource = resolver.getResource(storePath);

Check warning on line 152 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L152

Added line #L152 was not covered by tests
if (storeResource != null) {
Node storeParentNode = (Node) storeResource.getParent().adaptTo(Node.class);

Check warning on line 154 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L154

Added line #L154 was not covered by tests

try {
Property passwordProperty = storeParentNode.getProperty("keystorePassword");

Check warning on line 157 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L157

Added line #L157 was not covered by tests
if (passwordProperty != null) {
return cryptoSupport.unprotect(passwordProperty.getString()).toCharArray();

Check warning on line 159 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L159

Added line #L159 was not covered by tests
} else {
throw new SecurityException(
"Missing 'keystorePassword' property on " + ResourceUtil.getParent(storePath));

Check warning on line 162 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L161-L162

Added lines #L161 - L162 were not covered by tests
}
} catch (RepositoryException var6) {
throw new SecurityException(var6);
} catch (CryptoException var7) {
throw new SecurityException(var7);

Check warning on line 167 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L164-L167

Added lines #L164 - L167 were not covered by tests
}
} else {
return null;

Check warning on line 170 in bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/AemKeyStoreFactory.java#L170

Added line #L170 was not covered by tests
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* ACS AEM Commons
*
* Copyright (C) 2024 Konrad Windszus
*
* 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 com.adobe.acs.commons.http.impl;

import java.net.Socket;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.Arrays;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.X509KeyManager;

import org.jetbrains.annotations.NotNull;

public class KeyManagerUtils {

private KeyManagerUtils() {
// no supposed to be instantiated
}

static @NotNull X509KeyManager createSingleClientSideCertificateKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password, @NotNull String clientCertAlias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
return new FixClientAliasX509KeyManagerWrapper(clientCertAlias, createKeyManager(keyStore, password));

Check warning on line 42 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L42

Added line #L42 was not covered by tests
}

private static @NotNull X509KeyManager createKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password)
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, password);
return (X509KeyManager) Arrays.stream(kmf.getKeyManagers()).filter(X509KeyManager.class::isInstance).findFirst().orElseThrow(() -> new IllegalStateException("The KeyManagerFactory does not expose a X509KeyManager"));

Check warning on line 49 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L47-L49

Added lines #L47 - L49 were not covered by tests
}

private static final class FixClientAliasX509KeyManagerWrapper implements X509KeyManager {
private final String clientAlias;
private final X509KeyManager delegate;

FixClientAliasX509KeyManagerWrapper(String clientAlias, X509KeyManager delegate) {
this.clientAlias = clientAlias;
this.delegate = delegate;
}

Check warning on line 59 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L56-L59

Added lines #L56 - L59 were not covered by tests

@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return clientAlias;

Check warning on line 63 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L63

Added line #L63 was not covered by tests
}

@Override
public X509Certificate[] getCertificateChain(String alias) {
return delegate.getCertificateChain(alias);

Check warning on line 68 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L68

Added line #L68 was not covered by tests
}

@Override
public String[] getClientAliases(String s, Principal[] principals) {
return delegate.getClientAliases(s, principals);

Check warning on line 73 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L73

Added line #L73 was not covered by tests
}

@Override
public String[] getServerAliases(String s, Principal[] principals) {
return delegate.getServerAliases(s, principals);

Check warning on line 78 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L78

Added line #L78 was not covered by tests
}

@Override
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
return delegate.chooseServerAlias(s, principals, socket);

Check warning on line 83 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L83

Added line #L83 was not covered by tests
}

@Override
public PrivateKey getPrivateKey(String s) {
return delegate.getPrivateKey(s);

Check warning on line 88 in bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java

View check run for this annotation

Codecov / codecov/patch

bundle/src/main/java/com/adobe/acs/commons/http/impl/KeyManagerUtils.java#L88

Added line #L88 was not covered by tests
}
}

}
Loading

0 comments on commit c56ac13

Please sign in to comment.