Skip to content

Commit 7a8bbfe

Browse files
committed
OSGi support for Java HTTP Client
This closes #3276
1 parent bc18c36 commit 7a8bbfe

12 files changed

+1013
-4
lines changed

all/src/main/content/jcr_root/apps/acs-commons/config/org.apache.sling.jcr.repoinit.RepositoryInitializer-acs-commons-all.config

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ set ACL for anonymous
4646
allow jcr:read on /conf restriction(rep:glob,/*/settings/redirects)
4747
end
4848

49+
# user to read keystores of users as well as global truststore
50+
create service user acs-commons-osgi-key-store-factory with path system/acs-commons
51+
set ACL for acs-commons-osgi-key-store-factory
52+
allow jcr:read on /home/users
53+
allow jcr:read on /etc/truststore
54+
end
55+
4956
create service user acs-commons-automatic-package-replicator-service with path system/acs-commons
5057
create path /etc/acs-commons/automatic-package-replication(sling:OrderedFolder)
5158
set ACL for acs-commons-automatic-package-replicator-service

all/src/main/content/jcr_root/apps/acs-commons/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-acs-commons-all.config

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ user.mapping=[ \
1515
"com.adobe.acs.acs-aem-commons-bundle:workflowpackagemanager-service\=[acs-commons-workflowpackagemanager-service]", \
1616
"com.adobe.acs.acs-aem-commons-bundle:redirect-manager\=[acs-commons-manage-redirects-service]", \
1717
"com.adobe.acs.acs-aem-commons-bundle:marketo-conf\=[acs-commons-marketo-conf-service]", \
18-
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]" \
18+
"com.adobe.acs.acs-aem-commons-bundle:package-garbage-collection\=[acs-commons-package-garbage-collection-service]", \
19+
"com.adobe.acs.acs-aem-commons-bundle:key-store-factory\=[acs-commons-osgi-key-store-factory]"
1920
]
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* ACS AEM Commons
3+
*
4+
* Copyright (C) 2024 Konrad Windszus
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.adobe.acs.commons.http;
19+
20+
import java.net.URI;
21+
import java.net.http.HttpClient;
22+
import java.net.http.HttpRequest;
23+
import java.util.function.Consumer;
24+
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
28+
/**
29+
* Encapsulates a single Java {@link HttpClient}. Its lifetime and basic configuration is managed via OSGi (Config Admin and Declarative
30+
* Services).
31+
* @since 2.2.0 (Bundle Version 6.5.0)
32+
* @see HttpClientFactory HttpClientFactory, for a similar service for the Apache Http Client
33+
*/
34+
public interface OsgiManagedJavaHttpClient {
35+
36+
/** Returns the configured HTTP client.
37+
*
38+
* @return the HTTP client
39+
*/
40+
@NotNull HttpClient getClient();
41+
42+
/**
43+
* Similar to {@link #getClient()} but customizes the underlying {@link HttpClient.Builder} which is used to create the singleton HTTP
44+
* client
45+
*
46+
* @param builderCustomizer a {@link Consumer} taking the {@link HttpClient.Builder} initialized with the configured basic options.
47+
*
48+
* @throws IllegalStateException in case {@link #getClient()} has been called already
49+
*/
50+
@NotNull HttpClient getClient(@Nullable Consumer<HttpClient.Builder> builderCustomizer);
51+
52+
/** Creates a new configured HTTP request.
53+
*
54+
* @param uri the URI to target
55+
* @return the new request
56+
*/
57+
@NotNull HttpRequest createRequest(@NotNull URI uri);
58+
59+
/** Creates a new configured HTTP request.
60+
*
61+
* @param uri the URI to target
62+
* @return the new request
63+
*/
64+
@NotNull HttpRequest createRequest(@NotNull URI uri, @Nullable Consumer<HttpRequest.Builder> builderCustomizer);
65+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* ACS AEM Commons
3+
*
4+
* Copyright (C) 2024 Konrad Windszus
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.adobe.acs.commons.http.impl;
19+
20+
import java.io.IOException;
21+
import java.security.KeyStore;
22+
import java.util.Map;
23+
24+
import javax.jcr.Node;
25+
import javax.jcr.Property;
26+
import javax.jcr.RepositoryException;
27+
import javax.net.ssl.X509TrustManager;
28+
29+
import org.apache.jackrabbit.api.security.user.Authorizable;
30+
import org.apache.jackrabbit.api.security.user.User;
31+
import org.apache.jackrabbit.api.security.user.UserManager;
32+
import org.apache.sling.api.SlingIOException;
33+
import org.apache.sling.api.resource.LoginException;
34+
import org.apache.sling.api.resource.Resource;
35+
import org.apache.sling.api.resource.ResourceResolver;
36+
import org.apache.sling.api.resource.ResourceResolverFactory;
37+
import org.apache.sling.api.resource.ResourceUtil;
38+
import org.apache.sling.serviceusermapping.ServiceUserMapped;
39+
import org.jetbrains.annotations.NotNull;
40+
import org.osgi.service.component.annotations.Activate;
41+
import org.osgi.service.component.annotations.Component;
42+
import org.osgi.service.component.annotations.Reference;
43+
44+
import com.adobe.granite.crypto.CryptoException;
45+
import com.adobe.granite.crypto.CryptoSupport;
46+
import com.adobe.granite.keystore.KeyStoreService;
47+
48+
@Component(service=AemKeyStoreFactory.class)
49+
public class AemKeyStoreFactory {
50+
51+
private static final String SUB_SERVICE_NAME = "key-store-factory";
52+
53+
private static final Map<String, Object> SERVICE_USER = Map.of(ResourceResolverFactory.SUBSERVICE,
54+
SUB_SERVICE_NAME);
55+
56+
/** Defer starting the service until service user mapping is available. */
57+
@Reference(target = "(|(" + ServiceUserMapped.SUBSERVICENAME + "=" + SUB_SERVICE_NAME + ")(!("
58+
+ ServiceUserMapped.SUBSERVICENAME + "=*)))")
59+
private ServiceUserMapped serviceUserMapped;
60+
61+
private final ResourceResolverFactory resolverFactory;
62+
private final KeyStoreService keyStoreService;
63+
private final CryptoSupport cryptoSupport;
64+
65+
@Activate()
66+
public AemKeyStoreFactory(@Reference ResourceResolverFactory resolverFactory,
67+
@Reference KeyStoreService keyStoreService,
68+
@Reference CryptoSupport cryptoSupport) {
69+
this.resolverFactory = resolverFactory;
70+
this.keyStoreService = keyStoreService;
71+
this.cryptoSupport = cryptoSupport;
72+
}
73+
74+
/** @return the global AEM trust store
75+
* @throws LoginException
76+
* @see <a href=
77+
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
78+
* internal APIs having private certificates</a> */
79+
public @NotNull X509TrustManager getTrustManager() throws LoginException {
80+
try (final var serviceResolver = getKeyStoreResourceResolver()) {
81+
return (X509TrustManager) keyStoreService.getTrustManager(serviceResolver);
82+
}
83+
}
84+
85+
/** @return the global AEM trust store
86+
* @throws LoginException
87+
* @see <a href=
88+
* "https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/call-internal-apis-having-private-certificate.html?lang=en">Call
89+
* internal APIs having private certificates</a> */
90+
public @NotNull KeyStore getTrustStore() throws LoginException {
91+
try (final var serviceResolver = getKeyStoreResourceResolver()) {
92+
var aemTrustStore = keyStoreService.getTrustStore(serviceResolver);
93+
return aemTrustStore;
94+
}
95+
}
96+
97+
public @NotNull KeyStore getKeyStore(@NotNull final String userId) throws LoginException {
98+
try (final var serviceResolver = getKeyStoreResourceResolver()) {
99+
// using the password set for the user Id's keystore to decrypt the entry
100+
return keyStoreService.getKeyStore(serviceResolver, userId);
101+
}
102+
}
103+
104+
public @NotNull char[] getKeyStorePassword(@NotNull final String userId) throws LoginException {
105+
try (final var serviceResolver = getKeyStoreResourceResolver()) {
106+
User user = retrieveUser(serviceResolver, userId);
107+
String path = getKeyStorePathForUser(user, "store.p12");
108+
return extractStorePassword(serviceResolver, path, cryptoSupport);
109+
}
110+
}
111+
112+
private @NotNull ResourceResolver getKeyStoreResourceResolver() throws LoginException {
113+
return this.resolverFactory.getServiceResourceResolver(SERVICE_USER);
114+
}
115+
116+
// the following methods are extracted from com.adobe.granite.keystore.internal.KeyStoreServiceImpl, because there is no public method
117+
// for retrieving the keystore's password
118+
private static User retrieveUser(ResourceResolver resolver, String userId)
119+
throws IllegalArgumentException, SlingIOException {
120+
UserManager userManager = (UserManager) resolver.adaptTo(UserManager.class);
121+
if (userManager != null) {
122+
Authorizable authorizable;
123+
try {
124+
authorizable = userManager.getAuthorizable(userId);
125+
} catch (RepositoryException var6) {
126+
throw new SlingIOException(new IOException(var6));
127+
}
128+
129+
if (authorizable != null && !authorizable.isGroup()) {
130+
User user = (User) authorizable;
131+
return user;
132+
} else {
133+
throw new IllegalArgumentException("The provided userId does not identify an existing user.");
134+
}
135+
} else {
136+
throw new IllegalArgumentException("Cannot obtain a UserManager for the given resource resolver.");
137+
}
138+
}
139+
140+
private static String getKeyStorePathForUser(User user, String keyStoreFileName) throws SlingIOException {
141+
String userHome;
142+
try {
143+
userHome = user.getPath();
144+
} catch (RepositoryException var4) {
145+
throw new SlingIOException(new IOException(var4));
146+
}
147+
return userHome + "/" + "keystore" + "/" + keyStoreFileName;
148+
}
149+
150+
private static char[] extractStorePassword(ResourceResolver resolver, String storePath, CryptoSupport cryptoSupport)
151+
throws SecurityException {
152+
Resource storeResource = resolver.getResource(storePath);
153+
if (storeResource != null) {
154+
Node storeParentNode = (Node) storeResource.getParent().adaptTo(Node.class);
155+
156+
try {
157+
Property passwordProperty = storeParentNode.getProperty("keystorePassword");
158+
if (passwordProperty != null) {
159+
return cryptoSupport.unprotect(passwordProperty.getString()).toCharArray();
160+
} else {
161+
throw new SecurityException(
162+
"Missing 'keystorePassword' property on " + ResourceUtil.getParent(storePath));
163+
}
164+
} catch (RepositoryException var6) {
165+
throw new SecurityException(var6);
166+
} catch (CryptoException var7) {
167+
throw new SecurityException(var7);
168+
}
169+
} else {
170+
return null;
171+
}
172+
}
173+
174+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* ACS AEM Commons
3+
*
4+
* Copyright (C) 2024 Konrad Windszus
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package com.adobe.acs.commons.http.impl;
19+
20+
import java.net.Socket;
21+
import java.security.KeyStore;
22+
import java.security.KeyStoreException;
23+
import java.security.NoSuchAlgorithmException;
24+
import java.security.Principal;
25+
import java.security.PrivateKey;
26+
import java.security.UnrecoverableKeyException;
27+
import java.security.cert.X509Certificate;
28+
import java.util.Arrays;
29+
30+
import javax.net.ssl.KeyManagerFactory;
31+
import javax.net.ssl.X509KeyManager;
32+
33+
import org.jetbrains.annotations.NotNull;
34+
35+
public class KeyManagerUtils {
36+
37+
private KeyManagerUtils() {
38+
// no supposed to be instantiated
39+
}
40+
41+
static @NotNull X509KeyManager createSingleClientSideCertificateKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password, @NotNull String clientCertAlias) throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException {
42+
return new FixClientAliasX509KeyManagerWrapper(clientCertAlias, createKeyManager(keyStore, password));
43+
}
44+
45+
private static @NotNull X509KeyManager createKeyManager(@NotNull KeyStore keyStore, @NotNull char[] password)
46+
throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
47+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
48+
kmf.init(keyStore, password);
49+
return (X509KeyManager) Arrays.stream(kmf.getKeyManagers()).filter(X509KeyManager.class::isInstance).findFirst().orElseThrow(() -> new IllegalStateException("The KeyManagerFactory does not expose a X509KeyManager"));
50+
}
51+
52+
private static final class FixClientAliasX509KeyManagerWrapper implements X509KeyManager {
53+
private final String clientAlias;
54+
private final X509KeyManager delegate;
55+
56+
FixClientAliasX509KeyManagerWrapper(String clientAlias, X509KeyManager delegate) {
57+
this.clientAlias = clientAlias;
58+
this.delegate = delegate;
59+
}
60+
61+
@Override
62+
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
63+
return clientAlias;
64+
}
65+
66+
@Override
67+
public X509Certificate[] getCertificateChain(String alias) {
68+
return delegate.getCertificateChain(alias);
69+
}
70+
71+
@Override
72+
public String[] getClientAliases(String s, Principal[] principals) {
73+
return delegate.getClientAliases(s, principals);
74+
}
75+
76+
@Override
77+
public String[] getServerAliases(String s, Principal[] principals) {
78+
return delegate.getServerAliases(s, principals);
79+
}
80+
81+
@Override
82+
public String chooseServerAlias(String s, Principal[] principals, Socket socket) {
83+
return delegate.chooseServerAlias(s, principals, socket);
84+
}
85+
86+
@Override
87+
public PrivateKey getPrivateKey(String s) {
88+
return delegate.getPrivateKey(s);
89+
}
90+
}
91+
92+
}

0 commit comments

Comments
 (0)