Skip to content

Commit 1fd4a03

Browse files
sashaNeshcheretoctavia-squidington-iiirodireich
authored
Source postgres: fix schema permission issue (#19024)
* Source postgres: fix schema permission issue * Source postgres: add test for schema permission issue * Source postgres: format code * Source postgres: added unit test * Source postgres: bump version * auto-bump connector version Co-authored-by: Octavia Squidington III <[email protected]> Co-authored-by: Rodi Reich Zilberman <[email protected]>
1 parent b72cd71 commit 1fd4a03

File tree

7 files changed

+118
-28
lines changed

7 files changed

+118
-28
lines changed

airbyte-config/init/src/main/resources/seed/source_definitions.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1242,7 +1242,7 @@
12421242
- name: Postgres
12431243
sourceDefinitionId: decd338e-5647-4c0b-adf4-da0e75f5a750
12441244
dockerRepository: airbyte/source-postgres
1245-
dockerImageTag: 1.0.28
1245+
dockerImageTag: 1.0.30
12461246
documentationUrl: https://docs.airbyte.com/integrations/sources/postgres
12471247
icon: postgresql.svg
12481248
sourceType: database

airbyte-config/init/src/main/resources/seed/source_specs.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11313,7 +11313,7 @@
1131311313
supportsNormalization: false
1131411314
supportsDBT: false
1131511315
supported_destination_sync_modes: []
11316-
- dockerImage: "airbyte/source-postgres:1.0.28"
11316+
- dockerImage: "airbyte/source-postgres:1.0.30"
1131711317
spec:
1131811318
documentationUrl: "https://docs.airbyte.com/integrations/sources/postgres"
1131911319
connectionSpecification:

airbyte-integrations/connectors/source-postgres-strict-encrypt/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ ENV APPLICATION source-postgres-strict-encrypt
1616

1717
COPY --from=build /airbyte /airbyte
1818

19-
LABEL io.airbyte.version=1.0.28
19+
LABEL io.airbyte.version=1.0.30
2020
LABEL io.airbyte.name=airbyte/source-postgres-strict-encrypt

airbyte-integrations/connectors/source-postgres/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ ENV APPLICATION source-postgres
1616

1717
COPY --from=build /airbyte /airbyte
1818

19-
LABEL io.airbyte.version=1.0.28
19+
LABEL io.airbyte.version=1.0.30
2020
LABEL io.airbyte.name=airbyte/source-postgres

airbyte-integrations/connectors/source-postgres/src/main/java/io/airbyte/integrations/source/postgres/PostgresSource.java

+1
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ public Set<JdbcPrivilegeDto> getPrivilegesTableForCurrentUser(final JdbcDatabase
381381
FROM pg_class c
382382
JOIN pg_namespace n on c.relnamespace = n.oid
383383
WHERE has_table_privilege(c.oid, 'SELECT')
384+
AND has_schema_privilege(current_user, nspname, 'USAGE')
384385
-- r = ordinary table, i = index, S = sequence, t = TOAST table, v = view, m = materialized view, c = composite type, f = foreign table, p = partitioned table, I = partitioned index
385386
AND relkind in ('r', 'm', 'v', 't', 'f', 'p')
386387
and ((? is null) OR nspname = ?)

airbyte-integrations/connectors/source-postgres/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/PostgresSourceAcceptanceTest.java

+110-23
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
package io.airbyte.integrations.io.airbyte.integration_tests.sources;
66

7+
import static org.junit.jupiter.api.Assertions.assertFalse;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
710
import com.fasterxml.jackson.databind.JsonNode;
811
import com.google.common.collect.ImmutableMap;
912
import com.google.common.collect.Lists;
@@ -16,6 +19,8 @@
1619
import io.airbyte.integrations.standardtest.source.SourceAcceptanceTest;
1720
import io.airbyte.integrations.standardtest.source.TestDestinationEnv;
1821
import io.airbyte.integrations.util.HostPortResolver;
22+
import io.airbyte.protocol.models.AirbyteCatalog;
23+
import io.airbyte.protocol.models.AirbyteRecordMessage;
1924
import io.airbyte.protocol.models.CatalogHelpers;
2025
import io.airbyte.protocol.models.ConfiguredAirbyteCatalog;
2126
import io.airbyte.protocol.models.ConfiguredAirbyteStream;
@@ -24,39 +29,36 @@
2429
import io.airbyte.protocol.models.Field;
2530
import io.airbyte.protocol.models.JsonSchemaType;
2631
import io.airbyte.protocol.models.SyncMode;
32+
import java.sql.SQLException;
2733
import java.util.HashMap;
2834
import java.util.List;
2935
import org.jooq.DSLContext;
3036
import org.jooq.SQLDialect;
37+
import org.junit.jupiter.api.Test;
3138
import org.testcontainers.containers.PostgreSQLContainer;
3239

3340
public class PostgresSourceAcceptanceTest extends SourceAcceptanceTest {
3441

3542
private static final String STREAM_NAME = "public.id_and_name";
3643
private static final String STREAM_NAME2 = "public.starships";
3744
private static final String STREAM_NAME_MATERIALIZED_VIEW = "public.testview";
45+
public static final String LIMIT_PERMISSION_SCHEMA = "limit_perm_schema";
46+
public static final String LIMIT_PERMISSION_ROLE = "limit_perm_role";
47+
public static final String LIMIT_PERMISSION_ROLE_PASSWORD = "test";
3848

3949
private PostgreSQLContainer<?> container;
4050
private JsonNode config;
51+
private Database database;
52+
private ConfiguredAirbyteCatalog configCatalog;
4153

4254
@Override
4355
protected void setupEnvironment(final TestDestinationEnv environment) throws Exception {
4456
container = new PostgreSQLContainer<>("postgres:13-alpine");
4557
container.start();
46-
final JsonNode replicationMethod = Jsons.jsonNode(ImmutableMap.builder()
47-
.put("method", "Standard")
48-
.build());
49-
config = Jsons.jsonNode(ImmutableMap.builder()
50-
.put(JdbcUtils.HOST_KEY, HostPortResolver.resolveHost(container))
51-
.put(JdbcUtils.PORT_KEY, HostPortResolver.resolvePort(container))
52-
.put(JdbcUtils.DATABASE_KEY, container.getDatabaseName())
53-
.put(JdbcUtils.SCHEMAS_KEY, Jsons.jsonNode(List.of("public")))
54-
.put(JdbcUtils.USERNAME_KEY, container.getUsername())
55-
.put(JdbcUtils.PASSWORD_KEY, container.getPassword())
56-
.put(JdbcUtils.SSL_KEY, false)
57-
.put("replication_method", replicationMethod)
58-
.build());
59-
58+
String username = container.getUsername();
59+
String password = container.getPassword();
60+
List<String> schemas = List.of("public");
61+
config = getConfig(username, password, schemas);
6062
try (final DSLContext dslContext = DSLContextFactory.create(
6163
config.get(JdbcUtils.USERNAME_KEY).asText(),
6264
config.get(JdbcUtils.PASSWORD_KEY).asText(),
@@ -66,7 +68,7 @@ protected void setupEnvironment(final TestDestinationEnv environment) throws Exc
6668
container.getFirstMappedPort(),
6769
config.get(JdbcUtils.DATABASE_KEY).asText()),
6870
SQLDialect.POSTGRES)) {
69-
final Database database = new Database(dslContext);
71+
database = new Database(dslContext);
7072

7173
database.query(ctx -> {
7274
ctx.fetch("CREATE TABLE id_and_name(id INTEGER, name VARCHAR(200));");
@@ -76,9 +78,26 @@ protected void setupEnvironment(final TestDestinationEnv environment) throws Exc
7678
ctx.fetch("CREATE MATERIALIZED VIEW testview AS select * from id_and_name where id = '2';");
7779
return null;
7880
});
81+
configCatalog = getCommonConfigCatalog();
7982
}
8083
}
8184

85+
private JsonNode getConfig(String username, String password, List<String> schemas) {
86+
final JsonNode replicationMethod = Jsons.jsonNode(ImmutableMap.builder()
87+
.put("method", "Standard")
88+
.build());
89+
return Jsons.jsonNode(ImmutableMap.builder()
90+
.put(JdbcUtils.HOST_KEY, HostPortResolver.resolveHost(container))
91+
.put(JdbcUtils.PORT_KEY, HostPortResolver.resolvePort(container))
92+
.put(JdbcUtils.DATABASE_KEY, container.getDatabaseName())
93+
.put(JdbcUtils.SCHEMAS_KEY, Jsons.jsonNode(schemas))
94+
.put(JdbcUtils.USERNAME_KEY, username)
95+
.put(JdbcUtils.PASSWORD_KEY, password)
96+
.put(JdbcUtils.SSL_KEY, false)
97+
.put("replication_method", replicationMethod)
98+
.build());
99+
}
100+
82101
@Override
83102
protected void tearDown(final TestDestinationEnv testEnv) {
84103
container.close();
@@ -101,6 +120,71 @@ protected JsonNode getConfig() {
101120

102121
@Override
103122
protected ConfiguredAirbyteCatalog getConfiguredCatalog() {
123+
return configCatalog;
124+
}
125+
126+
@Override
127+
protected JsonNode getState() {
128+
return Jsons.jsonNode(new HashMap<>());
129+
}
130+
131+
@Override
132+
protected boolean supportsPerStream() {
133+
return true;
134+
}
135+
136+
@Test
137+
public void testFullRefreshWithRevokingSchemaPermissions() throws Exception {
138+
prepareEnvForUserWithoutPermissions(database);
139+
140+
config = getConfig(LIMIT_PERMISSION_ROLE, LIMIT_PERMISSION_ROLE_PASSWORD, List.of(LIMIT_PERMISSION_SCHEMA));
141+
final ConfiguredAirbyteCatalog configuredCatalog = getLimitPermissionConfiguredCatalog();
142+
143+
final List<AirbyteRecordMessage> fullRefreshRecords = filterRecords(runRead(configuredCatalog));
144+
final String assertionMessage = "Expected records after full refresh sync for user with schema permission";
145+
assertFalse(fullRefreshRecords.isEmpty(), assertionMessage);
146+
147+
revokeSchemaPermissions(database);
148+
149+
final List<AirbyteRecordMessage> lessPermFullRefreshRecords = filterRecords(runRead(configuredCatalog));
150+
final String assertionMessageWithoutPermission = "Expected no records after full refresh sync for user without schema permission";
151+
assertTrue(lessPermFullRefreshRecords.isEmpty(), assertionMessageWithoutPermission);
152+
153+
}
154+
155+
@Test
156+
public void testDiscoverWithRevokingSchemaPermissions() throws Exception {
157+
prepareEnvForUserWithoutPermissions(database);
158+
revokeSchemaPermissions(database);
159+
config = getConfig(LIMIT_PERMISSION_ROLE, LIMIT_PERMISSION_ROLE_PASSWORD, List.of(LIMIT_PERMISSION_SCHEMA));
160+
161+
runDiscover();
162+
AirbyteCatalog lastPersistedCatalogSecond = getLastPersistedCatalog();
163+
final String assertionMessageWithoutPermission = "Expected no streams after discover for user without schema permissions";
164+
assertTrue(lastPersistedCatalogSecond.getStreams().isEmpty(), assertionMessageWithoutPermission);
165+
}
166+
167+
private void revokeSchemaPermissions(Database database) throws SQLException {
168+
database.query(ctx -> {
169+
ctx.fetch(String.format("REVOKE USAGE ON schema %s FROM %s;", LIMIT_PERMISSION_SCHEMA, LIMIT_PERMISSION_ROLE));
170+
return null;
171+
});
172+
}
173+
174+
private void prepareEnvForUserWithoutPermissions(Database database) throws SQLException {
175+
database.query(ctx -> {
176+
ctx.fetch(String.format("CREATE ROLE %s WITH LOGIN PASSWORD '%s';", LIMIT_PERMISSION_ROLE, LIMIT_PERMISSION_ROLE_PASSWORD));
177+
ctx.fetch(String.format("CREATE SCHEMA %s;", LIMIT_PERMISSION_SCHEMA));
178+
ctx.fetch(String.format("GRANT CONNECT ON DATABASE test TO %s;", LIMIT_PERMISSION_ROLE));
179+
ctx.fetch(String.format("GRANT USAGE ON schema %s TO %s;", LIMIT_PERMISSION_SCHEMA, LIMIT_PERMISSION_ROLE));
180+
ctx.fetch(String.format("CREATE TABLE %s.id_and_name(id INTEGER, name VARCHAR(200));", LIMIT_PERMISSION_SCHEMA));
181+
ctx.fetch(String.format("INSERT INTO %s.id_and_name (id, name) VALUES (1,'picard'), (2, 'crusher'), (3, 'vash');", LIMIT_PERMISSION_SCHEMA));
182+
ctx.fetch(String.format("GRANT SELECT ON table %s.id_and_name TO %s;", LIMIT_PERMISSION_SCHEMA, LIMIT_PERMISSION_ROLE));
183+
return null;
184+
});
185+
}
186+
187+
private ConfiguredAirbyteCatalog getCommonConfigCatalog() {
104188
return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList(
105189
new ConfiguredAirbyteStream()
106190
.withSyncMode(SyncMode.INCREMENTAL)
@@ -131,14 +215,17 @@ protected ConfiguredAirbyteCatalog getConfiguredCatalog() {
131215
.withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)))));
132216
}
133217

134-
@Override
135-
protected JsonNode getState() {
136-
return Jsons.jsonNode(new HashMap<>());
137-
}
138-
139-
@Override
140-
protected boolean supportsPerStream() {
141-
return true;
218+
private ConfiguredAirbyteCatalog getLimitPermissionConfiguredCatalog() {
219+
return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList(
220+
new ConfiguredAirbyteStream()
221+
.withSyncMode(SyncMode.INCREMENTAL)
222+
.withCursorField(Lists.newArrayList("id"))
223+
.withDestinationSyncMode(DestinationSyncMode.APPEND)
224+
.withStream(CatalogHelpers.createAirbyteStream(
225+
LIMIT_PERMISSION_SCHEMA + "." + "id_and_name",
226+
Field.of("id", JsonSchemaType.NUMBER),
227+
Field.of("name", JsonSchemaType.STRING))
228+
.withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)))));
142229
}
143230

144231
}

docs/integrations/sources/postgres.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,9 @@ The root causes is that the WALs needed for the incremental sync has been remove
415415

416416
| Version | Date | Pull Request | Subject |
417417
|:--------|:-----------|:-------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
418-
| 1.0.28 | 2022-11-22 | [19514](https://github.com/airbytehq/airbyte/pull/19514) | Adjust batch selection memory limits databases. |
418+
| 1.0.30 | 2022-11-29 | [19024](https://github.com/airbytehq/airbyte/pull/19024) | Skip tables from schema where user do not have Usage permission during discovery. |
419+
| 1.0.29 | 2022-11-29 | [19623](https://github.com/airbytehq/airbyte/pull/19623) | Mark PSQLException related to using replica that is configured as a hot standby server as config error. |
420+
| 1.0.28 | 2022-11-28 | [19514](https://github.com/airbytehq/airbyte/pull/19514) | Adjust batch selection memory limits databases. |
419421
| 1.0.27 | 2022-11-28 | [16990](https://github.com/airbytehq/airbyte/pull/16990) | Handle arrays data types |
420422
| 1.0.26 | 2022-11-18 | [19551](https://github.com/airbytehq/airbyte/pull/19551) | Fixes bug with ssl modes |
421423
| 1.0.25 | 2022-11-16 | [19004](https://github.com/airbytehq/airbyte/pull/19004) | Use Debezium heartbeats to improve CDC replication of large databases. |

0 commit comments

Comments
 (0)