* fix(api): tell callers why a request was forbidden
The scope filter already computes an exact reason ("Missing API token
scope: skill:delete", "API token cannot access endpoint: /x") and the
access-denied handler discarded it, returning a bare "Forbidden" for
every case: missing scope, endpoint closed to API tokens, and paths
that simply don't exist. Clients cannot tell those apart, so they
guess — the published CLI reports every 403 as "token may lack
required scope", which sent us debugging token scopes for an hour when
the real causes were a revoked token and a mistyped namespace path.
The reason now rides in the response via a new error.forbidden.detail
message (en + zh), and is logged alongside the exception type.
Signed-off-by: Gal Eyal <gal.e@popai.health>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(api): safely expose API token denial reasons
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
The shared RedisTemplate uses GenericJackson2JsonRedisSerializer with
the application ObjectMapper, which embeds no type information, so
stored DeviceCodeData deserializes as a LinkedHashMap. The typed casts
in pollToken and authorizeDeviceCode then throw ClassCastException on
every call, making the whole device authorization flow unusable
(every poll returns 500).
Convert the raw value with ObjectMapper.convertValue instead of
casting; this reads both the current untyped map format and any typed
format, so no stored-data migration is needed. Adds bean setters to
DeviceCodeData for map conversion and regression tests that feed the
service exactly what Redis returns in production (untyped maps).
Fixes#604
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Gal Eyal <gal.e@popai.health>
* 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
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