Skip to content

Commit 6780918

Browse files
committed
Add configurable output directories for export data
1 parent b94d007 commit 6780918

10 files changed

Lines changed: 1029 additions & 3 deletions

File tree

geode-docs/tools_modules/gfsh/command-pages/export.html.md.erb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,23 @@ In this scenario, partitioned region data is exported simultaneously on all host
165165
| <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.|
166166
| <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. |
167167

168+
**Permitted export locations:**
169+
170+
The snapshot is written by the member named in `--member`, on that member's host. A member only
171+
writes exports into its own working directory (and sub-directories of it). To export somewhere
172+
else, such as a mounted backup location, set the `gemfire.export.data.dirs` system property on the
173+
member to the directories it may write into, separated by the platform's path separator:
174+
175+
``` pre
176+
-Dgemfire.export.data.dirs=/mnt/backup/geode:/var/exports/geode
177+
```
178+
179+
Paths containing a `..` parent directory reference are rejected, and a path that resolves
180+
outside every permitted directory is refused by the member.
181+
182+
**Required permission:** `DATA:READ` on the exported region, plus `CLUSTER:WRITE` for the file the
183+
export writes on the member's host.
184+
168185
**Example Commands:**
169186

170187
``` pre
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
3+
* agreements. See the NOTICE file distributed with this work for additional information regarding
4+
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
16+
package org.apache.geode.management.internal.cli.commands;
17+
18+
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_MANAGER;
19+
import static org.apache.geode.management.internal.cli.functions.ExportDataFunction.EXPORT_DATA_DIRS_PROPERTY;
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import java.io.Serializable;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.Properties;
27+
28+
import org.junit.After;
29+
import org.junit.Before;
30+
import org.junit.BeforeClass;
31+
import org.junit.ClassRule;
32+
import org.junit.Rule;
33+
import org.junit.Test;
34+
import org.junit.experimental.categories.Category;
35+
import org.junit.rules.TemporaryFolder;
36+
37+
import org.apache.geode.cache.RegionShortcut;
38+
import org.apache.geode.examples.SimpleSecurityManager;
39+
import org.apache.geode.internal.cache.InternalCache;
40+
import org.apache.geode.management.internal.security.ResourceConstants;
41+
import org.apache.geode.test.dunit.IgnoredException;
42+
import org.apache.geode.test.dunit.rules.ClusterStartupRule;
43+
import org.apache.geode.test.dunit.rules.MemberVM;
44+
import org.apache.geode.test.junit.categories.SecurityTest;
45+
import org.apache.geode.test.junit.rules.GfshCommandRule;
46+
47+
/**
48+
* Tests which principals may run {@code export data} in a secured cluster.
49+
*
50+
* <p>
51+
* {@link SimpleSecurityManager} authorizes a user for exactly those permissions whose string form
52+
* starts with the user name, and treats a comma separated user name as a set of roles. So
53+
* "dataRead" holds DATA:READ alone, while "dataRead,clusterWrite" is the operator {@code
54+
* export data} requires.
55+
*/
56+
@Category(SecurityTest.class)
57+
public class ExportDataCommandSecurityTest implements Serializable {
58+
59+
private static final String REGION_NAME = "testRegion";
60+
private static final String READ_ONLY_USER = "dataRead";
61+
private static final String EXPORT_OPERATOR = "dataRead,clusterWrite";
62+
63+
@ClassRule
64+
public static ClusterStartupRule cluster = new ClusterStartupRule();
65+
66+
@Rule
67+
public GfshCommandRule gfsh = new GfshCommandRule();
68+
69+
@Rule
70+
public TemporaryFolder temporaryFolder = new TemporaryFolder();
71+
72+
private static MemberVM locator;
73+
private static MemberVM server;
74+
75+
/** The directory the server has been configured to permit exports into. */
76+
private Path permittedDir;
77+
78+
/** Any other location on the server host. */
79+
private Path otherDir;
80+
81+
@BeforeClass
82+
public static void beforeClass() {
83+
Properties locatorProps = new Properties();
84+
locatorProps.setProperty(SECURITY_MANAGER, SimpleSecurityManager.class.getName());
85+
locator = cluster.startLocatorVM(0, locatorProps);
86+
87+
Properties serverProps = new Properties();
88+
serverProps.setProperty(ResourceConstants.USER_NAME, "clusterManage");
89+
serverProps.setProperty(ResourceConstants.PASSWORD, "clusterManage");
90+
server = cluster.startServerVM(1, serverProps, locator.getPort());
91+
92+
server.invoke(() -> {
93+
InternalCache cache = ClusterStartupRule.getCache();
94+
assertThat(cache).isNotNull();
95+
cache.createRegionFactory(RegionShortcut.REPLICATE).create(REGION_NAME).put("key", "value");
96+
});
97+
}
98+
99+
@Before
100+
public void configurePermittedExportDirectory() throws Exception {
101+
// Refusing an export is logged at error level on the member; that is the expected outcome of
102+
// most of these tests, not a symptom of one going wrong.
103+
IgnoredException.addIgnoredException("Cannot export to");
104+
105+
permittedDir = temporaryFolder.newFolder("permitted").toPath();
106+
otherDir = temporaryFolder.newFolder("other").toPath();
107+
108+
String permitted = permittedDir.toString();
109+
server.invoke(() -> System.setProperty(EXPORT_DATA_DIRS_PROPERTY, permitted));
110+
}
111+
112+
@After
113+
public void clearPermittedExportDirectory() {
114+
server.invoke(() -> System.clearProperty(EXPORT_DATA_DIRS_PROPERTY));
115+
}
116+
117+
private void connectAs(String user) throws Exception {
118+
gfsh.secureConnectAndVerify(locator.getPort(), GfshCommandRule.PortType.locator, user, user);
119+
}
120+
121+
private String exportTo(String option, Path path) {
122+
return "export data --member=" + server.getName() + " --region=" + REGION_NAME + " --"
123+
+ option + "=" + path;
124+
}
125+
126+
/**
127+
* Read access to region data on its own does not permit an export.
128+
*/
129+
@Test
130+
public void dataReadUserCannotExport() throws Exception {
131+
connectAs(READ_ONLY_USER);
132+
Path target = permittedDir.resolve("snapshot.gfd");
133+
134+
gfsh.executeAndAssertThat(exportTo("file", target))
135+
.statusIsError()
136+
.containsOutput("not authorized for CLUSTER:WRITE");
137+
138+
assertThat(target).doesNotExist();
139+
}
140+
141+
/**
142+
* Permissions are checked before the path, so the target directory makes no difference.
143+
*/
144+
@Test
145+
public void dataReadUserIsRefusedForAnyDirectory() throws Exception {
146+
connectAs(READ_ONLY_USER);
147+
Path target = otherDir.resolve("snapshot.gfd");
148+
149+
gfsh.executeAndAssertThat(exportTo("file", target))
150+
.statusIsError()
151+
.containsOutput("not authorized for CLUSTER:WRITE");
152+
153+
assertThat(target).doesNotExist();
154+
}
155+
156+
/**
157+
* The command works for a principal holding both permissions.
158+
*/
159+
@Test
160+
public void operatorWithClusterWriteCanExportIntoThePermittedDirectory() throws Exception {
161+
connectAs(EXPORT_OPERATOR);
162+
Path target = permittedDir.resolve("snapshot.gfd");
163+
164+
gfsh.executeAndAssertThat(exportTo("file", target))
165+
.statusIsSuccess()
166+
.containsOutput("Data successfully exported");
167+
168+
assertThat(target).exists();
169+
}
170+
171+
/**
172+
* The directory restriction applies independently of the permission: even a permitted operator
173+
* cannot place the snapshot anywhere it likes.
174+
*/
175+
@Test
176+
public void operatorCannotExportOutsideThePermittedDirectory() throws Exception {
177+
connectAs(EXPORT_OPERATOR);
178+
Path target = otherDir.resolve("snapshot.gfd");
179+
180+
gfsh.executeAndAssertThat(exportTo("file", target)).statusIsError();
181+
182+
assertThat(target).doesNotExist();
183+
}
184+
185+
/**
186+
* Nor can the operator leave the permitted directory with "../".
187+
*/
188+
@Test
189+
public void operatorCannotLeavePermittedDirectoryWithParentReference() throws Exception {
190+
connectAs(EXPORT_OPERATOR);
191+
Path withParentReference = permittedDir.resolve("..").resolve("other");
192+
193+
gfsh.executeAndAssertThat(exportTo("dir", withParentReference)).statusIsError();
194+
195+
assertThat(otherDir.resolve(REGION_NAME + ".gfd")).doesNotExist();
196+
}
197+
198+
/**
199+
* An existing file outside the permitted directory survives an export aimed at it.
200+
*/
201+
@Test
202+
public void existingFileOutsideThePermittedDirectoryIsNotOverwritten() throws Exception {
203+
connectAs(EXPORT_OPERATOR);
204+
Path existingFile = otherDir.resolve("existing.gfd");
205+
String originalContent = "existing content";
206+
Files.write(existingFile, originalContent.getBytes(StandardCharsets.UTF_8));
207+
208+
gfsh.executeAndAssertThat(exportTo("file", existingFile)).statusIsError();
209+
210+
assertThat(new String(Files.readAllBytes(existingFile), StandardCharsets.UTF_8))
211+
.isEqualTo(originalContent);
212+
}
213+
214+
/**
215+
* Confirms the read only grant really is read only, so the refusals above are the permission
216+
* check taking effect rather than a misconfigured principal.
217+
*/
218+
@Test
219+
public void readOnlyUserCanStillReadData() throws Exception {
220+
connectAs(READ_ONLY_USER);
221+
222+
gfsh.executeAndAssertThat("get --region=" + REGION_NAME + " --key=key").statusIsSuccess();
223+
gfsh.executeAndAssertThat("put --region=" + REGION_NAME + " --key=k --value=v")
224+
.statusIsError()
225+
.containsOutput("dataRead not authorized for DATA:WRITE");
226+
}
227+
}

geode-gfsh/src/integrationTest/java/org/apache/geode/management/internal/cli/commands/ExportDataIntegrationTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.apache.geode.management.internal.cli.commands;
1818

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

@@ -31,6 +32,7 @@
3132
import org.junit.ClassRule;
3233
import org.junit.Rule;
3334
import org.junit.Test;
35+
import org.junit.contrib.java.lang.system.RestoreSystemProperties;
3436
import org.junit.rules.TemporaryFolder;
3537

3638
import org.apache.geode.DataSerializable;
@@ -58,6 +60,9 @@ public class ExportDataIntegrationTest {
5860
@Rule
5961
public TemporaryFolder tempDir = new TemporaryFolder();
6062

63+
@Rule
64+
public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
65+
6166
private Region<String, Object> region;
6267
private Path snapshotFile;
6368
private Path snapshotDir;
@@ -87,6 +92,8 @@ public void setup() throws Exception {
8792
region = server.getCache().getRegion(TEST_REGION_NAME);
8893
loadRegion("value");
8994
Path basePath = tempDir.getRoot().toPath();
95+
// export data only writes into directories the member permits; permit the test's folder
96+
System.setProperty(EXPORT_DATA_DIRS_PROPERTY, basePath.toString());
9097
snapshotFile = basePath.resolve(SNAPSHOT_FILE);
9198
snapshotDir = basePath.resolve(SNAPSHOT_DIR);
9299
}

0 commit comments

Comments
 (0)