Skip to content

Commit

Permalink
UI Configuration Service Configuration
Browse files Browse the repository at this point in the history
This patch moves the configuration of the user interface configuration
service (providing the `/ui-config` endpoint) from the global
configuration to a service configuration file.

It also adds support for returning an X-Accel-Redirect header along the
way.
  • Loading branch information
lkiesow committed Jul 23, 2021
1 parent 4b7beb7 commit 5fdb379
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 71 deletions.
9 changes: 9 additions & 0 deletions etc/org.opencastproject.uiconfig.UIConfigRest.cfg
@@ -0,0 +1,9 @@
# Directory user interface configuration is served from.
# Default: ${karaf.etc}/ui-config
#org.opencastproject.uiconfig.folder=${karaf.etc}/ui-config

# Makes the UI configuration service return an X-Accel-Redirect header using the
# following path prefix instead of serving the actual file.
# More details: https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
# Default: null
#x.accel.redirect=/protected-ui-config
3 changes: 0 additions & 3 deletions modules/user-interface-configuration/pom.xml
Expand Up @@ -94,9 +94,6 @@
<Export-Package>
org.opencastproject.uiconfig;version=${project.version},
</Export-Package>
<Service-Component>
OSGI-INF/uiconfig.xml
</Service-Component>
</instructions>
</configuration>
</plugin>
Expand Down
Expand Up @@ -33,7 +33,11 @@
import org.opencastproject.util.doc.rest.RestService;

import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -43,6 +47,7 @@
import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.Paths;
import java.util.Map;

import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
Expand All @@ -69,48 +74,65 @@
+ "<a href=\"https://github.com/opencast/opencast/issues\">Opencast Issue Tracker</a>"
}
)
@Component(
property = {
"service.description=UI Config REST Endpoint",
"opencast.service.type=org.opencastproject.uiconfig",
"opencast.service.path=/ui/config"
},
immediate = true,
service = UIConfigRest.class
)
public class UIConfigRest {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(UIConfigRest.class);

/** Configuration key for the ui config folder */
static final String UI_CONFIG_FOLDER_PROPERTY = "org.opencastproject.uiconfig.folder";
private static final String X_ACCEL_REDIRECT_PROPERTY = "x.accel.redirect";

/** Default Path for the ui configuration folder (relative to ${karaf.etc}) */
private static final String UI_CONFIG_FOLDER_DEFAULT = "ui-config";

/** The currently used path to the configuration folder */
private String uiConfigFolder = "";
private String xAccelRedirect = null;

/** The used SecurityService */
private SecurityService securityService;

@Reference
protected void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}


/**
* OSGI callback for activating this component
*
* @param cc
* the osgi component context
*/
public void activate(ComponentContext cc) throws ConfigurationException {
uiConfigFolder = cc.getBundleContext().getProperty(UI_CONFIG_FOLDER_PROPERTY);

if (StringUtils.isEmpty(uiConfigFolder)) {
String karafetc = cc.getBundleContext().getProperty("karaf.etc");

if (StringUtils.isBlank(karafetc)) {
throw new ConfigurationException(UI_CONFIG_FOLDER_PROPERTY + " not set and unable to"
+ " fall back to default location based on ${karaf.etc}");
@Activate
@Modified
public void activate(BundleContext context, Map<String, String> properties)
throws ConfigurationException, IOException {
uiConfigFolder = properties.get(UI_CONFIG_FOLDER_PROPERTY);
logger.debug("UI configuration folder set to '{}'", uiConfigFolder);
if (StringUtils.isNotEmpty(uiConfigFolder)) {
uiConfigFolder = new File(uiConfigFolder).getCanonicalPath();
} else {
final String karafEtc = context.getProperty("karaf.etc");
if (StringUtils.isBlank(karafEtc)) {
throw new ConfigurationException(String.format(
"%s not set and unable to fall back to default location based on ${karaf.etc}",
UI_CONFIG_FOLDER_PROPERTY));
}

uiConfigFolder = new File(karafetc, UI_CONFIG_FOLDER_DEFAULT).getAbsolutePath();
uiConfigFolder = new File(karafEtc, UI_CONFIG_FOLDER_DEFAULT).getCanonicalPath();
}
logger.debug("UI configuration folder set to '{}'", uiConfigFolder);

logger.info("UI configuration folder is '{}'", uiConfigFolder);
xAccelRedirect = properties.get(X_ACCEL_REDIRECT_PROPERTY);
logger.debug("X-Accel-Redirect path set to {}", xAccelRedirect);
}

@GET
Expand Down Expand Up @@ -151,12 +173,19 @@ public Response getConfigFile(@PathParam("component") String component, @PathPar
throw new AccessDeniedException(configFileCanPath);
}

// Falling back to default organization if files does not exist
// Falling back to default organization if file does not exist
if (!configFile.exists()) {
logger.debug("Falling back to default organization");
configFile = Paths.get(uiConfigFolder, DEFAULT_ORGANIZATION_ID, component, filename).toFile();
}

if (xAccelRedirect != null) {
final String relative = Paths.get(uiConfigFolder).relativize(configFile.toPath()).toString();
return Response.noContent()
.header("X-Accel-Redirect", Paths.get(xAccelRedirect, relative).toString())
.build();
}

// It is safe to pass the InputStream without closing it, JAX-RS takes care of that
return Response.ok(new FileInputStream(configFile))
.header("Content-Length", configFile.length())
Expand Down

This file was deleted.

Expand Up @@ -35,12 +35,13 @@
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.AccessDeniedException;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;

import javax.ws.rs.core.Response;

Expand All @@ -52,7 +53,7 @@ public class UIConfigTest {
public TemporaryFolder temporaryFolder = new TemporaryFolder();

@Before
public void setUp() throws Exception {
public void setUp() {
// create the needed mocks
Organization organization = EasyMock.createMock(Organization.class);
EasyMock.expect(organization.getId()).andReturn("org1").anyTimes();
Expand All @@ -69,49 +70,38 @@ public void setUp() throws Exception {

@Test
public void testActivate() throws Exception {
Map<String, String> properties = EasyMock.createMock(Map.class);
EasyMock.expect(properties.get(UI_CONFIG_FOLDER_PROPERTY)).andReturn(null).times(2);
EasyMock.expect(properties.get(UI_CONFIG_FOLDER_PROPERTY)).andReturn("/xy").once();

BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
EasyMock.expect(bundleContext.getProperty(UI_CONFIG_FOLDER_PROPERTY)).andReturn(null).times(2);
EasyMock.expect(bundleContext.getProperty(UI_CONFIG_FOLDER_PROPERTY)).andReturn("/xy").once();
EasyMock.expect(bundleContext.getProperty("karaf.etc")).andReturn(null).once();
EasyMock.expect(bundleContext.getProperty("karaf.etc")).andReturn("/xy").once();

ComponentContext componentContext = EasyMock.createMock(ComponentContext.class);
EasyMock.expect(componentContext.getBundleContext()).andReturn(bundleContext).anyTimes();

EasyMock.replay(bundleContext, componentContext);
EasyMock.replay(bundleContext, properties);

try {
uiConfigRest.activate(componentContext);
Assert.fail();
} catch (ConfigurationException e) {
// config and default are null. We expect this to fail
}
// Config and default are null. We expect this to fail
Assert.assertThrows(ConfigurationException.class, () -> {
uiConfigRest.activate(bundleContext, properties);
});

// Providing proper configuration now. This should work
uiConfigRest.activate(componentContext);
uiConfigRest.activate(componentContext);
uiConfigRest.activate(bundleContext, properties);
uiConfigRest.activate(bundleContext, properties);
}

@Test
public void testGetFile() throws Exception {
final File testDir = temporaryFolder.newFolder();
BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
EasyMock.expect(bundleContext.getProperty(UI_CONFIG_FOLDER_PROPERTY)).andReturn(testDir.getAbsolutePath()).once();

ComponentContext componentContext = EasyMock.createMock(ComponentContext.class);
EasyMock.expect(componentContext.getBundleContext()).andReturn(bundleContext).anyTimes();

EasyMock.replay(bundleContext, componentContext);

uiConfigRest.activate(componentContext);
// configure service
uiConfigRest.activate(null, Collections.singletonMap(UI_CONFIG_FOLDER_PROPERTY, testDir.getAbsolutePath()));

// test non-existing file
try {
// Test non-existing file
// We expect this to not be found
Assert.assertThrows(NotFoundException.class, () -> {
uiConfigRest.getConfigFile("player", "config.json");
Assert.fail();
} catch (NotFoundException e) {
// We expect this to not be found
}
});

// test existing file
File target = Paths.get(testDir.getAbsolutePath(), "org1", "player", "config.json").toFile();
Expand All @@ -120,12 +110,10 @@ public void testGetFile() throws Exception {
Response response = uiConfigRest.getConfigFile("player", "config.json");
Assert.assertEquals(200, response.getStatus());

// test path traversal
try {
// Test path traversal
// we expect access to be denied
Assert.assertThrows(AccessDeniedException.class, () -> {
uiConfigRest.getConfigFile("../player", "config.json");
Assert.fail();
} catch (AccessDeniedException e) {
// we expect access to be denied
}
});
}
}

0 comments on commit 5fdb379

Please sign in to comment.