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
4 changes: 4 additions & 0 deletions docs/src/main/sphinx/security/opa-access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ The `context` object contains all other contextual information about the query:
following two fields:
- `user`: username
- `groups`: list of groups this user belongs to
- `queryId`: Query id
- `softwareStack`: Information about the software stack issuing the request to
OPA. The following information is included:
- `trinoVersion`: Version of Trino used
Expand Down Expand Up @@ -158,6 +159,7 @@ Accessing a table results in a query similar to the following example:
"user": "foo",
"groups": ["some-group"]
},
"queryId": "20250718_081710_03427_trino",
"softwareStack": {
"trinoVersion": "434"
}
Expand Down Expand Up @@ -190,6 +192,7 @@ The `targetResource` is used in cases where a new resource, distinct from the on
"user": "foo",
"groups": ["some-group"]
},
"queryId": "20250718_081710_03427_trino",
"softwareStack": {
"trinoVersion": "434"
}
Expand Down Expand Up @@ -373,6 +376,7 @@ A batch column masking request is similar to the following example:
"user": "foo",
"groups": ["some-group"]
},
"queryId": "20250718_081710_03427_trino",
"softwareStack": {
"trinoVersion": "434"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -802,11 +802,11 @@ private static Map<String, Optional<Object>> convertProperties(Map<String, Objec

OpaQueryContext buildQueryContext(Identity trinoIdentity)
{
return new OpaQueryContext(TrinoIdentity.fromTrinoIdentity(trinoIdentity), pluginContext);
return new OpaQueryContext(TrinoIdentity.fromTrinoIdentity(trinoIdentity), pluginContext, Optional.empty());
}

OpaQueryContext buildQueryContext(SystemSecurityContext securityContext)
{
return new OpaQueryContext(TrinoIdentity.fromTrinoIdentity(securityContext.getIdentity()), pluginContext);
return new OpaQueryContext(TrinoIdentity.fromTrinoIdentity(securityContext.getIdentity()), pluginContext, Optional.of(securityContext.getQueryId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@
*/
package io.trino.plugin.opa.schema;

import io.trino.spi.QueryId;

import java.util.Optional;

import static java.util.Objects.requireNonNull;

public record OpaQueryContext(TrinoIdentity identity, OpaPluginContext softwareStack)
public record OpaQueryContext(TrinoIdentity identity, OpaPluginContext softwareStack, Optional<QueryId> queryId)
{
public OpaQueryContext
{
requireNonNull(identity, "identity is null");
requireNonNull(softwareStack, "softwareStack is null");
requireNonNull(queryId, "queryId is null");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.trino.plugin.opa.HttpClientUtils.InstrumentedHttpClient;
import io.trino.plugin.opa.HttpClientUtils.MockResponse;
import io.trino.plugin.opa.schema.OpaViewExpression;
import io.trino.spi.QueryId;
import io.trino.spi.connector.CatalogSchemaName;
import io.trino.spi.connector.CatalogSchemaRoutineName;
import io.trino.spi.connector.CatalogSchemaTableName;
Expand All @@ -37,6 +38,7 @@
import io.trino.spi.type.VarcharType;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -960,6 +962,51 @@ void testGetColumnMasksThrowsForIllegalResponse()
"Failed to deserialize");
}


@Test
public void testQueryIdPropagation()
{
QueryId queryId = new QueryId("20250718_081710_03427_trino");

SystemSecurityContext customSecurityContext = new SystemSecurityContext(TEST_IDENTITY, queryId, Instant.now());
CatalogSchemaTableName tableName = new CatalogSchemaTableName("my_catalog", "my_schema", "my_table");

ThrowingMethodWrapper wrappedMethod = new ThrowingMethodWrapper(accessControl ->
accessControl.checkCanShowCreateTable(customSecurityContext, tableName));

String expectedActionRequest =
"""
{
"operation": "ShowCreateTable",
"resource": {
"table": {
"catalogName": "%s",
"schemaName": "%s",
"tableName": "%s"
}
}
}
""".formatted(
tableName.getCatalogName(),
tableName.getSchemaTableName().getSchemaName(),
tableName.getSchemaTableName().getTableName());

InstrumentedHttpClient mockClient = createMockHttpClient(OPA_SERVER_URI, request -> {
JsonNode contextNode = request.path("input").path("context");

assertThat(contextNode.path("queryId").asText()).isEqualTo(queryId.id());
assertThat(contextNode.path("identity").path("user").asText()).isEqualTo(TEST_IDENTITY.getUser());
assertThat(contextNode.path("softwareStack").path("trinoVersion").asText()).isEqualTo("trino-version");

return OK_RESPONSE;
});

OpaAccessControl authorizer = createOpaAuthorizer(simpleOpaConfig(), mockClient);

assertThat(wrappedMethod.isAccessAllowed(authorizer)).isTrue();
assertStringRequestsEqual(ImmutableSet.of(expectedActionRequest), mockClient.getRequests(), "/input/action");
}

private void testGetColumnMasks(Map<ColumnSchema, String> columnResponseContent, Map<ColumnSchema, OpaViewExpression> expectedResult)
{
InstrumentedHttpClient httpClient = createMockHttpClient(
Expand Down