mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-11 22:51:04 +00:00
fix(suite): validate portable version tokens
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
d9696be9e4
commit
83ff64d76a
6 changed files with 65 additions and 14 deletions
|
|
@ -225,6 +225,7 @@ error.suite.publish.notPrivate=Only private Skill Suites can be published withou
|
|||
error.suite.publish.notDraft=Skill Suite version {0} is not a draft
|
||||
error.suite.displayName.required=Skill Suite display name is required
|
||||
error.suite.version.required=Skill Suite version is required
|
||||
error.suite.version.invalid=Skill Suite version must use 1-64 portable characters: letters, numbers, dot, underscore, plus, or hyphen
|
||||
error.suite.slug.exists=A Skill Suite with slug {0} already exists in this namespace
|
||||
error.suite.access.denied=You do not have permission to view this Skill Suite
|
||||
error.suite.install.unavailable=The Skill Suite cannot be installed because one or more members are unavailable
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ error.suite.publish.notPrivate=只有私有技能套件可以免审核发布
|
|||
error.suite.publish.notDraft=技能套件版本 {0} 不是草稿
|
||||
error.suite.displayName.required=技能套件显示名称不能为空
|
||||
error.suite.version.required=技能套件版本不能为空
|
||||
error.suite.version.invalid=技能套件版本须为 1-64 位,且只能包含字母、数字、点、下划线、加号或连字符
|
||||
error.suite.slug.exists=当前命名空间已存在 slug 为 {0} 的技能套件
|
||||
error.suite.access.denied=无权查看该技能套件
|
||||
error.suite.install.unavailable=技能套件中存在当前不可用的成员,无法安装
|
||||
|
|
|
|||
|
|
@ -17,12 +17,15 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Creates Suite drafts after member coordinates have been resolved to exact Skill versions. */
|
||||
@Service
|
||||
public class SkillSuiteDraftService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SkillSuiteDraftService.class);
|
||||
private static final Pattern PORTABLE_VERSION_PATTERN =
|
||||
Pattern.compile("[A-Za-z0-9][A-Za-z0-9._+-]{0,63}");
|
||||
|
||||
private final SkillSuiteRepository suiteRepository;
|
||||
private final SkillSuiteVersionRepository versionRepository;
|
||||
|
|
@ -202,6 +205,9 @@ public class SkillSuiteDraftService {
|
|||
if (command.version() == null || command.version().isBlank()) {
|
||||
throw new DomainBadRequestException("error.suite.version.required");
|
||||
}
|
||||
if (!PORTABLE_VERSION_PATTERN.matcher(command.version()).matches()) {
|
||||
throw new DomainBadRequestException("error.suite.version.invalid");
|
||||
}
|
||||
SkillSuiteCompositionPolicy.validate(command.members(), command.entrySkillVersionId());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.domain.audit.AuditLogService;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVisibility;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -17,6 +18,7 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
|
@ -126,6 +128,28 @@ class SkillSuiteDraftServiceTest {
|
|||
verify(publicationValidator).validate(suite, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSuiteVersionsThatCannotBeUsedSafelyAcrossCliShells() {
|
||||
Namespace namespace = new Namespace("team", "Team", "owner");
|
||||
setId(namespace, 1L);
|
||||
when(namespaceRepository.findById(1L)).thenReturn(Optional.of(namespace));
|
||||
SkillSuiteActionContext context = new SkillSuiteActionContext(
|
||||
"author", Map.of(1L, NamespaceRole.MEMBER), Set.of(),
|
||||
"request-3", "127.0.0.1", "test");
|
||||
|
||||
for (String version : List.of("1.0.0; touch pwned", "a".repeat(65))) {
|
||||
CreateSkillSuiteDraftCommand command = new CreateSkillSuiteDraftCommand(
|
||||
1L, "writers", "Writers", "Summary", null, version,
|
||||
SkillVisibility.PUBLIC, null, 40L,
|
||||
List.of(new SkillSuiteMemberSelection(
|
||||
30L, 40L, "global", "writer", "1.0.0", "sha256:abc")));
|
||||
|
||||
assertThatThrownBy(() -> service.create(command, context))
|
||||
.isInstanceOf(DomainBadRequestException.class)
|
||||
.hasMessage("error.suite.version.invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private void setId(Object target, Long id) {
|
||||
try {
|
||||
var field = target.getClass().getDeclaredField("id");
|
||||
|
|
|
|||
|
|
@ -189,4 +189,17 @@ describe('SuiteDetailPage', () => {
|
|||
)).not.toBeNull()
|
||||
expect(within(sidebar).getByLabelText('suite.copyInstallCommand')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not expose a copyable shell command for an unsafe legacy version', () => {
|
||||
const unsafeSuite = suite()
|
||||
unsafeSuite.version = '1.0.0; touch pwned'
|
||||
mocks.detail = { data: unsafeSuite, isLoading: false, error: null }
|
||||
|
||||
render(<SuiteDetailPage />)
|
||||
|
||||
const sidebar = screen.getByRole('complementary', { name: 'suite.detailsSidebar' })
|
||||
expect(within(sidebar).getByRole('alert').textContent)
|
||||
.toBe('skillDetail.installCommandUnsafeVersion')
|
||||
expect(within(sidebar).queryByLabelText('suite.copyInstallCommand')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useSuiteDetail, useSuiteVersions, useSubmitSuite } from '@/shared/hooks
|
|||
import { suiteBlockingReasonLabel, suiteStatusLabel, suiteVisibilityLabel } from '@/features/suite/suite-labels'
|
||||
import { SuiteManagementActions } from '@/features/suite/suite-management-actions'
|
||||
import { MarkdownRenderer } from '@/features/skill/markdown-renderer'
|
||||
import { getBaseUrl } from '@/features/skill/install-command'
|
||||
import { getBaseUrl, isPortableSkillVersion } from '@/features/skill/install-command'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { Button, buttonVariants } from '@/shared/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs'
|
||||
|
|
@ -26,7 +26,7 @@ export function SuiteDetailPage() {
|
|||
const submitMutation = useSubmitSuite()
|
||||
const registryUrl = useMemo(() => getBaseUrl(), [])
|
||||
const command = useMemo(
|
||||
() => suite
|
||||
() => suite && isPortableSkillVersion(suite.version)
|
||||
? `skillhub suite install @${suite.namespace}/${suite.slug} --version ${suite.version} --registry ${registryUrl}`
|
||||
: '',
|
||||
[registryUrl, suite],
|
||||
|
|
@ -272,18 +272,24 @@ export function SuiteDetailPage() {
|
|||
<Terminal className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
<span className="text-sm font-semibold font-heading text-foreground">{t('suite.installCommand')}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2 rounded-lg bg-secondary p-3">
|
||||
<code className="min-w-0 flex-1 overflow-x-auto text-sm">{command}</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('suite.copyInstallCommand')}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(command)
|
||||
toast.success(t('suite.commandCopied'))
|
||||
}}
|
||||
><Copy className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
{command ? (
|
||||
<div className="flex min-w-0 items-center gap-2 rounded-lg bg-secondary p-3">
|
||||
<code className="min-w-0 flex-1 overflow-x-auto text-sm">{command}</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('suite.copyInstallCommand')}
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(command)
|
||||
toast.success(t('suite.commandCopied'))
|
||||
}}
|
||||
><Copy className="h-4 w-4" /></Button>
|
||||
</div>
|
||||
) : (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
{t('skillDetail.installCommandUnsafeVersion')}
|
||||
</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{versions?.length ? (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue