Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion geode-docs/security/implementing_authorization.html.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ This table classifies the permissions assigned for `gfsh` operations.
| execute function | Defaults to DATA:WRITE. Override `Function.getRequiredPermissions` to change the permission. |
| export cluster-configuration | CLUSTER:READ |
| export config | CLUSTER:READ |
| export data | CLUSTER:READ |
| export data | DATA:READ:RegionName and CLUSTER:WRITE |
| export logs | CLUSTER:READ |
| export offline-disk-store | CLUSTER:READ |
| export stack-traces | CLUSTER:READ |
Expand Down
16 changes: 16 additions & 0 deletions geode-docs/tools_modules/gfsh/command-pages/export.html.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ In this scenario, partitioned region data is exported simultaneously on all host
| <span class="keyword parmname">&#8209;&#8209;dir</span> | Directory to which the exported data is to be written. Required if &#8209;&#8209;parallel is true. Cannot be specified at the same time as &#8209;&#8209;file.|
| <span class="keyword parmname">&#8209;&#8209;parallel</span> | Export local data on each node to a directory on that machine. Available for partitioned regions only. |

**Export locations:**

The snapshot is written by the member named in `--member`, on that member's host. A member writes
exports into its own working directory (and sub-directories of it). To export somewhere else, such
as a mounted backup location, set the `gemfire.export.data.dirs` system property on the member to
the additional directories, separated by the platform's path separator:

``` pre
-Dgemfire.export.data.dirs=/mnt/backup/geode:/var/exports/geode
```

A path containing a `..` segment is not accepted, and a path that resolves outside the configured
directories is rejected by the member.

**Required permission:** `DATA:READ` on the exported region, plus `CLUSTER:WRITE`.

**Example Commands:**

``` pre
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.geode.management.internal.cli.commands;

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.management.internal.cli.functions.ExportDataFunction.EXPORT_DATA_DIRS_PROPERTY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;

Expand All @@ -31,6 +32,7 @@
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.rules.TemporaryFolder;

import org.apache.geode.DataSerializable;
Expand Down Expand Up @@ -58,6 +60,9 @@ public class ExportDataIntegrationTest {
@Rule
public TemporaryFolder tempDir = new TemporaryFolder();

@Rule
public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();

private Region<String, Object> region;
private Path snapshotFile;
private Path snapshotDir;
Expand Down Expand Up @@ -87,6 +92,8 @@ public void setup() throws Exception {
region = server.getCache().getRegion(TEST_REGION_NAME);
loadRegion("value");
Path basePath = tempDir.getRoot().toPath();
// configure the test's temporary folder as an export destination
System.setProperty(EXPORT_DATA_DIRS_PROPERTY, basePath.toString());
snapshotFile = basePath.resolve(SNAPSHOT_FILE);
snapshotDir = basePath.resolve(SNAPSHOT_DIR);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.apache.geode.management.internal.cli.commands;

import static org.apache.geode.cache.Region.SEPARATOR;
import static org.apache.geode.management.internal.cli.functions.ExportDataFunction.EXPORT_DATA_DIRS_PROPERTY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;

Expand All @@ -30,6 +31,7 @@
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
import org.junit.rules.TemporaryFolder;

import org.apache.geode.cache.Region;
Expand All @@ -55,6 +57,9 @@ public class ImportDataIntegrationTest {
@Rule
public TemporaryFolder tempDir = new TemporaryFolder();

@Rule
public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();

private Region<String, String> region;
private Path snapshotFile;
private Path snapshotDir;
Expand All @@ -65,6 +70,8 @@ public void setup() throws Exception {
region = server.getCache().getRegion(TEST_REGION_NAME);
loadRegion("value");
Path basePath = tempDir.getRoot().toPath();
// configure the test's temporary folder as an export destination
System.setProperty(EXPORT_DATA_DIRS_PROPERTY, basePath.toString());
snapshotFile = basePath.resolve(SNAPSHOT_FILE);
snapshotDir = basePath.resolve(SNAPSHOT_DIR);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
package org.apache.geode.management.internal.cli.commands;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Optional;

Expand All @@ -33,6 +35,7 @@
import org.apache.geode.management.internal.cli.result.model.ResultModel;
import org.apache.geode.management.internal.functions.CliFunctionResult;
import org.apache.geode.management.internal.i18n.CliStrings;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.security.ResourcePermission.Operation;
import org.apache.geode.security.ResourcePermission.Resource;

Expand All @@ -54,6 +57,7 @@ public ResultModel exportData(
help = CliStrings.EXPORT_DATA__PARALLEL_HELP) boolean parallel) {

authorize(Resource.DATA, Operation.READ, regionName);
authorize(Resource.CLUSTER, Operation.WRITE, ResourcePermission.ALL);
final DistributedMember targetMember = getMember(memberNameOrId);

Optional<ResultModel> validationResult = validatePath(filePath, dirPath, parallel);
Expand Down Expand Up @@ -100,6 +104,28 @@ private Optional<ResultModel> validatePath(String filePath, String dirPath, bool
return Optional.of(ResultModel.createError(CliStrings.format(
CliStrings.INVALID_FILE_EXTENSION, CliStrings.GEODE_DATA_FILE_EXTENSION)));
}

if (filePath != null && containsParentDirectorySegment(filePath)) {
return Optional.of(invalidPathError(CliStrings.EXPORT_DATA__FILE, filePath));
}
if (dirPath != null && containsParentDirectorySegment(dirPath)) {
return Optional.of(invalidPathError(CliStrings.EXPORT_DATA__DIR, dirPath));
}

return Optional.empty();
}

private static boolean containsParentDirectorySegment(String path) {
for (Path element : Paths.get(path)) {
if ("..".equals(element.toString())) {
return true;
}
}
return false;
}

private static ResultModel invalidPathError(String option, String path) {
return ResultModel.createError(String.format(
"Option \"%s\" must not contain a \"..\" path segment: %s", option, path));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package org.apache.geode.management.internal.cli.functions;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.geode.cache.Cache;
import org.apache.geode.cache.Region;
Expand All @@ -27,19 +30,34 @@
import org.apache.geode.management.cli.CliFunction;
import org.apache.geode.management.internal.functions.CliFunctionResult;
import org.apache.geode.management.internal.i18n.CliStrings;
import org.apache.geode.util.internal.GeodeGlossary;

/***
* Function which carries out the export of a region to a file on a member. Uses the
* RegionSnapshotService to export the data
*
*
* <p>
* Export destinations are resolved to their canonical form and must be within the export
* directories configured for this member.
*/
public class ExportDataFunction extends CliFunction<String[]> {
private static final long serialVersionUID = 1L;

private static final String ID =
"org.apache.geode.management.internal.cli.functions.ExportDataFunction";

/**
* System property naming additional directories this member writes {@code export data} snapshots
* into. Several directories may be listed, separated by {@link File#pathSeparator}. Exports into
* sub-directories of a configured directory are included.
*
* <p>
* The member's working directory is always configured, since that is where a relative export
* path resolves to, so when this property is not set it is the only export destination.
*/
public static final String EXPORT_DATA_DIRS_PROPERTY =
GeodeGlossary.GEMFIRE_PREFIX + "export.data.dirs";

@Override
public String getId() {
return ID;
Expand All @@ -62,7 +80,7 @@ public CliFunctionResult executeFunction(FunctionContext<String[]> context) thro
String hostName = cache.getDistributedSystem().getDistributedMember().getHost();
if (region != null) {
RegionSnapshotService<Object, Object> snapshotService = region.getSnapshotService();
final File exportFile = new File(fileName);
final File exportFile = resolveExportFile(fileName);
if (parallel) {
SnapshotOptions<Object, Object> options = new SnapshotOptionsImpl<>().setParallelMode(true);
snapshotService.save(exportFile, SnapshotFormat.GEODE, options);
Expand All @@ -81,4 +99,42 @@ public CliFunctionResult executeFunction(FunctionContext<String[]> context) thro

return result;
}

/**
* Resolves the requested export path against the export directories configured for this member.
*
* @param fileName the path requested by the caller, which may be relative or absolute
* @return the canonical file to export to
* @throws IllegalArgumentException if the path is not within a configured export directory
*/
static File resolveExportFile(String fileName) throws IOException {
File exportFile = new File(fileName).getCanonicalFile();
List<File> exportDirs = configuredExportDirs();

for (File exportDir : exportDirs) {
if (exportFile.toPath().startsWith(exportDir.toPath())) {
return exportFile;
}
}

throw new IllegalArgumentException(String.format(
"Cannot export to %s: the path is not within the export directories configured for this member (%s). Use the %s system property to configure additional directories.",
exportFile, exportDirs, EXPORT_DATA_DIRS_PROPERTY));
}

private static List<File> configuredExportDirs() throws IOException {
List<File> exportDirs = new ArrayList<>();
exportDirs.add(new File(System.getProperty("user.dir")).getCanonicalFile());

String configuredDirs = System.getProperty(EXPORT_DATA_DIRS_PROPERTY);
if (configuredDirs != null) {
for (String configuredDir : configuredDirs.split(File.pathSeparator)) {
if (!configuredDir.trim().isEmpty()) {
exportDirs.add(new File(configuredDir.trim()).getCanonicalFile());
}
}
}

return exportDirs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@

package org.apache.geode.management.internal.cli.commands;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.test.junit.rules.GfshParserRule;


Expand All @@ -34,11 +42,35 @@ public void setUp() {
command = new ExportDataCommand();
}

/** A command whose option values are checked without contacting a member. */
private ExportDataCommand commandWithMember() {
ExportDataCommand withMember = spy(ExportDataCommand.class);
doNothing().when(withMember).authorize(any(), any(), anyString());
doReturn(mock(DistributedMember.class)).when(withMember).getMember(anyString());
return withMember;
}

@Test
public void missingMember() throws Exception {
// Command parses successfully but fails during execution because cache is null
gfsh.executeAndAssertThat(command, "export data --region=regionA --file=test")
.statusIsError()
.containsOutput("cache");
}

@Test
public void fileOptionWithParentDirectorySegmentIsRejected() {
gfsh.executeAndAssertThat(commandWithMember(),
"export data --member=server1 --region=regionA --file=exports/../regionA.gfd")
.statusIsError()
.containsOutput("must not contain a \"..\" path segment");
}

@Test
public void dirOptionWithParentDirectorySegmentIsRejected() {
gfsh.executeAndAssertThat(commandWithMember(),
"export data --member=server1 --region=regionA --dir=exports/../elsewhere")
.statusIsError()
.containsOutput("must not contain a \"..\" path segment");
}
}
Loading
Loading