mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add personal skill storage, API, and SDK (#25363)
> Mux updated this PR on behalf of Mike. ## Stack Context This PR is the storage, permissions, API, and SDK layer for experimental personal skills. #25362 has landed on `main`, so this branch is restacked directly on `main`. Stack order: 1. #25363 storage, permissions, API, and SDK 2. #25365 API test coverage 3. #25366 chattool and chatd integration 4. #25066 settings UI and docs 5. #25386 personal skills slash menu ## What? Adds the `user_skills` database table, generated queries, RBAC resources and scopes, audit resource handling, experimental user-scoped CRUD endpoints, SDK types, and generated API/site types. Follow-up review and restack fixes: - Enforce a bounded personal skill description in parser and database constraints. - Return `403 Forbidden` for unauthorized create and update attempts. - Return explicit conflict responses when soft-deleted users are targeted. - Keep user admins out of personal skills, while site owners can read and delete but not create or update. - Document trigger-raised constraint names and keep schema constants covered by tests. - Reuse `UserSkillMetadata` in the full `UserSkill` SDK response type. - Generate user skill IDs in Go instead of relying on a database default. - Rebase on latest `main` and renumber the user skills migration to `000502_user_skills`. ## Why? Personal skills need durable user-owned storage with owner authorization, limited site-owner moderation, and a hidden API surface before chatd can consume them. ## Validation - `make gen` - `go test ./coderd/database -run '^TestUserSkillSchemaConstants$' -count=1` - `go test ./coderd/database/dbauthz -run '^TestMethodTestSuite/TestUserSkills$' -count=1` - `go test ./coderd -run '^TestPatchUserSkill$' -count=1` - `go test ./codersdk ./coderd/database/db2sdk` - `make lint` - pre-commit hook on `97fd58108d`
This commit is contained in:
@@ -385,6 +385,16 @@ var (
|
||||
Type: "user_secret",
|
||||
}
|
||||
|
||||
// ResourceUserSkill
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a user skill
|
||||
// - "ActionDelete" :: delete a user skill
|
||||
// - "ActionRead" :: read user skill metadata and content
|
||||
// - "ActionUpdate" :: update user skill metadata and content
|
||||
ResourceUserSkill = Object{
|
||||
Type: "user_skill",
|
||||
}
|
||||
|
||||
// ResourceWebpushSubscription
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create webpush subscriptions
|
||||
@@ -500,6 +510,7 @@ func AllResources() []Objecter {
|
||||
ResourceUsageEvent,
|
||||
ResourceUser,
|
||||
ResourceUserSecret,
|
||||
ResourceUserSkill,
|
||||
ResourceWebpushSubscription,
|
||||
ResourceWorkspace,
|
||||
ResourceWorkspaceAgentDevcontainers,
|
||||
|
||||
@@ -379,6 +379,14 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionDelete: "delete a user secret",
|
||||
},
|
||||
},
|
||||
"user_skill": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: "create a user skill",
|
||||
ActionRead: "read user skill metadata and content",
|
||||
ActionUpdate: "update user skill metadata and content",
|
||||
ActionDelete: "delete a user skill",
|
||||
},
|
||||
},
|
||||
"usage_event": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionCreate: "create a usage event",
|
||||
|
||||
@@ -301,12 +301,14 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
Site: append(
|
||||
// Workspace dormancy and workspace are omitted.
|
||||
// Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec.
|
||||
// Owners cannot access other users' secrets.
|
||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent, ResourceBoundaryUsage, ResourceAiSeat),
|
||||
// Owners can inspect and delete personal skills for operability and
|
||||
// abuse handling, but cannot create or edit user-authored instructions.
|
||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUserSkill, ResourceUsageEvent, ResourceBoundaryUsage, ResourceAiSeat),
|
||||
// This adds back in the Workspace permissions.
|
||||
Permissions(map[string][]policy.Action{
|
||||
ResourceWorkspace.Type: ownerWorkspaceActions,
|
||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
||||
ResourceUserSkill.Type: {policy.ActionRead, policy.ActionDelete},
|
||||
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
||||
|
||||
@@ -1110,6 +1110,34 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Skills are user-authored instructions, not secrets. Owners can inspect
|
||||
// and delete them, but only the user can create or update them.
|
||||
{
|
||||
Name: "UserSkillsReadDelete",
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionDelete},
|
||||
Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser},
|
||||
false: {
|
||||
orgAdmin,
|
||||
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
|
||||
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UserSkillsCreateUpdate",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {memberMe, agentsAccessUser},
|
||||
false: {
|
||||
owner, orgAdmin,
|
||||
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
|
||||
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UsageEvents",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
|
||||
@@ -53,6 +53,13 @@ var externalLowLevel = map[ScopeName]struct{}{
|
||||
"user_secret:delete": {},
|
||||
"user_secret:*": {},
|
||||
|
||||
// User skills
|
||||
"user_skill:read": {},
|
||||
"user_skill:create": {},
|
||||
"user_skill:update": {},
|
||||
"user_skill:delete": {},
|
||||
"user_skill:*": {},
|
||||
|
||||
// Tasks
|
||||
"task:create": {},
|
||||
"task:read": {},
|
||||
|
||||
@@ -134,6 +134,10 @@ const (
|
||||
ScopeUserSecretDelete ScopeName = "user_secret:delete"
|
||||
ScopeUserSecretRead ScopeName = "user_secret:read"
|
||||
ScopeUserSecretUpdate ScopeName = "user_secret:update"
|
||||
ScopeUserSkillCreate ScopeName = "user_skill:create"
|
||||
ScopeUserSkillDelete ScopeName = "user_skill:delete"
|
||||
ScopeUserSkillRead ScopeName = "user_skill:read"
|
||||
ScopeUserSkillUpdate ScopeName = "user_skill:update"
|
||||
ScopeWebpushSubscriptionCreate ScopeName = "webpush_subscription:create"
|
||||
ScopeWebpushSubscriptionDelete ScopeName = "webpush_subscription:delete"
|
||||
ScopeWebpushSubscriptionRead ScopeName = "webpush_subscription:read"
|
||||
@@ -307,6 +311,10 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeUserSecretDelete,
|
||||
ScopeUserSecretRead,
|
||||
ScopeUserSecretUpdate,
|
||||
ScopeUserSkillCreate,
|
||||
ScopeUserSkillDelete,
|
||||
ScopeUserSkillRead,
|
||||
ScopeUserSkillUpdate,
|
||||
ScopeWebpushSubscriptionCreate,
|
||||
ScopeWebpushSubscriptionDelete,
|
||||
ScopeWebpushSubscriptionRead,
|
||||
@@ -481,6 +489,10 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeUserSecretDelete,
|
||||
ScopeUserSecretRead,
|
||||
ScopeUserSecretUpdate,
|
||||
ScopeUserSkillCreate,
|
||||
ScopeUserSkillDelete,
|
||||
ScopeUserSkillRead,
|
||||
ScopeUserSkillUpdate,
|
||||
ScopeWebpushSubscriptionCreate,
|
||||
ScopeWebpushSubscriptionDelete,
|
||||
ScopeWebpushSubscriptionRead,
|
||||
|
||||
Reference in New Issue
Block a user