forked from wangziqi/ruoyi-vue-pro
fix(education): harden manual collection authoring
This commit is contained in:
@@ -46,7 +46,7 @@ class QuestionCollectionAuthoringControllerContractTest {
|
||||
|
||||
private QuestionCollectionDraftReqVO draft() {
|
||||
QuestionCollectionDraftReqVO r = new QuestionCollectionDraftReqVO();
|
||||
r.setNodeId(100L); r.setName("manual-set"); r.setTitle("Manual set"); r.setCollectionType("MANUAL");
|
||||
r.setNodeId(100L); r.setName("manual-set"); r.setTitle("Manual set");
|
||||
return r;
|
||||
}
|
||||
private QuestionCollectionMembershipReqVO membership() {
|
||||
|
||||
@@ -82,6 +82,14 @@ class QuestionCollectionAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbI
|
||||
assertEquals(0L, collectionMapper.selectCount());
|
||||
}
|
||||
|
||||
@Test void oversizedMembershipFailsBeforePersistence() {
|
||||
Long id = service.createDraft(command(null));
|
||||
List<Long> oversized = java.util.stream.LongStream.rangeClosed(1, 1001).boxed().toList();
|
||||
ServiceException tooLarge = assertThrows(ServiceException.class, () -> service.replaceMembership(id, oversized, 0));
|
||||
assertEquals(QUESTION_COLLECTION_MEMBERSHIP_TOO_LARGE.getCode(), tooLarge.getCode());
|
||||
assertEquals(0, collectionMapper.selectTenantOwnedById(10L, id).getAuthoringVersion());
|
||||
}
|
||||
|
||||
@Test void otherTenantCannotManageCollection() {
|
||||
Long id = service.createDraft(command(null));
|
||||
TenantContextHolder.setTenantId(20L);
|
||||
@@ -99,7 +107,7 @@ class QuestionCollectionAuthoringPostgreSqlIntegrationTest extends PostgreSqlDbI
|
||||
}
|
||||
|
||||
private QuestionCollectionAuthoringCommand command(Integer version) {
|
||||
return new QuestionCollectionAuthoringCommand(activeNodeId, "manual", "Manual", "MANUAL", 30, "{\"tier\":\"vip\"}", 0, null, version);
|
||||
return new QuestionCollectionAuthoringCommand(activeNodeId, "manual", "Manual", 30, "{\"tier\":\"vip\"}", 0, null, version);
|
||||
}
|
||||
private Long createPublishedQuestion() {
|
||||
Long id = questionService.createDraft(new cn.iocoder.yudao.module.education.service.question.authoring.QuestionDraftCommand(
|
||||
|
||||
@@ -675,6 +675,51 @@ class EducationFlywayMigrationIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHardenManualCollectionLifecycleAndRuntimePrivileges() throws SQLException {
|
||||
String schema = createSchema("collection_guards");
|
||||
configureFlyway(schema, false).load().migrate();
|
||||
assertThat(queryLong(schema, """
|
||||
SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
|
||||
WHERE n.nspname=current_schema() AND p.proname LIKE 'education_%question_collection%'
|
||||
AND p.proconfig @> ARRAY['search_path=pg_catalog, pg_temp']::text[]
|
||||
AND NOT has_function_privilege('public', p.oid, 'EXECUTE')
|
||||
""")).isGreaterThanOrEqualTo(5L);
|
||||
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,'Node','category','ACTIVE',true,false);
|
||||
INSERT INTO education_question_collection(id,tenant_id,scope,entry_id,node_id,name,collection_type,question_count,publication_status,is_active,is_hidden)
|
||||
VALUES (300,10,'TENANT_OWNED',100,200,'Manual','MANUAL',0,'DRAFT',false,true);
|
||||
""");
|
||||
assertThatThrownBy(() -> execute(schema, "UPDATE education_question_collection SET publication_status='ACTIVE',is_active=true,is_hidden=false,authoring_version=1 WHERE id=300"))
|
||||
.hasMessageContaining("collection lifecycle audit required");
|
||||
assertThatThrownBy(() -> execute(schema, "UPDATE education_question_collection SET collection_type='DYNAMIC',authoring_version=1 WHERE id=300"))
|
||||
.hasMessageContaining("tenant collection type is immutable MANUAL");
|
||||
|
||||
String runtimeRole = "edu_collection_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, "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA " + schema + " TO " + runtimeRole);
|
||||
execute(schema, "REVOKE ALL ON " + schema + ".education_question_collection_lifecycle_transition_token, " + schema + ".education_question_collection_membership_token FROM " + runtimeRole);
|
||||
try (Connection connection = DriverManager.getConnection(jdbcUrl(schema), runtimeRole, "runtime_test"); var statement = connection.createStatement()) {
|
||||
statement.execute("""
|
||||
DO $activate$ BEGIN
|
||||
UPDATE education_question_collection SET publication_status='ACTIVE',is_active=true,is_hidden=false,authoring_version=1 WHERE id=300;
|
||||
INSERT INTO education_question_collection_lifecycle_audit(tenant_id,collection_id,authoring_version,actor_id,from_status,to_status) VALUES(10,300,1,7,'DRAFT','ACTIVE');
|
||||
END $activate$;
|
||||
""");
|
||||
assertThatThrownBy(() -> statement.execute("INSERT INTO education_question_collection_lifecycle_transition_token VALUES(1,10,300,2,'ACTIVE','ARCHIVED')"))
|
||||
.hasMessageContaining("permission denied");
|
||||
assertThatThrownBy(() -> statement.execute("UPDATE education_question_collection_lifecycle_audit SET actor_id=8 WHERE collection_id=300"))
|
||||
.hasMessageContaining("append-only");
|
||||
}
|
||||
} finally {
|
||||
execute(schema, "DROP OWNED BY " + runtimeRole + "; DROP ROLE " + runtimeRole);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNormalizeHistoricalQuestionVisibilityIntoLifecycleStates() throws SQLException {
|
||||
String schema = createSchema("question_history");
|
||||
|
||||
Reference in New Issue
Block a user