Self-hosted deployments want every new account to have somewhere of its
own to publish, without asking an administrator for a namespace first and
without pushing drafts into `global`.
Add an operator-controlled policy, off by default so upgrading changes no
behaviour. When enabled, an account that becomes usable gets a namespace
it owns. "Private" here means a team namespace whose only member is that
account: namespaces have no visibility flag, and skill visibility stays a
property of each skill.
Trigger points. UserActivatedEvent is published wherever an account first
becomes usable:
- LocalAuthService.register
- IdentityBindingService.bindOrCreate, for ACTIVE first logins
- AdminUserAppService.updateUserStatus, on a transition into ACTIVE
The third matters for deployments that gate access behind approval: those
accounts are created PENDING at the first OAuth attempt and only become
usable when an administrator approves them.
Why an AFTER_COMMIT listener rather than a call alongside
GlobalNamespaceMembershipService.ensureMember. Both namespace.created_by
and namespace_member.user_id reference user_account(id), which rules out
each obvious alternative:
- Joining the registration transaction lets a slug clash roll the
registration back, so a namespace failure costs the user their account
— or, on OAuth, their login.
- Suspending it with REQUIRES_NEW leaves the new transaction unable to see
the uncommitted user_account row, so the foreign key check blocks on the
outer transaction's row lock and the two wait on each other.
Provisioning after commit avoids both. The listener is deliberately not
@Async, so the namespace exists by the time the user's next request
arrives, and it swallows failures.
Naming. Two templates over ${username}, ${email_prefix} and ${user_id};
unknown placeholders are left in place so a typo is visible rather than
silently dropped. ${username} falls back to the email local part and then
to the user id. Slugs go through the existing slug rules, which is why the
console renders a live preview: underscores are not legal in a slug, so
`${username}_space` yields `alice-space`. A taken or reserved candidate
gets a numeric suffix, so `admin` becomes `admin-2`. Owning any non-global
namespace already skips provisioning, which keeps re-enabling an account
from handing out a second one.
The templates are not exposed in application.yml: they contain ${...},
which Spring would resolve as property references, and Boot 3.2 predates
placeholder escaping. Only the enable flag lives there; templates are set
in the console and default from PersonalNamespaceProvisioningProperties.
Updating the policy writes an audit entry with the before and after.
SkillHub has no mechanism for settings an operator can change without a
redeploy: the only per-deployment knobs live in application.yml, and the
only stored preferences are per-user notification preferences.
Add a generic store. One row holds one setting group serialized as JSON,
so a group can gain fields without a schema migration.
Reads take the caller's defaults:
<T> T get(String settingKey, Class<T> type, T defaults)
which gives two properties worth keeping:
- A group nobody has overridden has no row, and resolves to whatever the
deployment configured. Configuration-file-only deployments keep working
exactly as before, and an upgrade changes no behaviour.
- A stored document that can no longer be parsed also falls back to the
defaults, with a warning. One malformed row must not take down the flows
that read settings, such as login.
Groups are deserialized with unknown fields ignored so a rolling upgrade
can read documents written by a newer node.
No consumer yet; the following commit adds the first one.
Add ownerId and ownerDisplayName fields to SkillSummaryResponse,
and render author and last update time at the bottom of each skill card.
在技能卡片底部新增作者和最近更新时间显示,搜索结果接口新增
ownerId 和 ownerDisplayName 字段。
Log: 技能卡片新增作者和更新时间
Influence: 搜索结果页技能卡片底部显示作者和更新时间信息,接口新增 ownerId/ownerDisplayName 字段。
Signed-off-by: wurongjie <wurongjie@uniontech.com>
* fix(compat): prioritize PUBLIC/GLOBAL skills in legacy slug lookup
When multiple skills share the same slug across namespaces,
findByLegacySlug now prefers PUBLIC visibility and GLOBAL
namespace over NAMESPACE_ONLY/PRIVATE ones, so plain slug
lookups resolve to the most accessible skill. Namespaces are
batch-fetched via findByIdIn to avoid N+1 database queries.
Signed-off-by: wurongjie <wurongjie@uniontech.com>
* fix(compat): prefer published legacy slug candidates
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: wurongjie <wurongjie@uniontech.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* feat(api): return skill labels from the skill listing endpoints on request
Skill labels were reachable only one skill at a time, through
/api/{v1,web}/skills/{namespace}/{slug}/labels, so a client rendering a list had
to issue a follow-up request per row.
Add includeLabels=true to GET /api/v1/skills and GET /api/web/skills. The labels
array is populated only when the parameter is set and left out of the payload
otherwise, so existing responses are byte-identical.
Labels for the whole page are resolved by SkillLabelProjectionService in three
queries — assignments, definitions, translations — rather than three per skill.
Closes#710
Signed-off-by: FenjuFu <fufenjupku@gmail.com>
* fix(api): use include parameter for skill labels
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* test(api): reject unsupported include before search
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: FenjuFu <fufenjupku@gmail.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
RouteSecurityPolicyRegistry keeps two policy lists — AUTHORIZATION_POLICIES for
session/cookie access and API_TOKEN_POLICIES for Bearer tokens — with nothing
keeping them in step. Routes the authorization list opens but the token list
never registers fall through to the catch-all and answer
API token cannot access endpoint: <path>.
Register the routes reported in #713 (/api/v1/labels, the star and rating
writes) plus the same-class gaps for /api/v1/auth/methods and paths below
/api/v1/download, and add a guard test that walks the authorization list and
fails when a route is neither token-reachable nor declared session-only.
DELETE /api/v1/skills/{id}/star also matched the SUPER_ADMIN rule for
DELETE /api/v1/skills/*/*, so un-starring was refused for ordinary accounts on
the session path too. Star and rating writes now have their own authorization
entries ahead of that rule.
Closes#713
Signed-off-by: FenjuFu <fufenjupku@gmail.com>
* fix(security): publish scan task after transaction commit
SecurityScanService.triggerScan is @Transactional but published the Redis
Stream scan task inline, before the transaction committed. The stream
consumer could receive the task before the skill_version / security_audit
rows were visible, fail with "SkillVersion not found" / "SecurityAudit not
found", exhaust its immediate retries while the publishing transaction was
still open, and leave the committed version stuck in SCANNING.
Defer the publish to an afterCommit transaction synchronization so the
consumer only ever sees the task once the rows are committed and visible; on
rollback the task is never published. Falls back to an inline publish when
called outside a transaction.
Closes#612
Signed-off-by: FenjuFu <fufenjupku@gmail.com>
* test(security): cover scan task after-commit publishing
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
* refactor(security): hide scan publish transaction callback
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
---------
Signed-off-by: FenjuFu <fufenjupku@gmail.com>
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Co-authored-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
Downloading a skill whose slug is non-ASCII (e.g. a Chinese name) through
the ClawHub CLI compatibility route failed: the 302 Location header was
built by string-concatenating the raw slug, and Tomcat encodes header
values as ISO-8859-1, so a character outside 0-255 makes it drop the
Location header entirely and the download breaks. The skillhub CLI path
was unaffected because it doesn't go through this redirect.
Build the Location with UriComponentsBuilder.pathSegment(...).encode(), so
each segment is percent-encoded while the '/' separators stay literal.
"需求" becomes %E9%9C%80%E6%B1%82 and the header is ISO-8859-1-writable.
Fixes#658
Signed-off-by: FenjuFu <92919259+FenjuFu@users.noreply.github.com>
Remove terminal review tasks before deleting an allowed skill version. Lock all versions of the aggregate in stable order so concurrent deletes preserve the last-version invariant and return business errors instead of 500 responses.
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
OAuth2AuthorizationRequestRedirectFilter invokes the resolver on every
request in the chain and the delegate answers null for anything that is
not an authorization request. Recording the return target on those calls
cleared it again on the next request without a returnTo parameter — the
provider callback included, which this filter processes before login
succeeds. The success handler therefore always found an empty session
attribute and fell back to the default target, so returnTo never worked.
Guard the write on a non-null authorization request. As a side effect,
anonymous API requests no longer allocate a session via getSession().
Signed-off-by: Gal Eyal <gal.e@popai.health>