Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -9,6 +9,7 @@

import dagger.assisted.Assisted;
import dagger.assisted.AssistedInject;
import de.ii.xtraplatform.base.domain.Encryption;
import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry;
import de.ii.xtraplatform.cache.domain.Cache;
import de.ii.xtraplatform.cql.domain.Cql;
Expand Down Expand Up @@ -218,6 +219,7 @@ public FeatureProviderGeoParquet(
VolatileRegistry volatileRegistry,
Cache cache,
Scheduler scheduler,
Encryption encryption,
AuditLog auditLog,
@Assisted FeatureProviderDataV2 data) {
super(
Expand All @@ -233,6 +235,7 @@ public FeatureProviderGeoParquet(
volatileRegistry,
cache,
scheduler,
encryption,
auditLog,
data,
Map.of());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.google.common.collect.ImmutableList;
import dagger.assisted.Assisted;
import dagger.assisted.AssistedInject;
import de.ii.xtraplatform.base.domain.Encryption;
import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry;
import de.ii.xtraplatform.codelists.domain.Codelist;
import de.ii.xtraplatform.cql.domain.Cql;
Expand Down Expand Up @@ -143,6 +144,7 @@ public FeatureProviderWfs(
Reactive reactive,
ValueStore valueStore,
ProviderExtensionRegistry extensionRegistry,
Encryption encryption,
AuditLog auditLog,
VolatileRegistry volatileRegistry,
@Assisted FeatureProviderDataV2 data) {
Expand All @@ -154,6 +156,7 @@ public FeatureProviderWfs(
extensionRegistry,
valueStore.forType(Codelist.class),
auditLog,
encryption,
data,
volatileRegistry);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import dagger.assisted.Assisted;
import dagger.assisted.AssistedInject;
import de.ii.xtraplatform.base.domain.Encryption;
import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry;
import de.ii.xtraplatform.codelists.domain.Codelist;
import de.ii.xtraplatform.cql.domain.Cql;
Expand Down Expand Up @@ -198,6 +199,7 @@ public FeatureProviderGraphQl(
Reactive reactive,
ValueStore valueStore,
ProviderExtensionRegistry extensionRegistry,
Encryption encryption,
AuditLog auditLog,
VolatileRegistry volatileRegistry,
@Assisted FeatureProviderDataV2 data) {
Expand All @@ -209,6 +211,7 @@ public FeatureProviderGraphQl(
extensionRegistry,
valueStore.forType(Codelist.class),
auditLog,
encryption,
data,
volatileRegistry);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import dagger.assisted.Assisted;
import dagger.assisted.AssistedInject;
import de.ii.xtraplatform.base.domain.Encryption;
import de.ii.xtraplatform.base.domain.resiliency.VolatileRegistry;
import de.ii.xtraplatform.cache.domain.Cache;
import de.ii.xtraplatform.cql.domain.Cql;
Expand Down Expand Up @@ -127,6 +128,7 @@ public FeatureProviderOracle(
VolatileRegistry volatileRegistry,
Cache cache,
Scheduler scheduler,
Encryption encryption,
AuditLog auditLog,
@Assisted FeatureProviderDataV2 data) {
super(
Expand All @@ -142,6 +144,7 @@ public FeatureProviderOracle(
volatileRegistry,
cache,
scheduler,
encryption,
auditLog,
data,
Map.of());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import de.ii.xtraplatform.geometries.domain.transcode.wktwkb.WkbDialect;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -289,8 +290,12 @@ private void handleColumns(SqlRow sqlRow) {
getDownstream().onGeometry(context);
}
} else {
Object value = sqlRow.getValues().get(i);
context.setValueType(Type.STRING);
context.setValue((String) sqlRow.getValues().get(i));
context.setValue(
value instanceof byte[]
? Base64.getEncoder().encodeToString((byte[]) value)
: (String) value);
context.setSchemaIndex(sqlRow.getSchemaIndex(i));

if (sqlRow.isSubDecoderColumn(i) && Objects.nonNull(context.value())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import de.ii.xtraplatform.features.domain.SchemaBase.Type;
import de.ii.xtraplatform.features.domain.pipeline.FeatureEventHandlerSimple.ModifiableContext;
import de.ii.xtraplatform.features.domain.pipeline.FeatureTokenEncoderBaseSimple;
import de.ii.xtraplatform.features.domain.transform.PropertyEncryption;
import de.ii.xtraplatform.features.json.domain.JsonBuilder;
import de.ii.xtraplatform.features.sql.domain.SqlQueryColumn;
import de.ii.xtraplatform.features.sql.domain.SqlQueryColumn.Operation;
Expand Down Expand Up @@ -58,6 +59,7 @@ public class FeatureEncoderSql
private final Optional<CrsTransformer> crsTransformer;
private final Optional<ZoneId> timeZone;
private final Optional<String> nullValue;
private final Optional<PropertyEncryption> encryption;
private Map<String, JsonBuilder> jsonColumns;
private final boolean isPatch;
private final boolean trace;
Expand All @@ -78,14 +80,16 @@ public FeatureEncoderSql(
EpsgCrs nativeCrs,
CrsTransformerFactory crsTransformerFactory,
Optional<ZoneId> timeZone,
Optional<String> nullValue) {
Optional<String> nullValue,
Optional<PropertyEncryption> encryption) {
this.mapping = mapping;
this.inputCrs = inputCrs;
this.crsTransformerFactory = crsTransformerFactory;
this.crsTransformer = crsTransformerFactory.getTransformer(inputCrs, nativeCrs);
this.nativeCrs = nativeCrs;
this.timeZone = timeZone;
this.nullValue = nullValue;
this.encryption = encryption;
this.jsonColumns = new LinkedHashMap<>();
this.isPatch = nullValue.isPresent();
this.trace = LOGGER.isTraceEnabled();
Expand Down Expand Up @@ -334,9 +338,18 @@ public void onValue(ModifiableContext<SqlQuerySchema, SqlQueryMapping> context)
column -> {
String value = context.value();

if (timeZone.isPresent()
&& column.second().getType() == Type.DATETIME
&& Objects.nonNull(value)) {
Type columnType = column.second().getType();
boolean encrypted = columnType == Type.ENCRYPTED;
Type logicalType =
encrypted
? Type.valueOf(
column
.second()
.getOperationParameter(
SqlQueryColumn.Operation.ENCRYPT, Type.STRING.name()))
: columnType;

if (timeZone.isPresent() && logicalType == Type.DATETIME && Objects.nonNull(value)) {
value = toTimeZone(context.pathAsString(), value, timeZone.get(), trace);
}

Expand All @@ -351,11 +364,23 @@ public void onValue(ModifiableContext<SqlQuerySchema, SqlQueryMapping> context)

// Numeric/boolean values are validated and re-rendered (never inlined as raw request
// text); string/date values are quoted with quote-doubling. A null stays null so the
// downstream row renderer emits SQL NULL. See SqlLiterals.
value =
Objects.nonNull(value)
? SqlLiterals.forType(column.second().getType(), value)
: value;
// downstream row renderer emits SQL NULL. See SqlLiterals. Values for encrypted
// columns are normalized and encrypted; the patch null sentinel must stay
// recognizable for FeatureDataSql.patchWith and is quoted like a string.
if (Objects.nonNull(value)) {
if (encrypted) {
value =
isPatch && value.equals(nullValue.get())
? SqlLiterals.string(value)
: SqlLiterals.encrypted(
encryption.orElseThrow(FeatureEncoderSql::noEncryptionKey),
logicalType,
value,
column.second().getName());
} else {
value = SqlLiterals.forType(columnType, value);
}
}

boolean junctionElement =
currentArrayJunctionTable != null
Expand All @@ -375,6 +400,11 @@ public void onValue(ModifiableContext<SqlQuerySchema, SqlQueryMapping> context)
});
}

private static IllegalStateException noEncryptionKey() {
return new IllegalStateException(
"The provider has properties of type ENCRYPTED, but no encryptionKey is configured.");
}

@Override
public Class<? extends ModifiableContext<SqlQuerySchema, SqlQueryMapping>> getContextInterface() {
return FeatureEncoderSqlContext.class;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
package de.ii.xtraplatform.features.sql.app;

import de.ii.xtraplatform.features.domain.SchemaBase;
import de.ii.xtraplatform.features.domain.transform.PropertyEncryption;
import java.math.BigDecimal;
import java.util.HexFormat;
import java.util.Locale;

/**
Expand Down Expand Up @@ -36,6 +38,12 @@ static String forType(SchemaBase.Type type, String value) {
return number(value);
case BOOLEAN:
return bool(value);
case ENCRYPTED:
case ENCRYPTED_ARRAY:
// Values for encrypted columns must be rendered with encrypted(), never quoted as-is —
// the quoted plaintext would be silently accepted by the bytea column.
throw new IllegalStateException(
"the value of an encrypted property reached the SQL encoder unencrypted");
case STRING:
case DATE:
case DATETIME:
Expand All @@ -47,6 +55,16 @@ static String forType(SchemaBase.Type type, String value) {
}
}

static String encrypted(
PropertyEncryption encryption, SchemaBase.Type valueType, String value, String propertyName) {
if (value == null) {
return "NULL";
}
byte[] encryptedValue =
encryption.encrypt(encryption.normalize(value, valueType, propertyName));
return "'\\x" + HexFormat.of().formatHex(encryptedValue) + "'";
}

static String string(String value) {
if (value == null) {
return "NULL";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,22 @@ private Map<SqlQueryColumn.Operation, String[]> getColumnOperations(
operations.put(SqlQueryColumn.Operation.DATE, format);
}

if (column.getType() == Type.ENCRYPTED) {
if (sqlPath.isConnected()) {
throw new IllegalArgumentException(
String.format(
"Properties of type ENCRYPTED or ENCRYPTED_ARRAY are not supported in connected columns: %s",
column.getTarget()));
}
// The logical type of the plaintext (valueType) travels as the operation parameter, it is
// consumed by the write path to normalize the value before encryption.
operations.put(
SqlQueryColumn.Operation.ENCRYPT,
new String[] {
propertySchema.flatMap(FeatureSchema::getValueType).orElse(Type.STRING).name()
});
}

return operations;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import de.ii.xtraplatform.features.domain.MappingRule;
import de.ii.xtraplatform.features.domain.SchemaBase;
import de.ii.xtraplatform.features.domain.Tuple;
import de.ii.xtraplatform.features.domain.transform.PropertyEncryption;
import de.ii.xtraplatform.features.sql.domain.FeatureTokenStatsCollector;
import de.ii.xtraplatform.features.sql.domain.SqlQueryColumn;
import de.ii.xtraplatform.features.sql.domain.SqlQueryJoin;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class SqlMutationSession implements FeatureTransactions.Session {
private final EpsgCrs nativeCrs;
private final CrsTransformerFactory crsTransformerFactory;
private final Optional<ZoneId> nativeTimeZone;
private final Optional<PropertyEncryption> encryption;
private final Reactive.Runner streamRunner;

public SqlMutationSession(
Expand All @@ -74,14 +76,16 @@ public SqlMutationSession(
EpsgCrs nativeCrs,
CrsTransformerFactory crsTransformerFactory,
Optional<ZoneId> nativeTimeZone,
Reactive.Runner streamRunner) {
Reactive.Runner streamRunner,
Optional<PropertyEncryption> encryption) {
this.sqlSession = sqlSession;
this.queryMappings = queryMappings;
this.featureMutationsSql = featureMutationsSql;
this.nativeCrs = nativeCrs;
this.crsTransformerFactory = crsTransformerFactory;
this.nativeTimeZone = nativeTimeZone;
this.streamRunner = streamRunner;
this.encryption = encryption;
}

@Override
Expand Down Expand Up @@ -1419,6 +1423,18 @@ private String encodeLiteral(
|| column.hasOperation(SqlQueryColumn.Operation.WKB)) {
return encodeGeometryLiteral(column, value, crs);
}
if (column.getType() == SchemaBase.Type.ENCRYPTED) {
return SqlLiterals.encrypted(
encryption.orElseThrow(
() ->
new IllegalStateException(
"The provider has properties of type ENCRYPTED, but no encryptionKey is configured.")),
SchemaBase.Type.valueOf(
column.getOperationParameter(
SqlQueryColumn.Operation.ENCRYPT, SchemaBase.Type.STRING.name())),
value.asText(),
column.getName());
}
// Numeric/boolean values are validated and re-rendered (never inlined as raw request text);
// everything else is quoted. See SqlLiterals.
return SqlLiterals.forType(column.getType(), value.asText());
Expand Down Expand Up @@ -1636,7 +1652,8 @@ private void drainSource(
nativeCrs,
crsTransformerFactory,
nativeTimeZone,
partial ? Optional.of(FeatureTransactions.PATCH_NULL_VALUE) : Optional.empty()))
partial ? Optional.of(FeatureTransactions.PATCH_NULL_VALUE) : Optional.empty(),
encryption))
.via(Transformer.map(feature -> (FeatureDataSql) feature));

if (partial) {
Expand Down
Loading
Loading