mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
5a8d0016a5
> 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`
130 lines
3.3 KiB
Go
130 lines
3.3 KiB
Go
package rbac
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// externalLowLevel is the curated set of low-level scope names exposed to users.
|
|
// Any valid resource:action pair not in this set is considered internal-only
|
|
// and must not be user-requestable.
|
|
var externalLowLevel = map[ScopeName]struct{}{
|
|
// Workspaces
|
|
"workspace:read": {},
|
|
"workspace:create": {},
|
|
"workspace:update": {},
|
|
"workspace:delete": {},
|
|
"workspace:ssh": {},
|
|
"workspace:start": {},
|
|
"workspace:stop": {},
|
|
"workspace:application_connect": {},
|
|
"workspace:*": {},
|
|
|
|
// Templates
|
|
"template:read": {},
|
|
"template:create": {},
|
|
"template:update": {},
|
|
"template:delete": {},
|
|
"template:use": {},
|
|
"template:*": {},
|
|
|
|
// API keys (self-management)
|
|
"api_key:read": {},
|
|
"api_key:create": {},
|
|
"api_key:update": {},
|
|
"api_key:delete": {},
|
|
"api_key:*": {},
|
|
|
|
// Files
|
|
"file:read": {},
|
|
"file:create": {},
|
|
"file:*": {},
|
|
|
|
// Users
|
|
"user:read": {},
|
|
"user:read_personal": {},
|
|
"user:update_personal": {},
|
|
"user.*": {},
|
|
|
|
// User secrets
|
|
"user_secret:read": {},
|
|
"user_secret:create": {},
|
|
"user_secret:update": {},
|
|
"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": {},
|
|
"task:update": {},
|
|
"task:delete": {},
|
|
"task:*": {},
|
|
|
|
// Organizations
|
|
"organization:read": {},
|
|
"organization:update": {},
|
|
"organization:delete": {},
|
|
"organization:*": {},
|
|
}
|
|
|
|
// Public composite coder:* scopes exposed to users.
|
|
var externalComposite = map[ScopeName]struct{}{
|
|
"coder:workspaces.create": {},
|
|
"coder:workspaces.operate": {},
|
|
"coder:workspaces.delete": {},
|
|
"coder:workspaces.access": {},
|
|
"coder:templates.build": {},
|
|
"coder:templates.author": {},
|
|
"coder:apikeys.manage_self": {},
|
|
}
|
|
|
|
// IsExternalScope returns true if the scope is public, including the
|
|
// `all` and `application_connect` special scopes and the curated
|
|
// low-level resource:action scopes.
|
|
func IsExternalScope(name ScopeName) bool {
|
|
switch name {
|
|
// Include `all` and `application_connect` for backward compatibility.
|
|
case "all", ScopeAll, "application_connect", ScopeApplicationConnect:
|
|
return true
|
|
}
|
|
if _, ok := externalLowLevel[name]; ok {
|
|
return true
|
|
}
|
|
if _, ok := externalComposite[name]; ok {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ExternalScopeNames returns a sorted list of all public scopes, which
|
|
// includes the `all` and `application_connect` special scopes, curated
|
|
// low-level resource:action names, and curated composite coder:* scopes.
|
|
func ExternalScopeNames() []string {
|
|
names := make([]string, 0, len(externalLowLevel)+len(externalComposite)+2)
|
|
names = append(names, string(ScopeAll))
|
|
names = append(names, string(ScopeApplicationConnect))
|
|
|
|
// curated low-level names, filtered for validity
|
|
for name := range externalLowLevel {
|
|
if _, _, ok := parseLowLevelScope(name); ok {
|
|
names = append(names, string(name))
|
|
}
|
|
}
|
|
|
|
// curated composite names
|
|
for name := range externalComposite {
|
|
names = append(names, string(name))
|
|
}
|
|
|
|
sort.Slice(names, func(i, j int) bool { return strings.Compare(names[i], names[j]) < 0 })
|
|
return names
|
|
}
|