package jnpf.dmsMapper; import jnpf.dmsEntity.fenfa.model.DmsWenjianDayinTaizhangQuery; import jnpf.dmsPermission.DmsUserFolderPermissionTreeQuery; import jnpf.dmsPermission.DmsUserFolderPermissionPrincipal; import jnpf.dmsDatabase.DmsUuidTypeHandler; import jnpf.util.JsonUtil; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.scripting.defaults.DefaultParameterHandler; import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.*; import static org.junit.jupiter.api.Assertions.*; /** Executes the production mapper SQL against virtual rows, inside a read-only PostgreSQL transaction. */ @EnabledIfEnvironmentVariable(named = "DMS_LEDGER_TEST_JDBC_URL", matches = ".+") class DmsWenjianDayinTaizhangPostgresTest { private Configuration configuration; private String fixture; private String exclusions = "SELECT NULL::text tenant_id, NULL::text user_id, NULL::text action_code, " + "NULL::text resource_type, NULL::uuid resource_id, NULL::timestamptz revoked_at, " + "NULL::timestamptz valid_from, NULL::timestamptz valid_to WHERE FALSE"; @BeforeEach void setup() throws Exception { configuration = new Configuration(); configuration.getTypeHandlerRegistry().register(UUID.class, DmsUuidTypeHandler.class); try (InputStream xml = getClass().getResourceAsStream("/mapper/DmsUserFolderPermissionTreeMapper.xml")) { new XMLMapperBuilder(xml, configuration, "mapper/DmsUserFolderPermissionTreeMapper.xml", configuration.getSqlFragments()).parse(); } try (InputStream xml = getClass().getResourceAsStream("/mapper/DmsWenjianDayinTaizhangMapper.xml")) { new XMLMapperBuilder(xml, configuration, "mapper/DmsWenjianDayinTaizhangMapper.xml", configuration.getSqlFragments()).parse(); } try (InputStream sql = getClass().getResourceAsStream("/print-ledger-fixture.sql")) { fixture = new String(sql.readAllBytes(), StandardCharsets.UTF_8); } } @Test void readExclusionsOverrideDirectoryPersonalAndOverlappingGrants() throws Exception { for (String user : Arrays.asList("self", "subtree", "applicant")) { for (String type : Arrays.asList("DOCUMENT", "FILE_VERSION")) { exclusions = exclusion(user, type, "DOCUMENT".equals(type) ? 201 : 101); DmsWenjianDayinTaizhangQuery q = query("USER", user); q.setTaskId("task-01"); assertEquals(0, total(q), user + "/" + type); assertTrue(ids(q).isEmpty()); // Even when both allow branches match, an exclusion must win. q.setFolderIdsJson(query("USER", "subtree").getFolderIdsJson()); assertEquals(0, total(q)); assertTrue(ids(q).isEmpty()); } } } @Test void exclusionsApplyBeforeCountingPaginationSearchAndEcho() throws Exception { exclusions = exclusion("subtree", "DOCUMENT", 230); DmsWenjianDayinTaizhangQuery q = query("USER", "subtree"); q.setLimit(20); assertEquals(29, total(q)); assertEquals(20, ids(q).size()); assertEquals("task-29", ids(q).get(0)); q.setOffset(20); assertEquals(9, ids(q).size()); q.setOffset(0); q.setSearch("code-30"); assertEquals(0, total(q)); assertTrue(ids(q).isEmpty()); q.setSearch(null); q.setTaskId("task-30"); assertEquals(0, total(q)); assertTrue(ids(q).isEmpty()); q.setTaskId("task-29"); assertEquals(Collections.singletonList("task-29"), ids(q)); } @Test void inactiveOrUnrelatedExclusionsPreserveExistingVisibility() throws Exception { for (String user : Arrays.asList("self", "applicant")) { String active = exclusion(user, "DOCUMENT", 201); for (String ignored : Arrays.asList( active.replace("NULL::timestamptz revoked_at", "now() revoked_at"), active.replace("now()-interval '1 day'", "now()+interval '1 day'"), active.replace("NULL::timestamptz valid_to", "now() valid_to"), active.replace("'tenant-a'", "'tenant-b'"), active.replace("'" + user + "'", "'another-user'"), active.replace("'READ'", "'DOWNLOAD'"), active.replace("'DOCUMENT'", "'FILE_VERSION'"), exclusion(user, "DOCUMENT", 999))) { exclusions = ignored; DmsWenjianDayinTaizhangQuery q = query("USER", user); assertEquals(1, total(q), ignored); assertEquals(Collections.singletonList("task-01"), ids(q)); } exclusions = active.replace("NULL::timestamptz valid_to", "now()+interval '1 day' valid_to"); assertEquals(0, total(query("USER", user))); // Active finite exclusion. exclusions = active.replace("now()-interval '1 day'", "now()"); assertEquals(0, total(query("USER", user))); // Inclusive valid_from boundary. } } @Test void versionExclusionDoesNotHideOtherVersionsButDocumentExclusionDoes() throws Exception { // task-02 is a newer version of task-01's document; use version snapshots, not latest resource state. fixture = fixture.replace("'file-' || n wenjian_id", "'file-' || CASE WHEN n=2 THEN 1 ELSE n END wenjian_id") .replace("'D(00)'::text banbenhao", "CASE WHEN n=2 THEN 'D(01)' ELSE 'D(00)' END banbenhao") .replace("(200+n)", "(200+CASE WHEN n=2 THEN 1 ELSE n END)") .replace("WHEN n=8 THEN", "WHEN n=2 OR n=8 THEN"); DmsWenjianDayinTaizhangQuery q = query("USER", "self"); assertEquals(Arrays.asList("task-02", "task-01"), ids(q)); exclusions = exclusion("self", "FILE_VERSION", 102); assertEquals(Collections.singletonList("task-01"), ids(q)); exclusions = exclusion("self", "FILE_VERSION", 101); assertEquals(Collections.singletonList("task-02"), ids(q)); exclusions = exclusion("self", "DOCUMENT", 201); assertEquals(0, total(q)); assertTrue(ids(q).isEmpty()); } private String exclusion(String user, String type, int resource) { return "SELECT 'tenant-a'::text tenant_id, '" + user + "'::text user_id, 'READ'::text action_code, '" + type + "'::text resource_type, '00000000-0000-0000-0000-" + String.format(Locale.ROOT, "%012d", resource) + "'::uuid resource_id, " + "NULL::timestamptz revoked_at, now()-interval '1 day' valid_from, NULL::timestamptz valid_to"; } @Test void personalApplicationRequiresOwnershipApprovalAndMatchingSourceVersion() throws Exception { DmsWenjianDayinTaizhangQuery q = query("USER", "applicant"); assertEquals("[]", q.getFolderIdsJson()); assertEquals(Collections.singletonList("task-01"), ids(q)); assertEquals(1, total(q)); q.setTaskId("task-02"); assertEquals(0, total(q)); // Same version grant cannot expose another applicant's task. q.setTaskId("task-01"); q.setSearch("no-match"); assertEquals(0, total(q)); q.setSearch("code-1"); assertEquals(1, total(q)); q.setOffset(1); assertTrue(ids(q).isEmpty()); q.setTenantId("tenant-b"); assertEquals(0, total(q)); } @Test void overlappingDirectoryAndPersonalBranchesDoNotDuplicateTasks() throws Exception { DmsWenjianDayinTaizhangQuery q = query("USER", "applicant"); q.setFolderIdsJson(query("USER", "subtree").getFolderIdsJson()); assertEquals(30, total(q)); q.setLimit(20); assertEquals(20, ids(q).size()); q.setOffset(20); assertEquals(10, ids(q).size()); q.setTaskId("task-01"); assertEquals(1, total(q)); } @Test void inactiveOrUnrelatedPersonalGrantsNeverExposeApplicationTasks() throws Exception { String original = fixture; String grant = "NULL::timestamptz, now()-interval '1 day', NULL::timestamptz, NULL::integer, 0, 'WORKFLOW'"; for (String invalid : Arrays.asList( "now(), now()-interval '1 day', NULL::timestamptz, NULL::integer, 0, 'WORKFLOW'", "NULL::timestamptz, now()+interval '1 day', NULL::timestamptz, NULL::integer, 0, 'WORKFLOW'", "NULL::timestamptz, now()-interval '1 day', now(), NULL::integer, 0, 'WORKFLOW'", "NULL::timestamptz, now()-interval '1 day', NULL::timestamptz, 1, 1, 'WORKFLOW'", "NULL::timestamptz, now()-interval '1 day', NULL::timestamptz, NULL::integer, 0, 'MANUAL'")) { fixture = original.replace(grant, invalid); assertEquals(0, total(query("USER", "applicant")), invalid); } for (String[] replacement : Arrays.asList( new String[]{"'USER', 'applicant'", "'USER', 'other-user'"}, new String[]{"'tenant-a', 'USER', 'applicant'", "'tenant-b', 'USER', 'applicant'"}, new String[]{"'SELF', 'PRINT'", "'SELF', 'READ'"}, new String[]{"'FILE_VERSION'", "'FILE'"})) { fixture = original.replace(replacement[0], replacement[1]); assertEquals(0, total(query("USER", "applicant"))); } } @Test void selfDoesNotInheritAndNavigationAncestorsDoNotGrantAccess() throws Exception { assertEquals(Arrays.asList("task-01"), ids(query("USER", "self"))); assertEquals(Arrays.asList("task-25"), ids(query("USER", "leaf"))); for (String user : Arrays.asList("read-only", "unknown", "revoked", "expired", "future", "exhausted")) { assertEquals(0, total(query("USER", user)), user); } } @Test void inheritedPermissionsAreDeduplicatedAndPaginatedAfterFiltering() throws Exception { DmsWenjianDayinTaizhangQuery q = query("USER", "subtree"); assertEquals(30, total(q)); // Excludes missing/deleted resources, cross-tenant resources and canceled tasks. q.setLimit(20); List> first = rows("selectPage", q); assertEquals(20, first.size()); assertEquals("task-30", first.get(0).get("f_id")); assertEquals(1, ((Number)first.get(0).get("yidayin_fenshu")).intValue()); assertEquals(2, ((Number)first.get(0).get("weidayin_fenshu")).intValue()); q.setOffset(20); assertEquals(10, ids(q).size()); q.setOffset(40); assertTrue(ids(q).isEmpty()); assertEquals(30, total(q)); } @Test void organizationRolePositionAndGroupKindsMatchTreeRules() throws Exception { assertEquals(30, total(query("ORG_DIRECT", "org-direct"))); assertEquals(0, total(query("ORG_ANCESTOR", "org-direct"))); assertEquals(30, total(query("ORG_ANCESTOR", "org-parent"))); assertEquals(0, total(query("ORG_DIRECT", "org-parent"))); assertEquals(30, total(query("ROLE", "role-1"))); assertEquals(30, total(query("POSITION", "position-1"))); assertEquals(30, total(query("GROUP", "group-1"))); } @Test void searchEchoAndTenantUseSameScopeAsList() throws Exception { DmsWenjianDayinTaizhangQuery q = query("USER", "subtree"); q.setSearch("code-30"); assertEquals(1, total(q)); assertEquals(Arrays.asList("task-30"), ids(q)); q.setSearch(null); q.setTaskId("task-31"); assertEquals(0, total(q)); q.setTaskId("task-30"); assertEquals(Arrays.asList("task-30"), ids(q)); q.setTenantId("tenant-b"); assertEquals(0, total(q)); } private DmsWenjianDayinTaizhangQuery query(String kind, String principal) throws Exception { DmsWenjianDayinTaizhangQuery q = new DmsWenjianDayinTaizhangQuery(); q.setTenantId("tenant-a"); DmsUserFolderPermissionTreeQuery permissionQuery = new DmsUserFolderPermissionTreeQuery(); permissionQuery.setTenantId("tenant-a"); permissionQuery.setPrincipals(Collections.singletonList(new DmsUserFolderPermissionPrincipal("current-user", kind, principal))); List folderIds = new ArrayList<>(); for (Map row : rows("selectPrintFolders", permissionQuery)) folderIds.add(row.get("id").toString()); q.setFolderIdsJson(JsonUtil.getObjectToString(folderIds)); q.setUserId(principal); q.setApprovedFlowState(2); Map personal = new HashMap<>(); personal.put("tenantId", q.getTenantId()); personal.put("userId", principal); List versionIds = new ArrayList<>(); for (Map row : rows("selectWorkflowPrintVersions", personal)) versionIds.add(row.get("id").toString()); q.setWorkflowPrintVersionIdsJson(JsonUtil.getObjectToString(versionIds)); q.setLimit(100); return q; } private long total(DmsWenjianDayinTaizhangQuery q) throws Exception { return ((Number)rows("selectCount",q).get(0).get("total")).longValue(); } private List ids(DmsWenjianDayinTaizhangQuery q) throws Exception { List ids = new ArrayList<>(); for (Map row : rows("selectPage",q)) ids.add((String)row.get("f_id")); return ids; } private List> rows(String method, Object q) throws Exception { boolean personal = "selectWorkflowPrintVersions".equals(method); MappedStatement statement = configuration.getMappedStatement((personal || "selectPrintFolders".equals(method) ? "jnpf.dmsMapper.DmsUserFolderPermissionTreeMapper." : "jnpf.dmsMapper.DmsWenjianDayinTaizhangMapper.")+method); @SuppressWarnings("unchecked") Map params = personal ? (Map) q : Collections.singletonMap("q", q); BoundSql bound = statement.getBoundSql(params); String testData = fixture + "fixture_exclusions AS (" + exclusions + "), "; String sql = (personal ? "WITH RECURSIVE " + testData + " permission_result AS (" + bound.getSql() + ") SELECT * FROM permission_result" : bound.getSql().replaceFirst("WITH (RECURSIVE )?", "WITH RECURSIVE " + testData)) .replace("public.dms_access_exclusions", "fixture_exclusions") .replace("public.dms_file_permissions", "fixture_permissions") .replace("public.dms_folders", "fixture_folders") .replace("public.dms_wenjian_dayin_renwu", "fixture_tasks") .replace("public.dms_wenjian_ziyuan", "fixture_resources") .replace("public.dms_wenjian_dayin_shenqing_zixiang", "fixture_details") .replace("public.dms_wenjian_dayin_shenqing", "fixture_applications") .replace("public.dms_wenjian_banben", "fixture_versions") .replace("public.base_organize", "fixture_org") .replace("public.dms_wenjian_dayin_jilu", "fixture_prints"); try (Connection connection = DriverManager.getConnection(System.getenv("DMS_LEDGER_TEST_JDBC_URL"), System.getenv("DMS_LEDGER_TEST_DB_USER"), System.getenv("DMS_LEDGER_TEST_DB_PASSWORD"))) { connection.setReadOnly(true); connection.setAutoCommit(false); try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setQueryTimeout(20); new DefaultParameterHandler(statement, params, bound).setParameters(ps); try (ResultSet rs = ps.executeQuery()) { List> result = new ArrayList<>(); while (rs.next()) { Map row = new LinkedHashMap<>(); for (int i=1;i<=rs.getMetaData().getColumnCount();i++) row.put(rs.getMetaData().getColumnLabel(i),rs.getObject(i)); result.add(row); } return result; } } finally { connection.rollback(); } } } }