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
This commit is contained in:
XiaoSeS 2026-04-17 17:46:05 +08:00 committed by GitHub
parent c03790a11e
commit 080be28eef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 10 additions and 21 deletions

View file

@ -122,10 +122,8 @@ public class SkillSearchAppService {
}
private boolean hasPlatformWideReadAccess(Set<String> 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(

View file

@ -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) {

View file

@ -105,7 +105,6 @@ public class PostgresFullTextQueryService implements SearchQueryService {
Set<Long> 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) {

View file

@ -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<String> 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