Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
public class SQLTemplateController {
private final SQLTemplateExecutorService sqlTemplateExecutorService;

@GetMapping("/{fileName}")
public Object sqlTemplate(@PathVariable String dbId,
@PathVariable String fileName,
@RequestParam Map<String, String> requestParams,
@RequestHeader Map<String, String> requestHeaders,
@MatrixVariable Map<String, String> matrixVariables
) {
return executeGetTemplate(dbId, fileName, "", requestParams, requestHeaders, matrixVariables);
}

@GetMapping("/{fileName}/{*userPathVariable}")
public Object sqlTemplate(@PathVariable String dbId,
@PathVariable String fileName,
Expand All @@ -32,6 +42,15 @@ public Object sqlTemplate(@PathVariable String dbId,
@RequestHeader Map<String, String> requestHeaders,
@MatrixVariable Map<String, String> matrixVariables
) {
return executeGetTemplate(dbId, fileName, userPathVariable, requestParams, requestHeaders, matrixVariables);
}

private Object executeGetTemplate(String dbId,
String fileName,
String userPathVariable,
Map<String, String> requestParams,
Map<String, String> requestHeaders,
Map<String, String> matrixVariables) {
final Map<String, Object> context = createContext(requestParams, requestHeaders, matrixVariables);
Map<String, Object> paths = createPaths(userPathVariable, requestHeaders);
context.put("paths", paths);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
public class PostgreSQLContainerConfiguration {

private static final List<String> postgresScripts = List.of("pg/postgres-sakila.sql",
"pg/postgres-sakila-data.sql", "pg/pg-sakila-functions.sql");
"pg/postgres-sakila-data.sql", "pg/pg-sakila-functions.sql",
"pg/postgres-case-sensitive-table.sql");

private static final PostgreSQLContainer testPostgres =
(PostgreSQLContainer) new PostgreSQLContainer("postgres:15.2-alpine")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.homihq.db2rest.rest.pg;

import com.homihq.db2rest.PostgreSQLBaseIntegrationTest;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static com.homihq.db2rest.rest.RdbmsRestApi.VERSION;
import static org.hamcrest.Matchers.hasSize;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class PgCaseSensitiveTableNameTest extends PostgreSQLBaseIntegrationTest {

@Test
@DisplayName("Read a quoted mixed-case PostgreSQL table using its exact URL name")
void readCaseSensitiveTableUsingExactName() throws Exception {
mockMvc.perform(get(VERSION + "/pgsqldb/FilmData")
.accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].film_id").value(916))
.andExpect(jsonPath("$[0].title").value("Case-sensitive table names"));
}

@Test
@DisplayName("Read a quoted mixed-case PostgreSQL table from an explicit schema")
void readCaseSensitiveTableUsingSchemaHeader() throws Exception {
mockMvc.perform(get(VERSION + "/pgsqldb/FilmData")
.header("Accept-Profile", "stg")
.accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].film_id").value(916))
.andExpect(jsonPath("$[0].title").value("Case-sensitive table names"));
}

@Test
@DisplayName("Read a PostgreSQL table with a Unicode name")
void readUnicodeTableNameUsingSchemaHeader() throws Exception {
mockMvc.perform(get(VERSION + "/pgsqldb/überall")
.header("Accept-Profile", "stg")
.accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].film_id").value(917))
.andExpect(jsonPath("$[0].title").value("Unicode table names"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,17 @@ void updateNonExistentRecord() throws Exception {
void updateWithInvalidPayload() throws Exception {

Map<String, Object> invalidUpdate = Map.of(
"language_id", "invalid_value"
"unknown_column", "invalid_value"
);

mockMvc.perform(patch(VERSION + "/sqlitedb/film/1")
mockMvc.perform(patch(VERSION + "/sqlitedb/film")
.param("filter", "film_id==1")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidUpdate)))
.andDo(print())
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errorCategory", equalTo("Data-access-error")))
.andExpect(jsonPath("$.detail", equalTo("Column not found film.unknown_column")))
.andDo(document("sqlite-update-invalid-payload"));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DROP SCHEMA IF EXISTS stg CASCADE;
CREATE SCHEMA stg;

CREATE TABLE stg."FilmData" (
film_id integer PRIMARY KEY,
title character varying(100) NOT NULL
);

INSERT INTO stg."FilmData" (film_id, title)
VALUES (916, 'Case-sensitive table names');

CREATE TABLE stg."überall" (
film_id integer PRIMARY KEY,
title character varying(100) NOT NULL
);

INSERT INTO stg."überall" (film_id, title)
VALUES (917, 'Unicode table names');
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
Expand All @@ -35,6 +36,8 @@
public class JinJavaTemplateExecutorService implements SQLTemplateExecutorService {

private static final String SQL_TEMPLATE_EXTENSION = ".sql";
private static final Pattern SQL_TEMPLATE_NAME_PATTERN =
Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]*");
private static final String PLACEHOLDER_REGEX =
"\\{\\{\\s*([^|}]+?)\\s*(?:\\|\\s*([^}]+?))*\\s*}}";
private final Jinjava jinjava;
Expand Down Expand Up @@ -143,15 +146,20 @@ private Pair<String, Map<String, Object>> executeInternal(String templateFile, M

private String renderJinJavaTemplate(String templateFile, Map<String, Object> context) {
log.debug("Rendering query from template {}", templateFile);
validateTemplateName(templateFile);

synchronized (templateCache) {
if (templateCache.containsKey(templateFile)) {
final String templateContent = templateCache.get(templateFile);
return jinjava.render(templateContent, context);
} else {
final String userTemplateLocation = db2RestConfigProperties.getTemplates();
final Path templatePath = Paths.get(userTemplateLocation, templateFile + SQL_TEMPLATE_EXTENSION);
if (!Files.exists(templatePath)) {
final Path templateDirectory = Paths.get(userTemplateLocation).toAbsolutePath().normalize();
final Path templatePath = templateDirectory
.resolve(templateFile + SQL_TEMPLATE_EXTENSION)
.normalize();
if (!templatePath.startsWith(templateDirectory)
|| !Files.isRegularFile(templatePath, LinkOption.NOFOLLOW_LINKS)) {
throw new SqlTemplateNotFoundException(templateFile);
}
try {
Expand All @@ -166,6 +174,16 @@ private String renderJinJavaTemplate(String templateFile, Map<String, Object> co
}
}

private void validateTemplateName(String templateFile) {
if (templateFile == null
|| templateFile.contains("..")
|| templateFile.contains("/")
|| templateFile.contains("\\")
|| !SQL_TEMPLATE_NAME_PATTERN.matcher(templateFile).matches()) {
throw new SqlTemplateNotFoundException(templateFile);
}
}

private Map<String, Placeholder> extractPlaceHolder(final String renderedSqlTemplate) {
log.debug("Extracting placeholder from sql template");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.homihq.db2rest.jdbc.core.service;

import com.homihq.db2rest.core.exception.SqlTemplateNotFoundException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

class JinJavaTemplateExecutorServiceTest {

private final JinJavaTemplateExecutorService templateExecutorService =
new JinJavaTemplateExecutorService(null, null, null, null, null);

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {
" ",
".hidden",
"../secret",
"..\\secret",
"template..name",
"/absolute/path",
"nested/template",
"nested\\template",
"template name",
"template%2Fname"
})
void rejectsUnsafeTemplateNames(String templateName) {
assertThatThrownBy(() -> templateExecutorService.execute("db", templateName, Map.of()))
.isInstanceOf(SqlTemplateNotFoundException.class);
}
}
Loading