From 080be28eefcf2c1ee5a942d83588f2dbbc456d07 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:46:05 +0800 Subject: [PATCH] fix(search): align portal visibility for private skills (#323) * feat(ci): add AI-powered release notes generation - Add GitHub Models integration for automated release notes - Support bilingual (EN) release notes with highlights extraction - Fallback to conventional commit grouping when LLM unavailable - Trigger on tag push or manual workflow dispatch - Zero configuration: uses GitHub Models (gpt-4o-mini) by default * chore: pin action versions and update gitignore - Pin checkout and setup-deno to commit hashes matching project convention - Add .playwright-mcp/ and .mcp.json to gitignore * fix(search): keep private skills out of portal discovery - remove super admin expanded visibility from portal search - exclude all private skills from portal search results - keep namespace-only skills visible only to namespace members - reserve private skill access for my-skills and future admin surfaces * test(search): update test to reflect portal visibility policy - rename test from platformWideAccessShouldBypass... to platformWideAccessShouldNotBypass... - verify that platformWideAccess flag does not grant extra visibility in portal search - verify PRIVATE skills are excluded from search SQL - aligns test with new requirement: portal search uses standard visibility rules * test(search): update app service test for portal visibility policy - rename test from search_shouldGrantPlatformWideAccessToSuperAdmin to search_shouldNotGrantPlatformWideAccessToSuperAdminInPortal - verify super admin does not get platformWideAccess in portal search - aligns with requirement: portal uses standard visibility, admin features reserved for dedicated interface --- .../skillhub/service/SkillSearchAppService.java | 6 ++---- .../skillhub/service/SkillSearchAppServiceTest.java | 4 ++-- .../postgres/PostgresFullTextQueryService.java | 9 --------- .../postgres/PostgresFullTextQueryServiceTest.java | 12 ++++++------ 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java index 57a1145a..820980bb 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSearchAppService.java @@ -122,10 +122,8 @@ public class SkillSearchAppService { } private boolean hasPlatformWideReadAccess(Set platformRoles) { - if (platformRoles == null || platformRoles.isEmpty()) { - return false; - } - return platformRoles.contains("SUPER_ADMIN"); + // Super admins should use a dedicated admin interface, not the public portal + return false; } private SearchResponse searchVisibleSkills( diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSearchAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSearchAppServiceTest.java index 57710c88..fdac46f8 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSearchAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSearchAppServiceTest.java @@ -216,7 +216,7 @@ class SkillSearchAppServiceTest { } @Test - void search_shouldGrantPlatformWideAccessToSuperAdmin() { + void search_shouldNotGrantPlatformWideAccessToSuperAdminInPortal() { when(searchQueryService.search(any())) .thenReturn(new SearchResult(List.of(), 0, 0, 20)); when(rbacService.getUserRoleCodes("admin-1")).thenReturn(Set.of("SUPER_ADMIN", "USER")); @@ -228,7 +228,7 @@ class SkillSearchAppServiceTest { SearchVisibilityScope scope = captor.getValue().visibilityScope(); assertEquals("admin-1", scope.userId()); - assertEquals(true, scope.platformWideAccess()); + assertEquals(false, scope.platformWideAccess()); } private void setField(Object target, String fieldName, Object value) { diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java index e6888827..ec0cfa37 100644 --- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java +++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryService.java @@ -105,7 +105,6 @@ public class PostgresFullTextQueryService implements SearchQueryService { Set adminNamespaceIds = query.visibilityScope().adminNamespaceIds().isEmpty() ? Set.of(-1L) : query.visibilityScope().adminNamespaceIds(); - boolean platformWideAccess = query.visibilityScope().platformWideAccess(); StringBuilder sql = new StringBuilder(); sql.append("SELECT d.skill_id "); @@ -118,9 +117,6 @@ public class PostgresFullTextQueryService implements SearchQueryService { sql.append("AND (d.visibility = 'PUBLIC' "); if (query.visibilityScope().userId() != null) { sql.append("OR (d.visibility = 'NAMESPACE_ONLY' AND d.namespace_id IN :memberNamespaceIds) "); - sql.append("OR (d.visibility = 'NAMESPACE_ONLY' AND :platformWideAccess = TRUE) "); - sql.append("OR (d.visibility = 'PRIVATE' AND (d.namespace_id IN :adminNamespaceIds OR d.owner_id = :userId)) "); - sql.append("OR (d.visibility = 'PRIVATE' AND :platformWideAccess = TRUE) "); } sql.append(") "); @@ -131,7 +127,6 @@ public class PostgresFullTextQueryService implements SearchQueryService { sql.append("AND (n.status <> 'ARCHIVED' "); if (query.visibilityScope().userId() != null) { sql.append("OR d.namespace_id IN :memberNamespaceIds "); - sql.append("OR :platformWideAccess = TRUE "); } sql.append(") "); @@ -196,8 +191,6 @@ public class PostgresFullTextQueryService implements SearchQueryService { if (query.visibilityScope().userId() != null) { nativeQuery.setParameter("memberNamespaceIds", memberNamespaceIds); nativeQuery.setParameter("adminNamespaceIds", adminNamespaceIds); - nativeQuery.setParameter("platformWideAccess", platformWideAccess); - nativeQuery.setParameter("userId", query.visibilityScope().userId()); } if (query.namespaceId() != null) { @@ -243,8 +236,6 @@ public class PostgresFullTextQueryService implements SearchQueryService { if (query.visibilityScope().userId() != null) { countQuery.setParameter("memberNamespaceIds", memberNamespaceIds); countQuery.setParameter("adminNamespaceIds", adminNamespaceIds); - countQuery.setParameter("platformWideAccess", platformWideAccess); - countQuery.setParameter("userId", query.visibilityScope().userId()); } if (query.namespaceId() != null) { diff --git a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryServiceTest.java b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryServiceTest.java index ea33092b..0025f2ec 100644 --- a/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryServiceTest.java +++ b/server/skillhub-search/src/test/java/com/iflytek/skillhub/search/postgres/PostgresFullTextQueryServiceTest.java @@ -393,7 +393,7 @@ class PostgresFullTextQueryServiceTest { } @Test - void platformWideAccessShouldBypassNamespaceVisibilityRestrictions() { + void platformWideAccessShouldNotBypassVisibilityInPortalSearch() { EntityManager entityManager = mock(EntityManager.class); Query nativeQuery = mock(Query.class); Query countQuery = mock(Query.class); @@ -418,12 +418,12 @@ class PostgresFullTextQueryServiceTest { ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); verify(entityManager, org.mockito.Mockito.times(2)).createNativeQuery(sqlCaptor.capture()); + // Portal search should not include platformWideAccess bypass logic assertThat(sqlCaptor.getAllValues().getFirst()) - .contains("OR (d.visibility = 'NAMESPACE_ONLY' AND :platformWideAccess = TRUE)") - .contains("OR (d.visibility = 'PRIVATE' AND :platformWideAccess = TRUE)") - .contains("OR :platformWideAccess = TRUE"); - verify(nativeQuery).setParameter("platformWideAccess", true); - verify(countQuery).setParameter("platformWideAccess", true); + .doesNotContain("platformWideAccess") + .doesNotContain("PRIVATE"); + verify(nativeQuery, never()).setParameter("platformWideAccess", true); + verify(countQuery, never()).setParameter("platformWideAccess", true); } @Test