The callback_unexpectedError test still referenced the old
?error=internal_error redirect; align it with the ?reason=internalError
convention introduced in the previous commit.
- Remove ticket value from debug log to prevent credential leakage
- Change CAS error redirects from ?error=snake_case to ?reason=camelCase
to align with frontend login page's existing search.reason handling
- Add CAS error messages to login page with i18n support (en/zh)
- Strengthen XXE/billion-laughs test assertions to verify SAXParseException
cause with DOCTYPE rejection message, preventing false-pass scenarios
B1 — Ticket log leak: log.debug now records only the validation path
(resolvedProtocolVersion().validatePath()); the catch block surfaces only
e.getClass().getSimpleName() instead of e.getMessage(), preventing the
full validation URL (with ticket query param) from reaching log streams.
B2 — emailVerified semantics: CasIdentityClaims.emailVerified() now
constantly returns false. CAS passes through email attributes from the
upstream directory (LDAP/AD) without cryptographic verification, so
returning true was a false signal to any AccessPolicy that gates on it.
B3 — Exception package: AccountPendingException and AccountDisabledException
moved from auth.oauth to auth.identity; all import sites updated. OAuth,
CAS, and future SAML/OIDC flows now import from the neutral package.
S6 — Login CSRF via state nonce: login() generates a cryptographically
random 24-byte nonce, stores it in the session under a CAS-specific key
(skillhub.cas.state), and appends state=<nonce> to the service URL that
is sent to the CAS server. callback() validates the incoming state param
against the session value before touching the ticket; a mismatch short-
circuits to redirect:/login?error=invalid_state. The CAS-specific session
key (skillhub.cas.state / skillhub.cas.returnTo) also eliminates the
previous shared-key concurrency hazard with the OAuth flow.
Blockers:
- Harden XML parsing against XXE (disallow DOCTYPE, external entities/DTDs,
enable FEATURE_SECURE_PROCESSING) and switch to UTF-8 byte decoding.
- Generalize AccessPolicy.evaluate from OAuthClaims to IdentityClaims; extract
IdentityAuthenticator so OAuth and CAS share allow/deny/pending evaluation.
CAS callback now goes through the policy instead of bypassing it with a
direct bindOrCreate call.
- Configure JDK HttpClient with connect/read timeouts (5s/10s) and disable
HTTP redirects to prevent ticket exfiltration via a malicious CAS server.
Major:
- Require HTTPS for skillhub.auth.cas.service-url in addition to server-url.
- Stop logging raw service tickets; log claims.subject() instead.
- Remove the dead authCasEnabled web flag — the backend AuthMethodCatalog is
the single source of truth for CAS visibility, matching how OAuth works.
- Wire SKILLHUB_AUTH_CAS_* env vars into compose.release.yml and add a fully
documented section in .env.release.example.
Minor:
- CasProtocolVersion enum replaces string comparisons in the validator.
- JSON multi-value array attributes are preserved as List<String> instead of
silently dropping all but the first element.
- AuthMethod.methodType union adds 'CAS_REDIRECT'.
- application.yml notes that service-url must equal
${SKILLHUB_PUBLIC_BASE_URL}/api/v1/auth/cas/callback.
Tests:
- CasTicketValidatorTest tightens URL matching to assert ticket/service/format
parameters and adds XXE + billion-laughs regression cases.
- IdentityAuthenticatorTest covers ALLOW / PENDING / DENY paths.
- AuthMethodCatalogTest exercises both cas.enabled=true and =false.
- isExternalRedirectMethod predicate extracted and unit-tested.
Implement native CAS protocol ticket validation for enterprise SSO
integration, supporting both CAS 2.0 (XML) and CAS 3.0 (JSON) modes.
Backend:
- Introduce IdentityClaims interface to abstract identity providers;
OAuthClaims now implements it, enabling CAS reuse of IdentityBindingService
- CasProperties with @PostConstruct HTTPS validation and feature flag
- CasTicketValidator: validates tickets via /serviceValidate (2.0) or
/p3/serviceValidate (3.0), parses XML/JSON responses
- CasLoginController: /api/v1/auth/cas/login (redirect) and /callback
(ticket validation + session establishment)
- RouteSecurityPolicyRegistry: permit /api/v1/auth/cas/**
- AuthMethodCatalog: expose CAS as CAS_REDIRECT method type
Frontend:
- LoginButton renders CAS_REDIRECT methods alongside OAuth providers
- Runtime config adds authCasEnabled flag
- CAS logo SVG added
Closes#456
* fix(auth): use SimpleUrlAuthenticationSuccessHandler for OAuth2 login
Replace SavedRequestAwareAuthenticationSuccessHandler with
SimpleUrlAuthenticationSuccessHandler to prevent redirecting to
saved API requests after OAuth2 login.
Previously, when a user accessed a protected API endpoint (e.g.,
/api/web/skills) without authentication, Spring Security would save
that request. After OAuth2 login, the handler would redirect back to
the API endpoint instead of the dashboard.
Now the handler only uses:
- returnTo parameter from session (if present)
- default target URL (/dashboard) as fallback
* test(auth): add regression for OAuth2 success redirect; restore clearAuthenticationAttributes
Cover the no-returnTo + cached-API-request branch with HttpSessionRequestCache so
the original bug (post-login redirect resolving to /api/web/skills) cannot be
silently reintroduced. Also restore clearAuthenticationAttributes() in the
returnTo branch so it stays symmetric with the default branch (super clears it).
---------
Co-authored-by: xiose <huyanlin@nuaa.edu.cn>
ApiTokenAuthenticationFilter authenticates /api/cli/** Bearer tokens
but ApiTokenScopeFilter.shouldNotFilter() previously skipped them.
The result: API token requests on CLI routes were authenticated and
authorization-policy-checked, but scope enforcement never ran. Tokens
without skill:publish or skill:delete could call /publish, /publish/validate,
and DELETE despite the policy table requiring those scopes.
Add /api/cli/ to the scope filter's covered prefixes and a filter-level
test that confirms a token missing skill:publish is rejected on the new
validate endpoint. Update the existing CLI controller tests to grant
the appropriate SCOPE_* authorities to their api_token principals so
they continue to pass under enforced scopes.
Fix three blockers and one contract drift issue surfaced in code review:
1. API token policy: add skill:publish scope policy and authentication
policy for /api/cli/v1/skills/*/publish/validate. Without these the
AntPathMatcher pattern /publish would not cover /publish/validate,
so Bearer-token requests would be rejected by the scope filter.
2. Warnings semantics: dry-run now treats warnings as making valid=false.
The CLI publish flow uses confirmWarnings=false, so the real publish
rejects any warnings; dry-run must mirror that to avoid false positives.
3. Visibility parameter: validate endpoint now accepts the same
visibility multipart field as publish. The CLI forwards --visibility
so invalid values are caught at dry-run time rather than at publish.
4. Schema drift: resolvedSlug and resolvedVersion are nullable in
practice (returned as null when validation fails before resolution).
Updated schema.d.ts to reflect string | null instead of optional string.
Tests added:
- RouteSecurityPolicyRegistryTest: validate endpoint scope check
- CliDryRunValidateTest: custom + invalid visibility cases
- publish-dry-run.test.ts: --visibility forwarded to server
- Exit non-zero (code 6) when --dry-run validation fails, enabling
CI/CD pipeline integration
- Add archived skill check: dry-run now detects when the publisher's
own skill is archived
- Add version-exists check: dry-run now detects when the resolved
version is already published
- Use StandardCharsets.UTF_8 for SKILL.md content parsing
Add a validate-only endpoint (POST /api/cli/v1/skills/{namespace}/publish/validate)
that runs the full pre-publish validation chain without persisting anything.
This allows developers to check their package locally before actual publishing.
The validation covers:
- SKILL.md existence and frontmatter parsing (name, description required)
- File extension whitelist and size limits
- Credential leak scanning with line-number precision
- Slug generation and name conflict detection
CLI usage: `skillhub publish <path> --dry-run`
Closes#429
When approving a promotion, the new SkillVersion was created without copying
bundleReady and downloadReady from the source version, causing the download
button to be permanently disabled for promoted skills.
Gemini review feedback: the previous != PUBLISHED condition was too broad
and could inadvertently overwrite terminal states like REJECTED or YANKED.
Now explicitly check == SCANNING before transitioning status.
Super admin auto-publish flow was skipping security scanning entirely.
Now triggerScan is called regardless of autoPublish flag, while preserving
the PUBLISHED status (scan runs as post-publish audit rather than blocking).
Closes#415
Resolved conflicts by keeping both sides:
- web/src/api/client.ts: preserve paginated listMembers(slug, {page,size})
and add delete(slug) from main.
- web/src/shared/hooks/use-namespace-queries.ts: keep useUpdateNamespace
and useTransferNamespaceOwnership from this branch, plus useDeleteNamespace
from main.
Implement POST /namespaces/{slug}/transfer-ownership to allow namespace owners to transfer ownership to existing members. Includes comprehensive test coverage for success and failure scenarios (non-owner, target not found, frozen namespace).
Introduce a dedicated `/space/$namespace/$slug/compare` page that compares
two published skill versions GitHub-style: left file list + right unified
diff. Backend exposes `GET /versions/compare` returning structured diff
(computed via java-diff-utils) with per-file hunks, binary placeholder,
and truncation flags. Frontend uses two version selectors scoped to
PUBLISHED versions, a file search box, active-file highlighting, and
whitespace-preserving unified view. E2E covers the publish + rerelease
+ approve round trip; controller/domain tests cover happy path and
same-version rejection.
Adds skillhub.storage.s3.disable-chunked-encoding (env:
SKILLHUB_STORAGE_S3_DISABLE_CHUNKED_ENCODING, default false) so
operators can turn off aws-chunked encoding when the S3 backend is
Aliyun OSS, which rejects it with 'InvalidArgument: aws-chunked
encoding is not supported'.
Closes#365
* feat(publish): increase max file count from 100 to 500
Configurable via SKILLHUB_PUBLISH_MAX_FILE_COUNT env var.
* feat(publish): support SKILL.md in subdirectory with warning for ignored files
When SKILL.md is found in a single subdirectory (e.g. my-skill/SKILL.md),
promote that directory's contents to root and discard files outside it.
Discarded files are reported as warnings through the existing confirm flow.
* feat(publish): pass extraction warnings through confirm flow
When files are ignored during SKILL.md subdirectory promotion,
warnings are surfaced to the user via the existing precheck confirm dialog.
* fix(security): add invalidSessionStrategy to return 401 on expired session
Handles the case where Spring Security detects an invalid session cookie,
returning a clean 401 JSON response instead of triggering cascading exceptions.
Closes#360 (part 1/2)
* fix(security): handle session invalidation IllegalStateException as 401
Catches IllegalStateException with "Session was invalidated" message and
returns 401 instead of letting it fall through to the generic 500 handler.
Non-session IllegalStateExceptions are re-thrown to the catch-all handler.
Closes#360 (part 2/2)
* feat(publish): filter macOS metadata and add integration tests
Skip __MACOSX/, .DS_Store, and ._ resource fork entries during zip
extraction. Add integration tests for nested SKILL.md warning flow,
session invalidation 401 response, and macOS metadata filtering.
* test(publish): add real-world macOS zip and edge case integration tests
Covers: macOS zip with nested SKILL.md + __MACOSX + .DS_Store + stray files,
simple macOS single-folder case, and missing SKILL.md fallback behavior.
Enable API token authentication for CLI endpoints by adding /api/cli/
to the filter's path whitelist. Previously, CLI endpoints were not
processed by the token authentication filter, causing all Bearer token
requests to fail with 401.
When access-key / secret-key are left blank, fall back to the AWS
DefaultCredentialsProvider chain so that deployments on EC2, ECS,
and EKS can authenticate via instance profile, task role, or IRSA
without static credentials.
- Extract buildCredentialsProvider() in S3StorageService
- Add sts dependency for Web Identity Token (EKS) support
- Add unit tests for credential provider selection
- Update storage-spi docs (zh + en) and env example
Include subscriptionCount in SkillDetailDTO and SkillDetailResponse
so the frontend SubscribeButton receives the updated count after
subscribe/unsubscribe mutations.
Revert emailVerified check in EmailDomainAccessPolicy to preserve
backward compatibility with GitHub/GitLab OAuth users. Instead, null
unverified emails in CustomOidcUserService.toOAuthClaims() so
EmailDomainAccessPolicy naturally denies them via null email.
Add SLF4J logging to CustomOidcUserService for OIDC authentication
flow tracing and failure diagnostics.
Add registration ID collision warning to deployment docs.
- Add null/blank validation for OIDC sub claim in CustomOidcUserService
- Throw OAuth2AuthenticationException when sub is missing or blank
- Complete .env.release.example with all required OIDC environment variables
- Add test cases for sub validation and providerLogin fallback scenarios
- All 5 tests passing