fix(education): enforce migration role separation
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.module.education.config;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Verifies that the runtime database role cannot forge protected lifecycle tokens.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "yudao.education", name = "enabled", havingValue = "true")
|
||||
public class EducationProtectedTokenRoleValidator implements ApplicationRunner {
|
||||
|
||||
private static final List<String> PROTECTED_TABLES = List.of(
|
||||
"education_question_lifecycle_transition_token",
|
||||
"education_content_node_lifecycle_transition_token",
|
||||
"education_question_collection_lifecycle_transition_token",
|
||||
"education_question_collection_membership_token");
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
validate(dataSource);
|
||||
}
|
||||
|
||||
public static void validate(DataSource dataSource) {
|
||||
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
|
||||
List<String> unsafeTables = jdbcTemplate.queryForList("""
|
||||
SELECT protected.table_name
|
||||
FROM (VALUES (?), (?), (?), (?)) AS protected(table_name)
|
||||
LEFT JOIN pg_namespace namespace ON namespace.nspname = current_schema()
|
||||
LEFT JOIN pg_class token_table
|
||||
ON token_table.relnamespace = namespace.oid
|
||||
AND token_table.relname = protected.table_name
|
||||
AND token_table.relkind IN ('r', 'p')
|
||||
WHERE token_table.oid IS NULL
|
||||
OR pg_has_role(current_user, token_table.relowner, 'MEMBER')
|
||||
OR has_table_privilege(current_user, token_table.oid, 'INSERT,UPDATE,DELETE,TRUNCATE')
|
||||
ORDER BY protected.table_name
|
||||
""", String.class, PROTECTED_TABLES.toArray());
|
||||
if (!unsafeTables.isEmpty()) {
|
||||
throw new IllegalStateException("Education runtime database role must not own or write protected token tables: "
|
||||
+ String.join(", ", unsafeTables));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
-- EDU-006 follow-up: fail closed when adopted Practice tables have incompatible scalar shapes.
|
||||
-- Published migrations V4030-V4060 remain immutable; this validates the post-adoption contract forward-only.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
mismatch TEXT;
|
||||
BEGIN
|
||||
WITH expected(table_name, column_name, data_type, nullable, min_length) AS (
|
||||
VALUES
|
||||
('education_practice_session','id','bigint',false,NULL::INTEGER),
|
||||
('education_practice_session','tenant_id','bigint',false,NULL),
|
||||
('education_practice_session','user_id','bigint',false,NULL),
|
||||
('education_practice_session','client_session_id','character varying',false,36),
|
||||
('education_practice_session','status','character varying',false,20),
|
||||
('education_practice_session','question_count','integer',false,NULL),
|
||||
('education_practice_session','version','integer',false,NULL),
|
||||
('education_practice_question','id','bigint',false,NULL),
|
||||
('education_practice_question','tenant_id','bigint',false,NULL),
|
||||
('education_practice_question','session_id','bigint',false,NULL),
|
||||
('education_practice_question','sequence','integer',false,NULL),
|
||||
('education_practice_question','question_id','character varying',false,64),
|
||||
('education_practice_question','content_version','character varying',false,64),
|
||||
('education_practice_question','stem','text',false,NULL),
|
||||
('education_practice_question','type','character varying',false,32),
|
||||
('education_practice_question','options','jsonb',false,NULL),
|
||||
('education_practice_question','is_answered','boolean',false,NULL),
|
||||
('education_practice_report','id','bigint',false,NULL),
|
||||
('education_practice_report','tenant_id','bigint',false,NULL),
|
||||
('education_practice_report','user_id','bigint',false,NULL),
|
||||
('education_practice_report','session_id','bigint',false,NULL),
|
||||
('education_practice_report','question_count','integer',false,NULL),
|
||||
('education_practice_report','score','integer',false,NULL),
|
||||
('education_practice_report_detail','id','bigint',false,NULL),
|
||||
('education_practice_report_detail','tenant_id','bigint',false,NULL),
|
||||
('education_practice_report_detail','user_id','bigint',false,NULL),
|
||||
('education_practice_report_detail','report_id','bigint',false,NULL),
|
||||
('education_practice_report_detail','session_id','bigint',false,NULL),
|
||||
('education_practice_report_detail','question_id','character varying',false,64),
|
||||
('education_practice_report_detail','sequence','integer',false,NULL),
|
||||
('education_practice_report_detail','stem','text',false,NULL),
|
||||
('education_practice_report_detail','type','character varying',false,32),
|
||||
('education_practice_report_detail','is_correct','boolean',false,NULL),
|
||||
('education_wrong_question','id','bigint',false,NULL),
|
||||
('education_wrong_question','tenant_id','bigint',false,NULL),
|
||||
('education_wrong_question','user_id','bigint',false,NULL),
|
||||
('education_wrong_question','question_id','character varying',false,64),
|
||||
('education_wrong_question','stem','text',false,NULL),
|
||||
('education_wrong_question','type','character varying',false,32),
|
||||
('education_wrong_question','options','jsonb',false,NULL),
|
||||
('education_wrong_question','first_wrong_time','timestamp without time zone',false,NULL),
|
||||
('education_wrong_question','last_wrong_time','timestamp without time zone',false,NULL),
|
||||
('education_wrong_question','wrong_count','integer',false,NULL),
|
||||
('education_wrong_question','master_status','character varying',false,20),
|
||||
('education_favorite','id','bigint',false,NULL),
|
||||
('education_favorite','tenant_id','bigint',false,NULL),
|
||||
('education_favorite','user_id','bigint',false,NULL),
|
||||
('education_favorite','target_type','character varying',false,32),
|
||||
('education_favorite','target_id','character varying',false,64),
|
||||
('education_favorite','content_version','character varying',false,64),
|
||||
('education_favorite','available','boolean',false,NULL),
|
||||
('education_idempotency','id','bigint',false,NULL),
|
||||
('education_idempotency','tenant_id','bigint',false,NULL),
|
||||
('education_idempotency','user_id','bigint',false,NULL),
|
||||
('education_idempotency','operation','character varying',false,32),
|
||||
('education_idempotency','idempotency_key','character varying',false,64),
|
||||
('education_idempotency','request_hash','character varying',false,64),
|
||||
('education_idempotency','session_id','bigint',false,NULL),
|
||||
('education_idempotency','status','character varying',false,20),
|
||||
('education_idempotency','business_payload','jsonb',true,NULL)
|
||||
), actual AS (
|
||||
SELECT e.*, c.data_type AS actual_type, c.is_nullable = 'YES' AS actual_nullable,
|
||||
c.character_maximum_length AS actual_length
|
||||
FROM expected e
|
||||
LEFT JOIN information_schema.columns c
|
||||
ON c.table_schema = current_schema() AND c.table_name = e.table_name
|
||||
AND c.column_name = e.column_name
|
||||
)
|
||||
SELECT STRING_AGG(format('%s.%s expected %s%s%s but found %s%s', table_name, column_name, data_type,
|
||||
CASE WHEN nullable THEN '' ELSE ' NOT NULL' END,
|
||||
CASE WHEN min_length IS NULL THEN '' ELSE format(' length >= %s', min_length) END,
|
||||
COALESCE(actual_type, 'MISSING'),
|
||||
CASE WHEN actual_type IS NULL THEN '' ELSE format(' nullable=%s length=%s', actual_nullable,
|
||||
COALESCE(actual_length::TEXT, 'n/a')) END), '; ' ORDER BY table_name, column_name)
|
||||
INTO mismatch
|
||||
FROM actual
|
||||
WHERE actual_type IS NULL OR actual_type <> data_type OR (NOT nullable AND actual_nullable)
|
||||
OR (min_length IS NOT NULL AND COALESCE(actual_length, 0) < min_length);
|
||||
|
||||
IF mismatch IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'Incompatible adopted Education Practice schema: %', mismatch;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Harden Education classroom tenant triggers and reconcile the System notify-template sequence.
|
||||
-- V4140 and V4160 remain immutable release history.
|
||||
|
||||
CREATE OR REPLACE FUNCTION education_validate_class_relationship_tenant()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $$
|
||||
DECLARE owner_tenant BIGINT;
|
||||
BEGIN
|
||||
EXECUTE format('SELECT tenant_id FROM %I.education_class WHERE id = $1 AND deleted = false', TG_TABLE_SCHEMA)
|
||||
INTO owner_tenant USING NEW.class_id;
|
||||
IF owner_tenant IS NULL OR owner_tenant <> NEW.tenant_id THEN
|
||||
RAISE EXCEPTION 'education class relationship cannot cross tenants';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION education_validate_class_relationship_tenant() FROM PUBLIC;
|
||||
|
||||
CREATE OR REPLACE FUNCTION education_validate_invitation_audit_tenant()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $$
|
||||
DECLARE owner_tenant BIGINT;
|
||||
BEGIN
|
||||
EXECUTE format('SELECT tenant_id FROM %I.education_class_invitation WHERE id = $1', TG_TABLE_SCHEMA)
|
||||
INTO owner_tenant USING NEW.invitation_id;
|
||||
IF owner_tenant IS NULL OR owner_tenant <> NEW.tenant_id THEN
|
||||
RAISE EXCEPTION 'education invitation audit cannot cross tenants';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
REVOKE ALL ON FUNCTION education_validate_invitation_audit_tenant() FROM PUBLIC;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
sequence_name TEXT;
|
||||
maximum_id BIGINT;
|
||||
BEGIN
|
||||
IF to_regclass('system_notify_template') IS NULL THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
SELECT pg_get_serial_sequence(format('%I.system_notify_template', current_schema()), 'id') INTO sequence_name;
|
||||
IF sequence_name IS NULL THEN
|
||||
sequence_name := CASE WHEN to_regclass('system_notify_template_seq') IS NOT NULL
|
||||
THEN format('%I.system_notify_template_seq', current_schema()) ELSE NULL END;
|
||||
END IF;
|
||||
IF sequence_name IS NOT NULL THEN
|
||||
SELECT COALESCE(MAX(id), 1) INTO maximum_id FROM system_notify_template;
|
||||
PERFORM setval(sequence_name::regclass, maximum_id, true);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,18 @@
|
||||
-- EDU-010 forward fix: PUBLIC and tenant-owned collections are lifecycle-managed records.
|
||||
-- V4110 already prevented tenant-owned physical deletion; replace the function without
|
||||
-- changing the published migration checksum so adopted PUBLIC rows receive the same guard.
|
||||
|
||||
CREATE OR REPLACE FUNCTION education_prevent_question_collection_delete()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = pg_catalog, pg_temp
|
||||
AS $$
|
||||
BEGIN
|
||||
IF OLD.scope IN ('PUBLIC', 'TENANT_OWNED') THEN
|
||||
RAISE EXCEPTION 'collection cannot be physically deleted' USING ERRCODE = '23514';
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION education_prevent_question_collection_delete() FROM PUBLIC;
|
||||
@@ -1,8 +1,10 @@
|
||||
package cn.iocoder.yudao.module.education.test;
|
||||
|
||||
import cn.iocoder.yudao.module.education.config.EducationProtectedTokenRoleValidator;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.postgresql.ds.PGSimpleDataSource;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
@@ -119,7 +121,7 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
|
||||
assertThat(queryStrings(schema,
|
||||
"SELECT COALESCE(version, 'BASELINE') FROM flyway_schema_history ORDER BY installed_rank"))
|
||||
.containsExactly("4009", "4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170");
|
||||
.containsExactly("4009", "4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170", "4180", "4190", "4200", "4210");
|
||||
assertThat(queryLong(schema,
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = current_schema() " +
|
||||
"AND table_name = 'education_idempotency'"))
|
||||
@@ -171,6 +173,31 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
.isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectIncompatibleAdoptedPracticeScalarShape() throws SQLException {
|
||||
String schema = createSchema("bad_shape");
|
||||
createCompatibleManualPracticeFixture(schema);
|
||||
execute(schema, "ALTER TABLE education_practice_session ALTER COLUMN tenant_id DROP NOT NULL");
|
||||
|
||||
assertThatThrownBy(() -> configureFlyway(schema, true).load().migrate())
|
||||
.hasMessageContaining("Incompatible adopted Education Practice schema")
|
||||
.hasMessageContaining("education_practice_session.tenant_id");
|
||||
assertThat(queryLong(schema,
|
||||
"SELECT COUNT(*) FROM flyway_schema_history WHERE version = '4180' AND success = TRUE"))
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectUndersizedAdoptedPracticeIdentifier() throws SQLException {
|
||||
String schema = createSchema("bad_length");
|
||||
createCompatibleManualPracticeFixture(schema);
|
||||
execute(schema, "ALTER TABLE education_practice_session ALTER COLUMN client_session_id TYPE VARCHAR(16)");
|
||||
|
||||
assertThatThrownBy(() -> configureFlyway(schema, true).load().migrate())
|
||||
.hasMessageContaining("Incompatible adopted Education Practice schema")
|
||||
.hasMessageContaining("education_practice_session.client_session_id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateClassRelationshipSchemaAndEnforceTenantBoundary() throws SQLException {
|
||||
String schema = createSchema("classes");
|
||||
@@ -211,7 +238,7 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
|
||||
assertThat(queryStrings(schema,
|
||||
"SELECT version FROM flyway_schema_history WHERE success = TRUE ORDER BY installed_rank"))
|
||||
.containsExactly("4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170");
|
||||
.containsExactly("4010", "4020", "4030", "4040", "4050", "4060", "4070", "4080", "4090", "4100", "4110", "4120", "4130", "4140", "4150", "4160", "4170", "4180", "4190", "4200", "4210");
|
||||
assertThat(queryStrings(schema,
|
||||
"SELECT table_name FROM information_schema.tables " +
|
||||
"WHERE table_schema = current_schema() AND table_name IN (" +
|
||||
@@ -748,6 +775,62 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPhysicalDeleteOfAdoptedEmptyPublicCollectionForRuntimeRole() throws SQLException {
|
||||
String schema = createSchema("public_collection_delete");
|
||||
configureFlyway(schema, false).target("4100").load().migrate();
|
||||
execute(schema, """
|
||||
INSERT INTO education_content_entry(id,tenant_id,scope,entry_key,name,entry_type)
|
||||
VALUES(100,0,'PUBLIC','questions','Questions','question');
|
||||
INSERT INTO education_content_node(id,tenant_id,scope,entry_id,name,node_type,publication_status,is_active,is_hidden)
|
||||
VALUES(200,0,'PUBLIC',100,'Public node','category','ACTIVE',true,false);
|
||||
INSERT INTO education_question_collection(id,tenant_id,scope,entry_id,node_id,name,collection_type,question_count,is_active,is_hidden)
|
||||
VALUES(300,0,'PUBLIC',100,200,'Adopted empty','MANUAL',0,true,false);
|
||||
""");
|
||||
configureFlyway(schema, false).load().migrate();
|
||||
|
||||
String runtimeRole = "edu_delete_runtime_" + UUID.randomUUID().toString().replace("-", "");
|
||||
try {
|
||||
execute(schema, "CREATE ROLE " + runtimeRole + " LOGIN PASSWORD 'runtime_test'");
|
||||
execute(schema, "GRANT USAGE ON SCHEMA " + schema + " TO " + runtimeRole);
|
||||
execute(schema, "GRANT SELECT, DELETE ON " + schema + ".education_question_collection TO " + runtimeRole);
|
||||
try (Connection connection = DriverManager.getConnection(jdbcUrl(schema), runtimeRole, "runtime_test");
|
||||
var statement = connection.createStatement()) {
|
||||
assertThatThrownBy(() -> statement.execute("DELETE FROM education_question_collection WHERE id=300"))
|
||||
.hasMessageContaining("collection cannot be physically deleted");
|
||||
}
|
||||
} finally {
|
||||
execute(schema, "DROP OWNED BY " + runtimeRole + "; DROP ROLE " + runtimeRole);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRuntimeRoleThatOwnsProtectedTokenTablesAndAcceptSeparatedRole() throws SQLException {
|
||||
String schema = createSchema("token_role_separation");
|
||||
configureFlyway(schema, false).load().migrate();
|
||||
PGSimpleDataSource ownerDataSource = dataSource(schema, USER, PASSWORD);
|
||||
assertThatThrownBy(() -> EducationProtectedTokenRoleValidator.validate(ownerDataSource))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("education_question_lifecycle_transition_token")
|
||||
.hasMessageContaining("education_content_node_lifecycle_transition_token")
|
||||
.hasMessageContaining("education_question_collection_lifecycle_transition_token")
|
||||
.hasMessageContaining("education_question_collection_membership_token");
|
||||
|
||||
String runtimeRole = "edu_safe_runtime_" + UUID.randomUUID().toString().replace("-", "");
|
||||
try {
|
||||
execute(schema, "CREATE ROLE " + runtimeRole + " LOGIN PASSWORD 'runtime_test'");
|
||||
execute(schema, "GRANT USAGE ON SCHEMA " + schema + " TO " + runtimeRole);
|
||||
execute(schema, "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA " + schema + " TO " + runtimeRole);
|
||||
execute(schema, "REVOKE ALL ON " + schema + ".education_question_lifecycle_transition_token, "
|
||||
+ schema + ".education_content_node_lifecycle_transition_token, "
|
||||
+ schema + ".education_question_collection_lifecycle_transition_token, "
|
||||
+ schema + ".education_question_collection_membership_token FROM " + runtimeRole);
|
||||
EducationProtectedTokenRoleValidator.validate(dataSource(schema, runtimeRole, "runtime_test"));
|
||||
} finally {
|
||||
execute(schema, "DROP OWNED BY " + runtimeRole + "; DROP ROLE " + runtimeRole);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseMigrationLocksThatBlockMembershipAndQuestionWrites() throws SQLException {
|
||||
String schema = createSchema("collection_lock_modes");
|
||||
@@ -1335,6 +1418,14 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static PGSimpleDataSource dataSource(String schema, String user, String password) {
|
||||
PGSimpleDataSource dataSource = new PGSimpleDataSource();
|
||||
dataSource.setUrl(jdbcUrl(schema));
|
||||
dataSource.setUser(user);
|
||||
dataSource.setPassword(password);
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
private static String jdbcUrl(String schema) {
|
||||
return adminUrl() + "?currentSchema=" + schema + "&stringtype=unspecified";
|
||||
}
|
||||
|
||||
@@ -47,15 +47,21 @@ public abstract class PostgreSqlDbIntegrationTest {
|
||||
private static final String USER = requiredEnv("EDU_TEST_POSTGRES_USER");
|
||||
private static final String PASSWORD = requiredEnv("EDU_TEST_POSTGRES_PASSWORD");
|
||||
private static final String SCHEMA = "edu_test_" + UUID.randomUUID().toString().replace("-", "");
|
||||
private static final String ROLE_SUFFIX = UUID.randomUUID().toString().replace("-", "");
|
||||
private static final String FLYWAY_ROLE = "edu_flyway_" + ROLE_SUFFIX;
|
||||
private static final String RUNTIME_ROLE = "edu_runtime_" + ROLE_SUFFIX;
|
||||
private static final String ROLE_PASSWORD = "education_test";
|
||||
|
||||
@BeforeAll
|
||||
static void createSchema() throws SQLException {
|
||||
try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD);
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute("CREATE SCHEMA " + SCHEMA);
|
||||
statement.execute("CREATE ROLE " + FLYWAY_ROLE + " LOGIN PASSWORD '" + ROLE_PASSWORD + "'");
|
||||
statement.execute("CREATE ROLE " + RUNTIME_ROLE + " LOGIN PASSWORD '" + ROLE_PASSWORD + "'");
|
||||
statement.execute("CREATE SCHEMA " + SCHEMA + " AUTHORIZATION " + FLYWAY_ROLE);
|
||||
}
|
||||
Flyway.configure()
|
||||
.dataSource(jdbcUrl(), USER, PASSWORD)
|
||||
.dataSource(jdbcUrl(FLYWAY_ROLE), FLYWAY_ROLE, ROLE_PASSWORD)
|
||||
.locations("classpath:db/migration/education")
|
||||
.schemas(SCHEMA)
|
||||
.defaultSchema(SCHEMA)
|
||||
@@ -65,6 +71,16 @@ public abstract class PostgreSqlDbIntegrationTest {
|
||||
.outOfOrder(false)
|
||||
.load()
|
||||
.migrate();
|
||||
try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD);
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute("GRANT USAGE ON SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE);
|
||||
statement.execute("GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE);
|
||||
statement.execute("GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA " + SCHEMA + " TO " + RUNTIME_ROLE);
|
||||
statement.execute("REVOKE ALL ON " + SCHEMA + ".education_question_lifecycle_transition_token, "
|
||||
+ SCHEMA + ".education_content_node_lifecycle_transition_token, "
|
||||
+ SCHEMA + ".education_question_collection_lifecycle_transition_token, "
|
||||
+ SCHEMA + ".education_question_collection_membership_token FROM " + RUNTIME_ROLE);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
@@ -72,6 +88,10 @@ public abstract class PostgreSqlDbIntegrationTest {
|
||||
try (var connection = DriverManager.getConnection(adminUrl(), USER, PASSWORD);
|
||||
var statement = connection.createStatement()) {
|
||||
statement.execute("DROP SCHEMA IF EXISTS " + SCHEMA + " CASCADE");
|
||||
statement.execute("DROP OWNED BY " + RUNTIME_ROLE);
|
||||
statement.execute("DROP ROLE " + RUNTIME_ROLE);
|
||||
statement.execute("DROP OWNED BY " + FLYWAY_ROLE);
|
||||
statement.execute("DROP ROLE " + FLYWAY_ROLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,14 +99,18 @@ public abstract class PostgreSqlDbIntegrationTest {
|
||||
static void postgresqlProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", PostgreSqlDbIntegrationTest::jdbcUrl);
|
||||
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
|
||||
registry.add("spring.datasource.username", () -> USER);
|
||||
registry.add("spring.datasource.password", () -> PASSWORD);
|
||||
registry.add("spring.datasource.username", () -> RUNTIME_ROLE);
|
||||
registry.add("spring.datasource.password", () -> ROLE_PASSWORD);
|
||||
registry.add("spring.sql.init.mode", () -> "never");
|
||||
}
|
||||
|
||||
private static String jdbcUrl() {
|
||||
return jdbcUrl(RUNTIME_ROLE);
|
||||
}
|
||||
|
||||
private static String jdbcUrl(String role) {
|
||||
return "jdbc:postgresql://" + HOST + ":" + PORT + "/" + DATABASE
|
||||
+ "?currentSchema=" + SCHEMA + "&stringtype=unspecified";
|
||||
+ "?currentSchema=" + SCHEMA + "&stringtype=unspecified&ApplicationName=" + role;
|
||||
}
|
||||
|
||||
private static String adminUrl() {
|
||||
|
||||
@@ -16,7 +16,6 @@ TRUNCATE TABLE
|
||||
education_content_import_asset,
|
||||
education_question_collection_lifecycle_audit,
|
||||
education_content_node_lifecycle_audit,
|
||||
education_question_lifecycle_transition_token,
|
||||
education_question_lifecycle_audit,
|
||||
education_question_version,
|
||||
education_question_collection_question,
|
||||
@@ -33,4 +32,4 @@ TRUNCATE TABLE
|
||||
education_content_node,
|
||||
education_content_entry,
|
||||
education_subject
|
||||
RESTART IDENTITY CASCADE;
|
||||
CASCADE;
|
||||
|
||||
@@ -26,8 +26,8 @@ spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
url: ${FLYWAY_URL:${spring.datasource.dynamic.datasource.master.url}}
|
||||
user: ${FLYWAY_USER:${spring.datasource.dynamic.datasource.master.username}}
|
||||
password: ${FLYWAY_PASSWORD:${spring.datasource.dynamic.datasource.master.password}}
|
||||
user: ${FLYWAY_USER}
|
||||
password: ${FLYWAY_PASSWORD}
|
||||
locations: classpath:db/migration
|
||||
table: flyway_schema_history
|
||||
baseline-on-migrate: true
|
||||
|
||||
@@ -26,8 +26,8 @@ spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
url: ${FLYWAY_URL:${spring.datasource.dynamic.datasource.master.url}}
|
||||
user: ${FLYWAY_USER:${spring.datasource.dynamic.datasource.master.username}}
|
||||
password: ${FLYWAY_PASSWORD:${spring.datasource.dynamic.datasource.master.password}}
|
||||
user: ${FLYWAY_USER}
|
||||
password: ${FLYWAY_PASSWORD}
|
||||
locations: classpath:db/migration
|
||||
table: flyway_schema_history
|
||||
baseline-on-migrate: true
|
||||
|
||||
Reference in New Issue
Block a user