Opencast still publishes global system account credentials
Description
Opencast is a free, open-source platform to support the management of educational audio and video content. Prior to version 17.6, Opencast would incorrectly send the hashed global system account credentials (ie: org.opencastproject.security.digest.user and org.opencastproject.security.digest.pass) when attempting to fetch mediapackage elements included in a mediapackage XML file. A previous CVE prevented many cases where the credentials were inappropriately sent, but not all. Anyone with ingest permissions could cause Opencast to send its hashed global system account credentials to a url of their choosing. This issue is fixed in Opencast 17.6.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.opencastproject:opencast-commonMaven | < 17.6 | 17.6 |
org.opencastproject:opencast-ingest-service-implMaven | < 17.6 | 17.6 |
org.opencastproject:opencast-kernelMaven | < 17.6 | 17.6 |
org.opencastproject:opencast-publication-service-oaipmh-remoteMaven | < 17.6 | 17.6 |
Affected products
1Patches
22d3219113e2bMerge commit from fork
7 files changed · +304 −56
modules/common/src/main/java/org/opencastproject/security/api/DefaultOrganization.java+7 −0 modified@@ -68,4 +68,11 @@ public DefaultOrganization() { DEFAULT_PROPERTIES); } + public DefaultOrganization(Map<String, Integer> override) { + super(DefaultOrganization.DEFAULT_ORGANIZATION_ID, DefaultOrganization.DEFAULT_ORGANIZATION_NAME, + null != override ? override : DEFAULT_SERVERS, + DefaultOrganization.DEFAULT_ORGANIZATION_ADMIN, DefaultOrganization.DEFAULT_ORGANIZATION_ANONYMOUS, + DEFAULT_PROPERTIES); + } + }
modules/ingest-service-impl/src/main/java/org/opencastproject/ingest/impl/IngestServiceImpl.java+2 −12 modified@@ -1704,7 +1704,6 @@ protected URI addContentToRepo(MediaPackage mp, String elementId, URI uri) throw try { if (uri.toString().startsWith("http")) { HttpGet get = new HttpGet(uri); - var clusterUrls = securityService.getOrganization().getServers().keySet(); if (!isBlank(downloadSource) && uri.toString().matches(downloadSource)) { // NB: We're creating a new client here with *different* auth than the system auth creds @@ -1717,13 +1716,9 @@ protected URI addContentToRepo(MediaPackage mp, String elementId, URI uri) throw get.setHeader(HttpHeaders.AUTHORIZATION, authHeader); } response = externalHttpClient.execute(get); - } else if (clusterUrls.contains(uri.getScheme() + "://" + uri.getHost())) { - // Only using the system-level httpclient and digest credentials against our own servers - response = httpClient.execute(get); } else { - //NB: No auth here at all - externalHttpClient = getNoAuthHttpClient(); - response = externalHttpClient.execute(get); + // httpClient checks internally to see if it should be sending the default auth, or not. + response = httpClient.execute(get); } if (null == response) { @@ -1917,11 +1912,6 @@ protected OrganizationDirectoryService getOrganizationDirectoryService() { return organizationDirectoryService; } - //Used in testing - protected CloseableHttpClient getNoAuthHttpClient() { - return HttpClientBuilder.create().build(); - } - protected CloseableHttpClient getAuthedHttpClient() { HttpClientBuilder cb = HttpClientBuilder.create(); CredentialsProvider provider = new BasicCredentialsProvider();
modules/ingest-service-impl/src/test/java/org/opencastproject/ingest/impl/IngestServiceImplTest.java+14 −29 modified@@ -73,6 +73,7 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -108,8 +109,8 @@ public class IngestServiceImplTest { private WorkflowInstance workflowInstance = null; private WorkingFileRepository wfr = null; private CloseableHttpResponse httpResponse = null; - private CloseableHttpClient credClient = null; - private CloseableHttpClient noCredClient = null; + private CloseableHttpClient customClient = null; + private TrustedHttpClient httpClient; private static URI baseDir; private static URI urlTrack; private static URI urlTrack1; @@ -273,7 +274,7 @@ private void setupService() throws Exception { EasyMock.expect(httpResponse.getEntity()).andReturn(entity).anyTimes(); EasyMock.replay(httpResponse); - TrustedHttpClient httpClient = EasyMock.createNiceMock(TrustedHttpClient.class); + httpClient = EasyMock.createNiceMock(TrustedHttpClient.class); EasyMock.expect(httpClient.execute((HttpGet) EasyMock.anyObject())).andReturn(httpResponse).anyTimes(); EasyMock.replay(httpClient); @@ -315,17 +316,6 @@ protected CloseableHttpClient getAuthedHttpClient() { EasyMock.replay(client); return client; } - @Override - protected CloseableHttpClient getNoAuthHttpClient() { - CloseableHttpClient client = EasyMock.createMock(CloseableHttpClient.class); - try { - EasyMock.expect(client.execute((HttpGet) EasyMock.anyObject())).andReturn(httpResponse).anyTimes(); - client.close(); - EasyMock.expectLastCall().once(); - } catch (Exception e) { } - EasyMock.replay(client); - return client; - } }; } service.setHttpClient(httpClient); @@ -434,33 +424,28 @@ public void testContentDisposition() throws Exception { } private void testAuthWhitelist(String url, String regex, boolean shouldFail, boolean shouldSendAuth, boolean shouldTouchMocks) throws Exception { - credClient = EasyMock.createNiceMock(CloseableHttpClient.class); - noCredClient = EasyMock.createNiceMock(CloseableHttpClient.class); + customClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.reset(httpClient); //There's one case (accessing the filesystem) where we *don't* expect the mocks to be used if (shouldTouchMocks) { if (shouldSendAuth) { - EasyMock.expect(credClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); - credClient.close(); + EasyMock.expect(customClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); + customClient.close(); EasyMock.expectLastCall().once(); } else { - EasyMock.expect(noCredClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); - noCredClient.close(); + EasyMock.expect(httpClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); + httpClient.close(EasyMock.anyObject(HttpResponse.class)); EasyMock.expectLastCall().once(); } } - EasyMock.replay(noCredClient, credClient); + EasyMock.replay(httpClient, customClient); //Recreate the service so we use our own, custom mocks service = new IngestServiceImpl() { @Override protected CloseableHttpClient getAuthedHttpClient() { - return credClient; - } - - @Override - protected CloseableHttpClient getNoAuthHttpClient() { - return noCredClient; + return customClient; } }; setupService(); @@ -481,8 +466,8 @@ protected CloseableHttpClient getNoAuthHttpClient() { Assert.fail("Should not have failed!"); } } - EasyMock.verify(credClient, noCredClient); - EasyMock.reset(credClient, noCredClient); + EasyMock.verify(customClient, httpClient); + EasyMock.reset(customClient, httpClient); } @Test
modules/kernel/src/main/java/org/opencastproject/kernel/security/TrustedHttpClientImpl.java+128 −11 modified@@ -24,8 +24,10 @@ import static org.opencastproject.kernel.rest.CurrentJobFilter.CURRENT_JOB_HEADER; import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.DIGEST_AUTH; import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER; +import static org.opencastproject.util.data.Collections.set; import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityConstants; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClient; @@ -34,7 +36,9 @@ import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; import org.opencastproject.security.util.HttpResponseWrapper; +import org.opencastproject.serviceregistry.api.HostRegistration; import org.opencastproject.serviceregistry.api.ServiceRegistry; +import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.urlsigning.utils.ResourceRequestUtil; import org.apache.commons.lang3.StringUtils; @@ -68,9 +72,13 @@ import java.io.IOException; import java.lang.management.ManagementFactory; +import java.net.URI; +import java.util.Collection; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import javax.management.MBeanServer; import javax.management.ObjectName; @@ -172,9 +180,16 @@ public class TrustedHttpClientImpl implements TrustedHttpClient, HttpConnectionM /** The security service */ protected SecurityService securityService = null; + /** The organization directory service */ + protected OrganizationDirectoryService organizationDirectoryService = null; + /** The url signing service */ protected UrlSigningService urlSigningService = null; + /** A regularly emptying cache of hosts in the cluster */ + private HostCache hosts = null; + + @Activate public void activate(ComponentContext cc) { logger.debug("activate"); @@ -210,6 +225,13 @@ public void activate(ComponentContext cc) { logger.debug("Expire signed URLs in {} seconds.", signedUrlExpiresDuration); } + private HostCache getHostCache() { + if (null == hosts) { + hosts = new HostCache(60000, this.organizationDirectoryService, this.serviceRegistry); + } + return hosts; + } + /** * Sets the service registry. * @@ -245,6 +267,17 @@ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } + /** + * Sets the organization directory service. + * + * @param organizationDirectoryService + * the organization directory service + */ + @Reference + public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) { + this.organizationDirectoryService = organizationDirectoryService; + } + /** * Sets the url signing service. * @@ -323,6 +356,7 @@ public void deactivate() { } public TrustedHttpClientImpl() { + } public TrustedHttpClientImpl(String user, String pass) { @@ -351,32 +385,41 @@ public HttpResponse execute(HttpUriRequest httpUriRequest) throws TrustedHttpCli @Override public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout, int socketTimeout) throws TrustedHttpClientException { - // Add the request header to elicit a digest auth response - httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH); - if (serviceRegistry != null && serviceRegistry.getCurrentJob() != null) { httpUriRequest.setHeader(CURRENT_JOB_HEADER, Long.toString(serviceRegistry.getCurrentJob().getId())); } + boolean enableDigest = getHostCache().contains(httpUriRequest.getURI().getHost()); + logger.debug("Digest auth enabled for this request: " + enableDigest); + if (enableDigest) { + // Add the request header to elicit a digest auth response + httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH); + } + // If a security service has been set, use it to pass the current security context on logger.debug("Adding security context to request"); final Organization organization = securityService.getOrganization(); if (organization != null) { httpUriRequest.setHeader(SecurityConstants.ORGANIZATION_HEADER, organization.getId()); final User currentUser = securityService.getUser(); - if (currentUser != null) { + if (enableDigest && currentUser != null) { httpUriRequest.setHeader(SecurityConstants.USER_HEADER, currentUser.getUsername()); } } final HttpClientBuilder clientBuilder = makeHttpClientBuilder(connectionTimeout, socketTimeout); if ("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod())) { - // Set the user/pass - CredentialsProvider provider = new BasicCredentialsProvider(); - provider.setCredentials( - new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST), - new UsernamePasswordCredentials(user, pass)); - final CloseableHttpClient httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build(); + final CloseableHttpClient httpClient; + if (enableDigest) { + // Set the user/pass + CredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials( + new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST), + new UsernamePasswordCredentials(user, pass)); + httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build(); + } else { + httpClient = clientBuilder.build(); + } // Run the request (the http client handles the multiple back-and-forth requests) try { httpUriRequest = getSignedUrl(httpUriRequest); @@ -391,7 +434,7 @@ public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout } throw new TrustedHttpClientException(e); } - } else { + } else if (enableDigest) { final CloseableHttpClient httpClient = clientBuilder.build(); // HttpClient doesn't handle the request dynamics for other verbs (especially when sending a streamed multipart // request), so we need to handle the details of the digest auth back-and-forth manually @@ -418,6 +461,21 @@ public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout } throw new TrustedHttpClientException(e); } + } else { + final CloseableHttpClient httpClient = clientBuilder.build(); + HttpResponse response = null; + try { + response = httpClient.execute(httpUriRequest); + return response; + } catch (Exception e) { + // close the http connection(s) + try { + httpClient.close(); + } catch (IOException ioException) { + throw new TrustedHttpClientException(e); + } + throw new TrustedHttpClientException(e); + } } } @@ -641,4 +699,63 @@ public int getRetryMaximumVariableTime() { return retryMaximumVariableTime; } + /** + * Very simple cache that does a <em>complete</em> refresh after a given interval. This type of cache is only suitable + * for small sets. + */ + private static final class HostCache { + private final Object lock = new Object(); + + // A simple hash map is sufficient here. + // No need to deal with soft references or an LRU map since the number of organizations + // will be quite low. + private final Set<String> hosts = set(); + private final long refreshInterval; + private long lastRefresh; + + private final OrganizationDirectoryService organizationDirectoryService; + private final ServiceRegistry serviceRegistry; + + HostCache(long refreshInterval, OrganizationDirectoryService orgDirSrv, ServiceRegistry serviceReg) { + this.refreshInterval = refreshInterval; + this.organizationDirectoryService = orgDirSrv; + this.serviceRegistry = serviceReg; + invalidate(); + } + + public boolean contains(String host) { + synchronized (lock) { + try { + refresh(); + } catch (ServiceRegistryException e) { + logger.error("Unable to update host cache due to service registry exception!", e); + } + return hosts.contains(host); + } + } + public void invalidate() { + this.lastRefresh = System.currentTimeMillis() - 2 * refreshInterval; + } + + private void refresh() throws ServiceRegistryException { + final long now = System.currentTimeMillis(); + if (now - lastRefresh > refreshInterval) { + hosts.clear(); + //The hosts from the SR come back as protocol://hostname:port, use the URI class to get just the host + hosts.addAll(serviceRegistry.getHostRegistrations().stream() + .map(HostRegistration::getBaseUrl) + .map(URI::create) + .map(URI::getHost) + .collect(Collectors.toSet())); + //This is the *org's* server urls, as defined by in the org config with + // something like prop.org.opencastproject.host.$SERVER_URL=$TENANT_URL + // You're getting just the hostname of the tenant URL + hosts.addAll(organizationDirectoryService.getOrganizations().stream() + .map(Organization::getServers) + .map(Map::keySet) + .flatMap(Collection::stream).collect(Collectors.toSet())); + lastRefresh = now; + } + } + } }
modules/kernel/src/test/java/org/opencastproject/kernel/security/TrustedHttpClientImplTest.java+120 −1 modified@@ -28,15 +28,20 @@ import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.opencastproject.job.api.Job; import org.opencastproject.security.api.DefaultOrganization; import org.opencastproject.security.api.JaxbUser; +import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClientException; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; +import org.opencastproject.serviceregistry.api.HostRegistration; +import org.opencastproject.serviceregistry.api.HostRegistrationInMemory; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.apache.http.Header; @@ -53,6 +58,7 @@ import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; import org.easymock.Capture; +import org.easymock.CaptureType; import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Before; @@ -61,6 +67,11 @@ import org.osgi.service.component.ComponentContext; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class TrustedHttpClientImplTest { @@ -72,6 +83,7 @@ public class TrustedHttpClientImplTest { private CloseableHttpResponse nonceResponse; private ServiceRegistry serviceRegistry; private SecurityService securityService; + private OrganizationDirectoryService organizationDirectoryService; @Before public void setUp() throws Exception { @@ -93,19 +105,36 @@ public void setUp() throws Exception { expect(currentJob.getId()).andReturn(currentJobId).anyTimes(); replay(currentJob); + DefaultOrganization defaultOrganization = new DefaultOrganization(); + securityService = createNiceMock(SecurityService.class); - expect(securityService.getOrganization()).andReturn(new DefaultOrganization()).anyTimes(); + expect(securityService.getOrganization()).andReturn(defaultOrganization).anyTimes(); expect(securityService.getUser()).andReturn(new JaxbUser()).anyTimes(); replay(securityService); + organizationDirectoryService = createNiceMock(OrganizationDirectoryService.class); + expect(organizationDirectoryService.getOrganizations()).andReturn(List.of(defaultOrganization)); + replay(organizationDirectoryService); + + HostRegistration localhost = new HostRegistrationInMemory("http://localhost:8080", "127.0.0.1", + "admin", 4.0f, 2, 4194304); + HostRegistration admin = new HostRegistrationInMemory("http://admin.example.org", "127.0.0.1", + "admin", 4.0f, 2, 4194304); + HostRegistration worker = new HostRegistrationInMemory("http://worker.example.org", "127.0.0.1", + "worker", 4.0f, 4, 2097152); + HostRegistration pres = new HostRegistrationInMemory("http://pres.example.org", "127.0.0.1", + "worker", 4.0f, 4, 2097152); + serviceRegistry = createNiceMock(ServiceRegistry.class); expect(serviceRegistry.getCurrentJob()).andReturn(currentJob).anyTimes(); expect(serviceRegistry.getJob(currentJobId)).andReturn(currentJob).anyTimes(); + expect(serviceRegistry.getHostRegistrations()).andReturn(List.of(localhost, admin, worker, pres)).anyTimes(); replay(serviceRegistry); client = new TrustedHttpClientImpl("u", "p"); client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); // Setup responses. final String digestValue = "Digest realm=\"testrealm@host.com\"," @@ -337,6 +366,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -383,6 +413,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -428,6 +459,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -474,6 +506,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -527,6 +560,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -571,6 +605,8 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket } }; client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); + client.setServiceRegistry(serviceRegistry); client.setUrlSigningService(urlSigningService); client.activate(componentContextMock); @@ -606,6 +642,8 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket } }; client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); + client.setServiceRegistry(serviceRegistry); client.setUrlSigningService(urlSigningService); client.execute(headRequest); @@ -638,4 +676,85 @@ public void testGetSignedUrl() throws IOException, UrlSigningException { assertEquals(client.getSignedUrl(notGetOrHead), notGetOrHead); assertEquals(client.getSignedUrl(ok).getURI().toString(), signedOk); } + + @Test + public void testClusterDetection() throws IOException { + + Map<String, Integer> servers = new HashMap<>(); + servers.put("tenant-admin.example.org", 8080); + servers.put("tenant-pres.example.org", 8080); + servers = Collections.unmodifiableMap(servers); + Organization fakeOrg = new DefaultOrganization(servers); + + SecurityService secSvc = createNiceMock(SecurityService.class); + expect(secSvc.getOrganization()).andReturn(fakeOrg).anyTimes(); + expect(secSvc.getUser()).andReturn(new JaxbUser()).anyTimes(); + replay(secSvc); + + OrganizationDirectoryService orgDirSvc = createNiceMock(OrganizationDirectoryService.class); + expect(orgDirSvc.getOrganizations()).andReturn(List.of(fakeOrg)).anyTimes(); + replay(orgDirSvc); + + // Setup signing service + UrlSigningService urlSigningService = EasyMock.createNiceMock(UrlSigningService.class); + EasyMock.expect(urlSigningService.accepts(EasyMock.anyString())).andReturn(false).anyTimes(); + EasyMock.replay(urlSigningService); + + Capture<HttpUriRequest> request = EasyMock.newCapture(CaptureType.ALL); + + // Setup Http Client + CloseableHttpClient httpClient = createNiceMock(CloseableHttpClient.class); + expect(httpClient.execute(EasyMock.capture(request))).andReturn(okResponse).anyTimes(); + httpClient.close(); + EasyMock.expectLastCall().anyTimes(); + + HttpClientBuilder httpClientBuilder = createNiceMock(HttpClientBuilder.class); + expect(httpClientBuilder.build()).andReturn(httpClient).anyTimes(); + + replay(httpClientBuilder, httpClient); + + client = new TrustedHttpClientImpl("u", "p") { + @Override + public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socketTimeout) { + return httpClientBuilder; + } + }; + client.setSecurityService(secSvc); + client.setServiceRegistry(serviceRegistry); + client.setOrganizationDirectoryService(orgDirSvc); + client.setUrlSigningService(urlSigningService); + + //Reminder: the valid nodes for the current org are: + // - http://localhost:80 + // - http://admin:8080 + //Note that we are *not* checking that the auth credentials are correct. That's a final method, which won't mock. + //Instead we check if the desire to use Digest auth is present. That's close enough. + client.execute(new HttpGet("http://localhost:8080/info/me.json")); + //This isn't part of the cluster at all, don't send auth + client.execute(new HttpGet("http://www.example.org")); + //This is the admin node, but https rather than http as defined in the org. Safe to send credentials, *but* + // the host cache in TrustedHttpClientImpl uses Organization::getServers, which returns a string like http://admin + // then compares the URI passed in to that string. Thus, while we *could* send the auth, we don't since http!=https + client.execute(new HttpGet("https://admin.example.org")); + //This is the admin node, but on the wrong port (80 vs 8080). Still safe to send credentials. + //The behaviour here is different than the above because same cache as above discards the port defined by the server + //This makes sense when you think of it - the *host* is the important part, not the port or protocol. + client.execute(new HttpGet("http://admin.example.org")); + //Similar to the admin node, but *not*. Don't send credentials. + client.execute(new HttpGet("http://tenant-admin.example.org")); + client.execute(new HttpGet("https://tenant-pres.example.org")); + + assertTrue(Arrays.stream(request.getValues().get(0).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertFalse(Arrays.stream(request.getValues().get(1).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(2).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(3).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(4).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(5).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + } }
modules/kernel/src/test/java/org/opencastproject/kernel/security/TrustedHttpClientResourceClosingTest.java+15 −1 modified@@ -23,9 +23,12 @@ import static org.junit.Assert.assertEquals; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; +import org.opencastproject.serviceregistry.api.ServiceRegistry; +import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; @@ -38,6 +41,7 @@ import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; +import java.util.LinkedList; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; @@ -49,7 +53,7 @@ public class TrustedHttpClientResourceClosingTest { private static final int PORT = 8952; private static final class TestHttpClient extends TrustedHttpClientImpl { - TestHttpClient() throws UrlSigningException { + TestHttpClient() throws UrlSigningException, ServiceRegistryException { super("user", "pass"); setSecurityService(EasyMock.createNiceMock(SecurityService.class)); // Setup signing service @@ -60,6 +64,16 @@ private static final class TestHttpClient extends TrustedHttpClientImpl { .andReturn("http://127.0.0.1:" + PORT); EasyMock.replay(urlSigningService); + ServiceRegistry serviceRegistry = EasyMock.createNiceMock(ServiceRegistry.class); + EasyMock.expect(serviceRegistry.getHostRegistrations()).andReturn(new LinkedList<>()); + EasyMock.replay(serviceRegistry); + setServiceRegistry(serviceRegistry); + + OrganizationDirectoryService orgDirSvc = EasyMock.createMock(OrganizationDirectoryService.class); + EasyMock.expect(orgDirSvc.getOrganizations()).andReturn(new LinkedList<>()); + EasyMock.replay(orgDirSvc); + setOrganizationDirectoryService(orgDirSvc); + setUrlSigningService(urlSigningService); }
modules/publication-service-oaipmh-remote/src/test/java/org/opencastproject/publication/oaipmh/endpoint/OaiPmhPublicationRestServiceTest.java+18 −2 modified@@ -31,8 +31,13 @@ import org.opencastproject.mediapackage.MediaPackageBuilderFactory; import org.opencastproject.mediapackage.MediaPackageParser; import org.opencastproject.publication.oaipmh.remote.OaiPmhPublicationServiceRemoteImpl; +import org.opencastproject.security.api.DefaultOrganization; +import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClientException; +import org.opencastproject.serviceregistry.api.HostRegistration; +import org.opencastproject.serviceregistry.api.HostRegistrationInMemory; import org.opencastproject.serviceregistry.api.ServiceRegistration; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.test.rest.RestServiceTestEnv; @@ -47,6 +52,7 @@ import java.net.URI; import java.util.HashSet; +import java.util.List; /** * These tests are tightly coupled to {@link TestOaiPmhPublicationRestService}. @@ -76,6 +82,10 @@ public void testPublishUsingRemoteService() throws Exception { mp.addCreator(CREATOR); // final ServiceRegistry registry = EasyMock.createNiceMock(ServiceRegistry.class); + List<HostRegistration> hostRegistrations = List.of( + new HostRegistrationInMemory("http://localhost", "127.0.0.1", "Fake", + 1.0f, 1, 1024)); + EasyMock.expect(registry.getHostRegistrations()).andReturn(hostRegistrations).anyTimes(); final ServiceRegistration registration = EasyMock.createNiceMock(ServiceRegistration.class); EasyMock.expect(registration.getHost()) .andReturn(rt.host("")) @@ -86,7 +96,7 @@ public void testPublishUsingRemoteService() throws Exception { .anyTimes(); EasyMock.replay(registry, registration); final OaiPmhPublicationServiceRemoteImpl remote = new OaiPmhPublicationServiceRemoteImpl(); - remote.setTrustedHttpClient(new TestHttpClient()); + remote.setTrustedHttpClient(new TestHttpClient(registry)); remote.setRemoteServiceManager(registry); // final Job job = remote.publish(mp, "mmp", new HashSet<String>(), new HashSet<String>(), false); @@ -98,9 +108,15 @@ public void testPublishUsingRemoteService() throws Exception { // private static final class TestHttpClient extends TrustedHttpClientImpl { - TestHttpClient() { + TestHttpClient(ServiceRegistry registry) { super("user", "pass"); setSecurityService(EasyMock.createNiceMock(SecurityService.class)); + OrganizationDirectoryService orgDirSvc = EasyMock.createMock(OrganizationDirectoryService.class); + Organization org = new DefaultOrganization(); + EasyMock.expect(orgDirSvc.getOrganizations()).andReturn(List.of(org)); + EasyMock.replay(orgDirSvc); + setOrganizationDirectoryService(orgDirSvc); + setServiceRegistry(registry); } /**
e89804353421Checking that a node is listed in the service registry prior to sending any credentials
7 files changed · +304 −56
modules/common/src/main/java/org/opencastproject/security/api/DefaultOrganization.java+7 −0 modified@@ -68,4 +68,11 @@ public DefaultOrganization() { DEFAULT_PROPERTIES); } + public DefaultOrganization(Map<String, Integer> override) { + super(DefaultOrganization.DEFAULT_ORGANIZATION_ID, DefaultOrganization.DEFAULT_ORGANIZATION_NAME, + null != override ? override : DEFAULT_SERVERS, + DefaultOrganization.DEFAULT_ORGANIZATION_ADMIN, DefaultOrganization.DEFAULT_ORGANIZATION_ANONYMOUS, + DEFAULT_PROPERTIES); + } + }
modules/ingest-service-impl/src/main/java/org/opencastproject/ingest/impl/IngestServiceImpl.java+2 −12 modified@@ -1704,7 +1704,6 @@ protected URI addContentToRepo(MediaPackage mp, String elementId, URI uri) throw try { if (uri.toString().startsWith("http")) { HttpGet get = new HttpGet(uri); - var clusterUrls = securityService.getOrganization().getServers().keySet(); if (!isBlank(downloadSource) && uri.toString().matches(downloadSource)) { // NB: We're creating a new client here with *different* auth than the system auth creds @@ -1717,13 +1716,9 @@ protected URI addContentToRepo(MediaPackage mp, String elementId, URI uri) throw get.setHeader(HttpHeaders.AUTHORIZATION, authHeader); } response = externalHttpClient.execute(get); - } else if (clusterUrls.contains(uri.getScheme() + "://" + uri.getHost())) { - // Only using the system-level httpclient and digest credentials against our own servers - response = httpClient.execute(get); } else { - //NB: No auth here at all - externalHttpClient = getNoAuthHttpClient(); - response = externalHttpClient.execute(get); + // httpClient checks internally to see if it should be sending the default auth, or not. + response = httpClient.execute(get); } if (null == response) { @@ -1917,11 +1912,6 @@ protected OrganizationDirectoryService getOrganizationDirectoryService() { return organizationDirectoryService; } - //Used in testing - protected CloseableHttpClient getNoAuthHttpClient() { - return HttpClientBuilder.create().build(); - } - protected CloseableHttpClient getAuthedHttpClient() { HttpClientBuilder cb = HttpClientBuilder.create(); CredentialsProvider provider = new BasicCredentialsProvider();
modules/ingest-service-impl/src/test/java/org/opencastproject/ingest/impl/IngestServiceImplTest.java+14 −29 modified@@ -73,6 +73,7 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -108,8 +109,8 @@ public class IngestServiceImplTest { private WorkflowInstance workflowInstance = null; private WorkingFileRepository wfr = null; private CloseableHttpResponse httpResponse = null; - private CloseableHttpClient credClient = null; - private CloseableHttpClient noCredClient = null; + private CloseableHttpClient customClient = null; + private TrustedHttpClient httpClient; private static URI baseDir; private static URI urlTrack; private static URI urlTrack1; @@ -273,7 +274,7 @@ private void setupService() throws Exception { EasyMock.expect(httpResponse.getEntity()).andReturn(entity).anyTimes(); EasyMock.replay(httpResponse); - TrustedHttpClient httpClient = EasyMock.createNiceMock(TrustedHttpClient.class); + httpClient = EasyMock.createNiceMock(TrustedHttpClient.class); EasyMock.expect(httpClient.execute((HttpGet) EasyMock.anyObject())).andReturn(httpResponse).anyTimes(); EasyMock.replay(httpClient); @@ -315,17 +316,6 @@ protected CloseableHttpClient getAuthedHttpClient() { EasyMock.replay(client); return client; } - @Override - protected CloseableHttpClient getNoAuthHttpClient() { - CloseableHttpClient client = EasyMock.createMock(CloseableHttpClient.class); - try { - EasyMock.expect(client.execute((HttpGet) EasyMock.anyObject())).andReturn(httpResponse).anyTimes(); - client.close(); - EasyMock.expectLastCall().once(); - } catch (Exception e) { } - EasyMock.replay(client); - return client; - } }; } service.setHttpClient(httpClient); @@ -434,33 +424,28 @@ public void testContentDisposition() throws Exception { } private void testAuthWhitelist(String url, String regex, boolean shouldFail, boolean shouldSendAuth, boolean shouldTouchMocks) throws Exception { - credClient = EasyMock.createNiceMock(CloseableHttpClient.class); - noCredClient = EasyMock.createNiceMock(CloseableHttpClient.class); + customClient = EasyMock.createNiceMock(CloseableHttpClient.class); + EasyMock.reset(httpClient); //There's one case (accessing the filesystem) where we *don't* expect the mocks to be used if (shouldTouchMocks) { if (shouldSendAuth) { - EasyMock.expect(credClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); - credClient.close(); + EasyMock.expect(customClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); + customClient.close(); EasyMock.expectLastCall().once(); } else { - EasyMock.expect(noCredClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); - noCredClient.close(); + EasyMock.expect(httpClient.execute(EasyMock.anyObject())).andReturn(httpResponse).once(); + httpClient.close(EasyMock.anyObject(HttpResponse.class)); EasyMock.expectLastCall().once(); } } - EasyMock.replay(noCredClient, credClient); + EasyMock.replay(httpClient, customClient); //Recreate the service so we use our own, custom mocks service = new IngestServiceImpl() { @Override protected CloseableHttpClient getAuthedHttpClient() { - return credClient; - } - - @Override - protected CloseableHttpClient getNoAuthHttpClient() { - return noCredClient; + return customClient; } }; setupService(); @@ -481,8 +466,8 @@ protected CloseableHttpClient getNoAuthHttpClient() { Assert.fail("Should not have failed!"); } } - EasyMock.verify(credClient, noCredClient); - EasyMock.reset(credClient, noCredClient); + EasyMock.verify(customClient, httpClient); + EasyMock.reset(customClient, httpClient); } @Test
modules/kernel/src/main/java/org/opencastproject/kernel/security/TrustedHttpClientImpl.java+128 −11 modified@@ -24,8 +24,10 @@ import static org.opencastproject.kernel.rest.CurrentJobFilter.CURRENT_JOB_HEADER; import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.DIGEST_AUTH; import static org.opencastproject.kernel.security.DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER; +import static org.opencastproject.util.data.Collections.set; import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityConstants; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClient; @@ -34,7 +36,9 @@ import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; import org.opencastproject.security.util.HttpResponseWrapper; +import org.opencastproject.serviceregistry.api.HostRegistration; import org.opencastproject.serviceregistry.api.ServiceRegistry; +import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.urlsigning.utils.ResourceRequestUtil; import org.apache.commons.lang3.StringUtils; @@ -68,9 +72,13 @@ import java.io.IOException; import java.lang.management.ManagementFactory; +import java.net.URI; +import java.util.Collection; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import javax.management.MBeanServer; import javax.management.ObjectName; @@ -172,9 +180,16 @@ public class TrustedHttpClientImpl implements TrustedHttpClient, HttpConnectionM /** The security service */ protected SecurityService securityService = null; + /** The organization directory service */ + protected OrganizationDirectoryService organizationDirectoryService = null; + /** The url signing service */ protected UrlSigningService urlSigningService = null; + /** A regularly emptying cache of hosts in the cluster */ + private HostCache hosts = null; + + @Activate public void activate(ComponentContext cc) { logger.debug("activate"); @@ -210,6 +225,13 @@ public void activate(ComponentContext cc) { logger.debug("Expire signed URLs in {} seconds.", signedUrlExpiresDuration); } + private HostCache getHostCache() { + if (null == hosts) { + hosts = new HostCache(60000, this.organizationDirectoryService, this.serviceRegistry); + } + return hosts; + } + /** * Sets the service registry. * @@ -245,6 +267,17 @@ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } + /** + * Sets the organization directory service. + * + * @param organizationDirectoryService + * the organization directory service + */ + @Reference + public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectoryService) { + this.organizationDirectoryService = organizationDirectoryService; + } + /** * Sets the url signing service. * @@ -323,6 +356,7 @@ public void deactivate() { } public TrustedHttpClientImpl() { + } public TrustedHttpClientImpl(String user, String pass) { @@ -351,32 +385,41 @@ public HttpResponse execute(HttpUriRequest httpUriRequest) throws TrustedHttpCli @Override public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout, int socketTimeout) throws TrustedHttpClientException { - // Add the request header to elicit a digest auth response - httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH); - if (serviceRegistry != null && serviceRegistry.getCurrentJob() != null) { httpUriRequest.setHeader(CURRENT_JOB_HEADER, Long.toString(serviceRegistry.getCurrentJob().getId())); } + boolean enableDigest = getHostCache().contains(httpUriRequest.getURI().getHost()); + logger.debug("Digest auth enabled for this request: " + enableDigest); + if (enableDigest) { + // Add the request header to elicit a digest auth response + httpUriRequest.setHeader(REQUESTED_AUTH_HEADER, DIGEST_AUTH); + } + // If a security service has been set, use it to pass the current security context on logger.debug("Adding security context to request"); final Organization organization = securityService.getOrganization(); if (organization != null) { httpUriRequest.setHeader(SecurityConstants.ORGANIZATION_HEADER, organization.getId()); final User currentUser = securityService.getUser(); - if (currentUser != null) { + if (enableDigest && currentUser != null) { httpUriRequest.setHeader(SecurityConstants.USER_HEADER, currentUser.getUsername()); } } final HttpClientBuilder clientBuilder = makeHttpClientBuilder(connectionTimeout, socketTimeout); if ("GET".equalsIgnoreCase(httpUriRequest.getMethod()) || "HEAD".equalsIgnoreCase(httpUriRequest.getMethod())) { - // Set the user/pass - CredentialsProvider provider = new BasicCredentialsProvider(); - provider.setCredentials( - new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST), - new UsernamePasswordCredentials(user, pass)); - final CloseableHttpClient httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build(); + final CloseableHttpClient httpClient; + if (enableDigest) { + // Set the user/pass + CredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials( + new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthSchemes.DIGEST), + new UsernamePasswordCredentials(user, pass)); + httpClient = clientBuilder.setDefaultCredentialsProvider(provider).build(); + } else { + httpClient = clientBuilder.build(); + } // Run the request (the http client handles the multiple back-and-forth requests) try { httpUriRequest = getSignedUrl(httpUriRequest); @@ -391,7 +434,7 @@ public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout } throw new TrustedHttpClientException(e); } - } else { + } else if (enableDigest) { final CloseableHttpClient httpClient = clientBuilder.build(); // HttpClient doesn't handle the request dynamics for other verbs (especially when sending a streamed multipart // request), so we need to handle the details of the digest auth back-and-forth manually @@ -418,6 +461,21 @@ public HttpResponse execute(HttpUriRequest httpUriRequest, int connectionTimeout } throw new TrustedHttpClientException(e); } + } else { + final CloseableHttpClient httpClient = clientBuilder.build(); + HttpResponse response = null; + try { + response = httpClient.execute(httpUriRequest); + return response; + } catch (Exception e) { + // close the http connection(s) + try { + httpClient.close(); + } catch (IOException ioException) { + throw new TrustedHttpClientException(e); + } + throw new TrustedHttpClientException(e); + } } } @@ -641,4 +699,63 @@ public int getRetryMaximumVariableTime() { return retryMaximumVariableTime; } + /** + * Very simple cache that does a <em>complete</em> refresh after a given interval. This type of cache is only suitable + * for small sets. + */ + private static final class HostCache { + private final Object lock = new Object(); + + // A simple hash map is sufficient here. + // No need to deal with soft references or an LRU map since the number of organizations + // will be quite low. + private final Set<String> hosts = set(); + private final long refreshInterval; + private long lastRefresh; + + private final OrganizationDirectoryService organizationDirectoryService; + private final ServiceRegistry serviceRegistry; + + HostCache(long refreshInterval, OrganizationDirectoryService orgDirSrv, ServiceRegistry serviceReg) { + this.refreshInterval = refreshInterval; + this.organizationDirectoryService = orgDirSrv; + this.serviceRegistry = serviceReg; + invalidate(); + } + + public boolean contains(String host) { + synchronized (lock) { + try { + refresh(); + } catch (ServiceRegistryException e) { + logger.error("Unable to update host cache due to service registry exception!", e); + } + return hosts.contains(host); + } + } + public void invalidate() { + this.lastRefresh = System.currentTimeMillis() - 2 * refreshInterval; + } + + private void refresh() throws ServiceRegistryException { + final long now = System.currentTimeMillis(); + if (now - lastRefresh > refreshInterval) { + hosts.clear(); + //The hosts from the SR come back as protocol://hostname:port, use the URI class to get just the host + hosts.addAll(serviceRegistry.getHostRegistrations().stream() + .map(HostRegistration::getBaseUrl) + .map(URI::create) + .map(URI::getHost) + .collect(Collectors.toSet())); + //This is the *org's* server urls, as defined by in the org config with + // something like prop.org.opencastproject.host.$SERVER_URL=$TENANT_URL + // You're getting just the hostname of the tenant URL + hosts.addAll(organizationDirectoryService.getOrganizations().stream() + .map(Organization::getServers) + .map(Map::keySet) + .flatMap(Collection::stream).collect(Collectors.toSet())); + lastRefresh = now; + } + } + } }
modules/kernel/src/test/java/org/opencastproject/kernel/security/TrustedHttpClientImplTest.java+120 −1 modified@@ -28,15 +28,20 @@ import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import org.opencastproject.job.api.Job; import org.opencastproject.security.api.DefaultOrganization; import org.opencastproject.security.api.JaxbUser; +import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClientException; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; +import org.opencastproject.serviceregistry.api.HostRegistration; +import org.opencastproject.serviceregistry.api.HostRegistrationInMemory; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.apache.http.Header; @@ -53,6 +58,7 @@ import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; import org.easymock.Capture; +import org.easymock.CaptureType; import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Before; @@ -61,6 +67,11 @@ import org.osgi.service.component.ComponentContext; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class TrustedHttpClientImplTest { @@ -72,6 +83,7 @@ public class TrustedHttpClientImplTest { private CloseableHttpResponse nonceResponse; private ServiceRegistry serviceRegistry; private SecurityService securityService; + private OrganizationDirectoryService organizationDirectoryService; @Before public void setUp() throws Exception { @@ -93,19 +105,36 @@ public void setUp() throws Exception { expect(currentJob.getId()).andReturn(currentJobId).anyTimes(); replay(currentJob); + DefaultOrganization defaultOrganization = new DefaultOrganization(); + securityService = createNiceMock(SecurityService.class); - expect(securityService.getOrganization()).andReturn(new DefaultOrganization()).anyTimes(); + expect(securityService.getOrganization()).andReturn(defaultOrganization).anyTimes(); expect(securityService.getUser()).andReturn(new JaxbUser()).anyTimes(); replay(securityService); + organizationDirectoryService = createNiceMock(OrganizationDirectoryService.class); + expect(organizationDirectoryService.getOrganizations()).andReturn(List.of(defaultOrganization)); + replay(organizationDirectoryService); + + HostRegistration localhost = new HostRegistrationInMemory("http://localhost:8080", "127.0.0.1", + "admin", 4.0f, 2, 4194304); + HostRegistration admin = new HostRegistrationInMemory("http://admin.example.org", "127.0.0.1", + "admin", 4.0f, 2, 4194304); + HostRegistration worker = new HostRegistrationInMemory("http://worker.example.org", "127.0.0.1", + "worker", 4.0f, 4, 2097152); + HostRegistration pres = new HostRegistrationInMemory("http://pres.example.org", "127.0.0.1", + "worker", 4.0f, 4, 2097152); + serviceRegistry = createNiceMock(ServiceRegistry.class); expect(serviceRegistry.getCurrentJob()).andReturn(currentJob).anyTimes(); expect(serviceRegistry.getJob(currentJobId)).andReturn(currentJob).anyTimes(); + expect(serviceRegistry.getHostRegistrations()).andReturn(List.of(localhost, admin, worker, pres)).anyTimes(); replay(serviceRegistry); client = new TrustedHttpClientImpl("u", "p"); client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); // Setup responses. final String digestValue = "Digest realm=\"testrealm@host.com\"," @@ -337,6 +366,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -383,6 +413,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -428,6 +459,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -474,6 +506,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -527,6 +560,7 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket }; client.setServiceRegistry(serviceRegistry); client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); client.activate(componentContextMock); HttpPost httpPost = new HttpPost("http://localhost:8080/fake"); @@ -571,6 +605,8 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket } }; client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); + client.setServiceRegistry(serviceRegistry); client.setUrlSigningService(urlSigningService); client.activate(componentContextMock); @@ -606,6 +642,8 @@ public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socket } }; client.setSecurityService(securityService); + client.setOrganizationDirectoryService(organizationDirectoryService); + client.setServiceRegistry(serviceRegistry); client.setUrlSigningService(urlSigningService); client.execute(headRequest); @@ -638,4 +676,85 @@ public void testGetSignedUrl() throws IOException, UrlSigningException { assertEquals(client.getSignedUrl(notGetOrHead), notGetOrHead); assertEquals(client.getSignedUrl(ok).getURI().toString(), signedOk); } + + @Test + public void testClusterDetection() throws IOException { + + Map<String, Integer> servers = new HashMap<>(); + servers.put("tenant-admin.example.org", 8080); + servers.put("tenant-pres.example.org", 8080); + servers = Collections.unmodifiableMap(servers); + Organization fakeOrg = new DefaultOrganization(servers); + + SecurityService secSvc = createNiceMock(SecurityService.class); + expect(secSvc.getOrganization()).andReturn(fakeOrg).anyTimes(); + expect(secSvc.getUser()).andReturn(new JaxbUser()).anyTimes(); + replay(secSvc); + + OrganizationDirectoryService orgDirSvc = createNiceMock(OrganizationDirectoryService.class); + expect(orgDirSvc.getOrganizations()).andReturn(List.of(fakeOrg)).anyTimes(); + replay(orgDirSvc); + + // Setup signing service + UrlSigningService urlSigningService = EasyMock.createNiceMock(UrlSigningService.class); + EasyMock.expect(urlSigningService.accepts(EasyMock.anyString())).andReturn(false).anyTimes(); + EasyMock.replay(urlSigningService); + + Capture<HttpUriRequest> request = EasyMock.newCapture(CaptureType.ALL); + + // Setup Http Client + CloseableHttpClient httpClient = createNiceMock(CloseableHttpClient.class); + expect(httpClient.execute(EasyMock.capture(request))).andReturn(okResponse).anyTimes(); + httpClient.close(); + EasyMock.expectLastCall().anyTimes(); + + HttpClientBuilder httpClientBuilder = createNiceMock(HttpClientBuilder.class); + expect(httpClientBuilder.build()).andReturn(httpClient).anyTimes(); + + replay(httpClientBuilder, httpClient); + + client = new TrustedHttpClientImpl("u", "p") { + @Override + public HttpClientBuilder makeHttpClientBuilder(int connectionTimeout, int socketTimeout) { + return httpClientBuilder; + } + }; + client.setSecurityService(secSvc); + client.setServiceRegistry(serviceRegistry); + client.setOrganizationDirectoryService(orgDirSvc); + client.setUrlSigningService(urlSigningService); + + //Reminder: the valid nodes for the current org are: + // - http://localhost:80 + // - http://admin:8080 + //Note that we are *not* checking that the auth credentials are correct. That's a final method, which won't mock. + //Instead we check if the desire to use Digest auth is present. That's close enough. + client.execute(new HttpGet("http://localhost:8080/info/me.json")); + //This isn't part of the cluster at all, don't send auth + client.execute(new HttpGet("http://www.example.org")); + //This is the admin node, but https rather than http as defined in the org. Safe to send credentials, *but* + // the host cache in TrustedHttpClientImpl uses Organization::getServers, which returns a string like http://admin + // then compares the URI passed in to that string. Thus, while we *could* send the auth, we don't since http!=https + client.execute(new HttpGet("https://admin.example.org")); + //This is the admin node, but on the wrong port (80 vs 8080). Still safe to send credentials. + //The behaviour here is different than the above because same cache as above discards the port defined by the server + //This makes sense when you think of it - the *host* is the important part, not the port or protocol. + client.execute(new HttpGet("http://admin.example.org")); + //Similar to the admin node, but *not*. Don't send credentials. + client.execute(new HttpGet("http://tenant-admin.example.org")); + client.execute(new HttpGet("https://tenant-pres.example.org")); + + assertTrue(Arrays.stream(request.getValues().get(0).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertFalse(Arrays.stream(request.getValues().get(1).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(2).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(3).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(4).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + assertTrue(Arrays.stream(request.getValues().get(5).getHeaders(DelegatingAuthenticationEntryPoint.REQUESTED_AUTH_HEADER)) + .anyMatch(header -> DelegatingAuthenticationEntryPoint.DIGEST_AUTH.equals(header.getValue()))); + } }
modules/kernel/src/test/java/org/opencastproject/kernel/security/TrustedHttpClientResourceClosingTest.java+15 −1 modified@@ -23,9 +23,12 @@ import static org.junit.Assert.assertEquals; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.urlsigning.exception.UrlSigningException; import org.opencastproject.security.urlsigning.service.UrlSigningService; +import org.opencastproject.serviceregistry.api.ServiceRegistry; +import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; @@ -38,6 +41,7 @@ import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; +import java.util.LinkedList; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; @@ -49,7 +53,7 @@ public class TrustedHttpClientResourceClosingTest { private static final int PORT = 8952; private static final class TestHttpClient extends TrustedHttpClientImpl { - TestHttpClient() throws UrlSigningException { + TestHttpClient() throws UrlSigningException, ServiceRegistryException { super("user", "pass"); setSecurityService(EasyMock.createNiceMock(SecurityService.class)); // Setup signing service @@ -60,6 +64,16 @@ private static final class TestHttpClient extends TrustedHttpClientImpl { .andReturn("http://127.0.0.1:" + PORT); EasyMock.replay(urlSigningService); + ServiceRegistry serviceRegistry = EasyMock.createNiceMock(ServiceRegistry.class); + EasyMock.expect(serviceRegistry.getHostRegistrations()).andReturn(new LinkedList<>()); + EasyMock.replay(serviceRegistry); + setServiceRegistry(serviceRegistry); + + OrganizationDirectoryService orgDirSvc = EasyMock.createMock(OrganizationDirectoryService.class); + EasyMock.expect(orgDirSvc.getOrganizations()).andReturn(new LinkedList<>()); + EasyMock.replay(orgDirSvc); + setOrganizationDirectoryService(orgDirSvc); + setUrlSigningService(urlSigningService); }
modules/publication-service-oaipmh-remote/src/test/java/org/opencastproject/publication/oaipmh/endpoint/OaiPmhPublicationRestServiceTest.java+18 −2 modified@@ -31,8 +31,13 @@ import org.opencastproject.mediapackage.MediaPackageBuilderFactory; import org.opencastproject.mediapackage.MediaPackageParser; import org.opencastproject.publication.oaipmh.remote.OaiPmhPublicationServiceRemoteImpl; +import org.opencastproject.security.api.DefaultOrganization; +import org.opencastproject.security.api.Organization; +import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.TrustedHttpClientException; +import org.opencastproject.serviceregistry.api.HostRegistration; +import org.opencastproject.serviceregistry.api.HostRegistrationInMemory; import org.opencastproject.serviceregistry.api.ServiceRegistration; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.test.rest.RestServiceTestEnv; @@ -47,6 +52,7 @@ import java.net.URI; import java.util.HashSet; +import java.util.List; /** * These tests are tightly coupled to {@link TestOaiPmhPublicationRestService}. @@ -76,6 +82,10 @@ public void testPublishUsingRemoteService() throws Exception { mp.addCreator(CREATOR); // final ServiceRegistry registry = EasyMock.createNiceMock(ServiceRegistry.class); + List<HostRegistration> hostRegistrations = List.of( + new HostRegistrationInMemory("http://localhost", "127.0.0.1", "Fake", + 1.0f, 1, 1024)); + EasyMock.expect(registry.getHostRegistrations()).andReturn(hostRegistrations).anyTimes(); final ServiceRegistration registration = EasyMock.createNiceMock(ServiceRegistration.class); EasyMock.expect(registration.getHost()) .andReturn(rt.host("")) @@ -86,7 +96,7 @@ public void testPublishUsingRemoteService() throws Exception { .anyTimes(); EasyMock.replay(registry, registration); final OaiPmhPublicationServiceRemoteImpl remote = new OaiPmhPublicationServiceRemoteImpl(); - remote.setTrustedHttpClient(new TestHttpClient()); + remote.setTrustedHttpClient(new TestHttpClient(registry)); remote.setRemoteServiceManager(registry); // final Job job = remote.publish(mp, "mmp", new HashSet<String>(), new HashSet<String>(), false); @@ -98,9 +108,15 @@ public void testPublishUsingRemoteService() throws Exception { // private static final class TestHttpClient extends TrustedHttpClientImpl { - TestHttpClient() { + TestHttpClient(ServiceRegistry registry) { super("user", "pass"); setSecurityService(EasyMock.createNiceMock(SecurityService.class)); + OrganizationDirectoryService orgDirSvc = EasyMock.createMock(OrganizationDirectoryService.class); + Organization org = new DefaultOrganization(); + EasyMock.expect(orgDirSvc.getOrganizations()).andReturn(List.of(org)); + EasyMock.replay(orgDirSvc); + setOrganizationDirectoryService(orgDirSvc); + setServiceRegistry(registry); } /**
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-j63h-hmgw-x4j7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-54380ghsaADVISORY
- github.com/opencast/opencast/commit/2d3219113e2b9fadfb06443f5468b1c2157827a6ghsaWEB
- github.com/opencast/opencast/commit/e8980435342149375802648b9c9e696c9a5f0c9aghsax_refsource_MISCWEB
- github.com/opencast/opencast/security/advisories/GHSA-hcxx-mp6g-6gr9ghsax_refsource_MISCWEB
- github.com/opencast/opencast/security/advisories/GHSA-j63h-hmgw-x4j7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.