001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.rest;
019
020import static org.junit.jupiter.api.Assertions.assertEquals;
021import static org.junit.jupiter.api.Assertions.assertFalse;
022import static org.junit.jupiter.api.Assertions.assertNotNull;
023
024import java.io.ByteArrayInputStream;
025import java.io.IOException;
026import java.io.StringWriter;
027import java.util.stream.Stream;
028import javax.xml.bind.JAXBContext;
029import javax.xml.bind.JAXBException;
030import org.apache.hadoop.conf.Configuration;
031import org.apache.hadoop.hbase.HBaseParameterizedTestTemplate;
032import org.apache.hadoop.hbase.HBaseTestingUtil;
033import org.apache.hadoop.hbase.TableName;
034import org.apache.hadoop.hbase.client.Admin;
035import org.apache.hadoop.hbase.rest.client.Client;
036import org.apache.hadoop.hbase.rest.client.Cluster;
037import org.apache.hadoop.hbase.rest.client.Response;
038import org.apache.hadoop.hbase.rest.model.ColumnSchemaModel;
039import org.apache.hadoop.hbase.rest.model.TableSchemaModel;
040import org.apache.hadoop.hbase.rest.model.TestTableSchemaModel;
041import org.apache.hadoop.hbase.testclassification.MediumTests;
042import org.apache.hadoop.hbase.testclassification.RestTests;
043import org.apache.hadoop.hbase.util.Bytes;
044import org.apache.http.Header;
045import org.apache.http.message.BasicHeader;
046import org.junit.jupiter.api.AfterAll;
047import org.junit.jupiter.api.AfterEach;
048import org.junit.jupiter.api.BeforeAll;
049import org.junit.jupiter.api.Tag;
050import org.junit.jupiter.api.TestTemplate;
051import org.junit.jupiter.params.provider.Arguments;
052
053@Tag(RestTests.TAG)
054@Tag(MediumTests.TAG)
055@HBaseParameterizedTestTemplate(name = "{index}: csrfEnabled = {0}")
056public class TestSchemaResource {
057
058  private static String TABLE1 = "TestSchemaResource1";
059  private static String TABLE2 = "TestSchemaResource2";
060
061  private static final HBaseTestingUtil TEST_UTIL = new HBaseTestingUtil();
062  private static final HBaseRESTTestingUtility REST_TEST_UTIL = new HBaseRESTTestingUtility();
063  private static Client client;
064  private static JAXBContext context;
065  private static Configuration conf;
066  private static TestTableSchemaModel testTableSchemaModel;
067  private static Header extraHdr = null;
068
069  private static boolean csrfEnabled = true;
070
071  public TestSchemaResource(boolean csrf) {
072    csrfEnabled = csrf;
073  }
074
075  public static Stream<Arguments> parameters() {
076    return Stream.of(Arguments.of(false), Arguments.of(true));
077  }
078
079  @BeforeAll
080  public static void setUpBeforeClass() throws Exception {
081    conf = TEST_UTIL.getConfiguration();
082    conf.setBoolean(RESTServer.REST_CSRF_ENABLED_KEY, csrfEnabled);
083    if (csrfEnabled) {
084      conf.set(RESTServer.REST_CSRF_BROWSER_USERAGENTS_REGEX_KEY, ".*");
085    }
086    extraHdr = new BasicHeader(RESTServer.REST_CSRF_CUSTOM_HEADER_DEFAULT, "");
087    TEST_UTIL.startMiniCluster();
088    REST_TEST_UTIL.startServletContainer(conf);
089    client = new Client(new Cluster().add("localhost", REST_TEST_UTIL.getServletPort()));
090    testTableSchemaModel = new TestTableSchemaModel();
091    context = JAXBContext.newInstance(ColumnSchemaModel.class, TableSchemaModel.class);
092  }
093
094  @AfterAll
095  public static void tearDownAfterClass() throws Exception {
096    REST_TEST_UTIL.shutdownServletContainer();
097    TEST_UTIL.shutdownMiniCluster();
098  }
099
100  @AfterEach
101  public void tearDown() throws Exception {
102    Admin admin = TEST_UTIL.getAdmin();
103
104    for (String table : new String[] { TABLE1, TABLE2 }) {
105      TableName t = TableName.valueOf(table);
106      if (admin.tableExists(t)) {
107        admin.disableTable(t);
108        admin.deleteTable(t);
109      }
110    }
111
112    conf.set("hbase.rest.readonly", "false");
113  }
114
115  private static byte[] toXML(TableSchemaModel model) throws JAXBException {
116    StringWriter writer = new StringWriter();
117    context.createMarshaller().marshal(model, writer);
118    return Bytes.toBytes(writer.toString());
119  }
120
121  private static TableSchemaModel fromXML(byte[] content) throws JAXBException {
122    return (TableSchemaModel) context.createUnmarshaller()
123      .unmarshal(new ByteArrayInputStream(content));
124  }
125
126  @TestTemplate
127  public void testTableCreateAndDeleteXML() throws IOException, JAXBException {
128    String schemaPath = "/" + TABLE1 + "/schema";
129    TableSchemaModel model;
130    Response response;
131
132    Admin admin = TEST_UTIL.getAdmin();
133    assertFalse(admin.tableExists(TableName.valueOf(TABLE1)),
134      "Table " + TABLE1 + " should not exist");
135
136    // create the table
137    model = testTableSchemaModel.buildTestModel(TABLE1);
138    testTableSchemaModel.checkModel(model, TABLE1);
139    if (csrfEnabled) {
140      // test put operation is forbidden without custom header
141      response = client.put(schemaPath, Constants.MIMETYPE_XML, toXML(model));
142      assertEquals(400, response.getCode());
143    }
144
145    response = client.put(schemaPath, Constants.MIMETYPE_XML, toXML(model), extraHdr);
146    assertEquals(201, response.getCode(),
147      "put failed with csrf " + (csrfEnabled ? "enabled" : "disabled"));
148
149    // recall the same put operation but in read-only mode
150    conf.set("hbase.rest.readonly", "true");
151    response = client.put(schemaPath, Constants.MIMETYPE_XML, toXML(model), extraHdr);
152    assertEquals(403, response.getCode());
153
154    // retrieve the schema and validate it
155    response = client.get(schemaPath, Constants.MIMETYPE_XML);
156    assertEquals(200, response.getCode());
157    assertEquals(Constants.MIMETYPE_XML, response.getHeader("content-type"));
158    model = fromXML(response.getBody());
159    testTableSchemaModel.checkModel(model, TABLE1);
160
161    // with json retrieve the schema and validate it
162    response = client.get(schemaPath, Constants.MIMETYPE_JSON);
163    assertEquals(200, response.getCode());
164    assertEquals(Constants.MIMETYPE_JSON, response.getHeader("content-type"));
165    model = testTableSchemaModel.fromJSON(Bytes.toString(response.getBody()));
166    testTableSchemaModel.checkModel(model, TABLE1);
167
168    if (csrfEnabled) {
169      // test delete schema operation is forbidden without custom header
170      response = client.delete(schemaPath);
171      assertEquals(400, response.getCode());
172    }
173
174    // test delete schema operation is forbidden in read-only mode
175    response = client.delete(schemaPath, extraHdr);
176    assertEquals(403, response.getCode());
177
178    // return read-only setting back to default
179    conf.set("hbase.rest.readonly", "false");
180
181    // delete the table and make sure HBase concurs
182    response = client.delete(schemaPath, extraHdr);
183    assertEquals(200, response.getCode());
184    assertFalse(admin.tableExists(TableName.valueOf(TABLE1)));
185  }
186
187  @TestTemplate
188  public void testTableCreateAndDeletePB() throws IOException {
189    String schemaPath = "/" + TABLE2 + "/schema";
190    TableSchemaModel model;
191    Response response;
192
193    Admin admin = TEST_UTIL.getAdmin();
194    assertFalse(admin.tableExists(TableName.valueOf(TABLE2)));
195
196    // create the table
197    model = testTableSchemaModel.buildTestModel(TABLE2);
198    testTableSchemaModel.checkModel(model, TABLE2);
199
200    if (csrfEnabled) {
201      // test put operation is forbidden without custom header
202      response = client.put(schemaPath, Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput());
203      assertEquals(400, response.getCode());
204    }
205    response =
206      client.put(schemaPath, Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput(), extraHdr);
207    assertEquals(201, response.getCode(),
208      "put failed with csrf " + (csrfEnabled ? "enabled" : "disabled"));
209
210    // recall the same put operation but in read-only mode
211    conf.set("hbase.rest.readonly", "true");
212    response =
213      client.put(schemaPath, Constants.MIMETYPE_PROTOBUF, model.createProtobufOutput(), extraHdr);
214    assertNotNull(extraHdr);
215    assertEquals(403, response.getCode());
216
217    // retrieve the schema and validate it
218    response = client.get(schemaPath, Constants.MIMETYPE_PROTOBUF);
219    assertEquals(200, response.getCode());
220    assertEquals(Constants.MIMETYPE_PROTOBUF, response.getHeader("content-type"));
221    model = new TableSchemaModel();
222    model.getObjectFromMessage(response.getBody());
223    testTableSchemaModel.checkModel(model, TABLE2);
224
225    // retrieve the schema and validate it with alternate pbuf type
226    response = client.get(schemaPath, Constants.MIMETYPE_PROTOBUF_IETF);
227    assertEquals(200, response.getCode());
228    assertEquals(Constants.MIMETYPE_PROTOBUF_IETF, response.getHeader("content-type"));
229    model = new TableSchemaModel();
230    model.getObjectFromMessage(response.getBody());
231    testTableSchemaModel.checkModel(model, TABLE2);
232
233    if (csrfEnabled) {
234      // test delete schema operation is forbidden without custom header
235      response = client.delete(schemaPath);
236      assertEquals(400, response.getCode());
237    }
238
239    // test delete schema operation is forbidden in read-only mode
240    response = client.delete(schemaPath, extraHdr);
241    assertEquals(403, response.getCode());
242
243    // return read-only setting back to default
244    conf.set("hbase.rest.readonly", "false");
245
246    // delete the table and make sure HBase concurs
247    response = client.delete(schemaPath, extraHdr);
248    assertEquals(200, response.getCode());
249    assertFalse(admin.tableExists(TableName.valueOf(TABLE2)));
250  }
251}