VYPR
Moderate severityNVD Advisory· Published Jul 26, 2025· Updated Jul 28, 2025

Opencast still publishes global system account credentials

CVE-2025-54380

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.

PackageAffected versionsPatched versions
org.opencastproject:opencast-commonMaven
< 17.617.6
org.opencastproject:opencast-ingest-service-implMaven
< 17.617.6
org.opencastproject:opencast-kernelMaven
< 17.617.6
org.opencastproject:opencast-publication-service-oaipmh-remoteMaven
< 17.617.6

Affected products

1

Patches

2
2d3219113e2b

Merge commit from fork

https://github.com/opencast/opencastGreg LoganJul 24, 2025via ghsa
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);
         }
     
         /**
    
e89804353421

Checking that a node is listed in the service registry prior to sending any credentials

https://github.com/opencast/opencastGreg LoganApr 13, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.