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:
Generated
+304
-2
@@ -877,6 +877,227 @@ const docTemplate = `{
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/experimental/users/{user}/skills": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "List user skills",
|
||||||
|
"operationId": "list-user-skills",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkillMetadata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Create a user skill",
|
||||||
|
"operationId": "create-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Create user skill request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.CreateUserSkillRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/experimental/users/{user}/skills/{skillName}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Get a user skill by name",
|
||||||
|
"operationId": "get-a-user-skill-by-name",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Delete a user skill",
|
||||||
|
"operationId": "delete-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Users"
|
||||||
|
],
|
||||||
|
"summary": "Update a user skill",
|
||||||
|
"operationId": "update-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update user skill request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/experimental/watch-all-workspacebuilds": {
|
"/api/experimental/watch-all-workspacebuilds": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -14802,6 +15023,11 @@ const docTemplate = `{
|
|||||||
"user_secret:delete",
|
"user_secret:delete",
|
||||||
"user_secret:read",
|
"user_secret:read",
|
||||||
"user_secret:update",
|
"user_secret:update",
|
||||||
|
"user_skill:*",
|
||||||
|
"user_skill:create",
|
||||||
|
"user_skill:delete",
|
||||||
|
"user_skill:read",
|
||||||
|
"user_skill:update",
|
||||||
"webpush_subscription:*",
|
"webpush_subscription:*",
|
||||||
"webpush_subscription:create",
|
"webpush_subscription:create",
|
||||||
"webpush_subscription:delete",
|
"webpush_subscription:delete",
|
||||||
@@ -15023,6 +15249,11 @@ const docTemplate = `{
|
|||||||
"APIKeyScopeUserSecretDelete",
|
"APIKeyScopeUserSecretDelete",
|
||||||
"APIKeyScopeUserSecretRead",
|
"APIKeyScopeUserSecretRead",
|
||||||
"APIKeyScopeUserSecretUpdate",
|
"APIKeyScopeUserSecretUpdate",
|
||||||
|
"APIKeyScopeUserSkillAll",
|
||||||
|
"APIKeyScopeUserSkillCreate",
|
||||||
|
"APIKeyScopeUserSkillDelete",
|
||||||
|
"APIKeyScopeUserSkillRead",
|
||||||
|
"APIKeyScopeUserSkillUpdate",
|
||||||
"APIKeyScopeWebpushSubscriptionAll",
|
"APIKeyScopeWebpushSubscriptionAll",
|
||||||
"APIKeyScopeWebpushSubscriptionCreate",
|
"APIKeyScopeWebpushSubscriptionCreate",
|
||||||
"APIKeyScopeWebpushSubscriptionDelete",
|
"APIKeyScopeWebpushSubscriptionDelete",
|
||||||
@@ -17400,6 +17631,15 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.CreateUserSkillRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.CreateWorkspaceBuildReason": {
|
"codersdk.CreateWorkspaceBuildReason": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -21537,6 +21777,7 @@ const docTemplate = `{
|
|||||||
"usage_event",
|
"usage_event",
|
||||||
"user",
|
"user",
|
||||||
"user_secret",
|
"user_secret",
|
||||||
|
"user_skill",
|
||||||
"webpush_subscription",
|
"webpush_subscription",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace_agent_devcontainers",
|
"workspace_agent_devcontainers",
|
||||||
@@ -21586,6 +21827,7 @@ const docTemplate = `{
|
|||||||
"ResourceUsageEvent",
|
"ResourceUsageEvent",
|
||||||
"ResourceUser",
|
"ResourceUser",
|
||||||
"ResourceUserSecret",
|
"ResourceUserSecret",
|
||||||
|
"ResourceUserSkill",
|
||||||
"ResourceWebpushSubscription",
|
"ResourceWebpushSubscription",
|
||||||
"ResourceWorkspace",
|
"ResourceWorkspace",
|
||||||
"ResourceWorkspaceAgentDevcontainers",
|
"ResourceWorkspaceAgentDevcontainers",
|
||||||
@@ -21811,7 +22053,8 @@ const docTemplate = `{
|
|||||||
"ai_provider_key",
|
"ai_provider_key",
|
||||||
"group_ai_budget",
|
"group_ai_budget",
|
||||||
"chat",
|
"chat",
|
||||||
"user_secret"
|
"user_secret",
|
||||||
|
"user_skill"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ResourceTypeTemplate",
|
"ResourceTypeTemplate",
|
||||||
@@ -21845,7 +22088,8 @@ const docTemplate = `{
|
|||||||
"ResourceTypeAIProviderKey",
|
"ResourceTypeAIProviderKey",
|
||||||
"ResourceTypeGroupAIBudget",
|
"ResourceTypeGroupAIBudget",
|
||||||
"ResourceTypeChat",
|
"ResourceTypeChat",
|
||||||
"ResourceTypeUserSecret"
|
"ResourceTypeUserSecret",
|
||||||
|
"ResourceTypeUserSkill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Response": {
|
"codersdk.Response": {
|
||||||
@@ -23781,6 +24025,15 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpdateUserSkillRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UpdateWorkspaceACL": {
|
"codersdk.UpdateWorkspaceACL": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -24307,6 +24560,55 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UserSkill": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.UserSkillMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UserStatus": {
|
"codersdk.UserStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
Generated
+282
-2
@@ -774,6 +774,205 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/experimental/users/{user}/skills": {
|
||||||
|
"get": {
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Users"],
|
||||||
|
"summary": "List user skills",
|
||||||
|
"operationId": "list-user-skills",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkillMetadata"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Users"],
|
||||||
|
"summary": "Create a user skill",
|
||||||
|
"operationId": "create-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Create user skill request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.CreateUserSkillRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/experimental/users/{user}/skills/{skillName}": {
|
||||||
|
"get": {
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Users"],
|
||||||
|
"summary": "Get a user skill by name",
|
||||||
|
"operationId": "get-a-user-skill-by-name",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": ["Users"],
|
||||||
|
"summary": "Delete a user skill",
|
||||||
|
"operationId": "delete-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"consumes": ["application/json"],
|
||||||
|
"produces": ["application/json"],
|
||||||
|
"tags": ["Users"],
|
||||||
|
"summary": "Update a user skill",
|
||||||
|
"operationId": "update-a-user-skill",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "User ID, username, or me",
|
||||||
|
"name": "user",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Skill name",
|
||||||
|
"name": "skillName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Update user skill request",
|
||||||
|
"name": "request",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UpdateUserSkillRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/codersdk.UserSkill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"CoderSessionToken": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-apidocgen": {
|
||||||
|
"skip": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/experimental/watch-all-workspacebuilds": {
|
"/api/experimental/watch-all-workspacebuilds": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": ["application/json"],
|
"produces": ["application/json"],
|
||||||
@@ -13254,6 +13453,11 @@
|
|||||||
"user_secret:delete",
|
"user_secret:delete",
|
||||||
"user_secret:read",
|
"user_secret:read",
|
||||||
"user_secret:update",
|
"user_secret:update",
|
||||||
|
"user_skill:*",
|
||||||
|
"user_skill:create",
|
||||||
|
"user_skill:delete",
|
||||||
|
"user_skill:read",
|
||||||
|
"user_skill:update",
|
||||||
"webpush_subscription:*",
|
"webpush_subscription:*",
|
||||||
"webpush_subscription:create",
|
"webpush_subscription:create",
|
||||||
"webpush_subscription:delete",
|
"webpush_subscription:delete",
|
||||||
@@ -13475,6 +13679,11 @@
|
|||||||
"APIKeyScopeUserSecretDelete",
|
"APIKeyScopeUserSecretDelete",
|
||||||
"APIKeyScopeUserSecretRead",
|
"APIKeyScopeUserSecretRead",
|
||||||
"APIKeyScopeUserSecretUpdate",
|
"APIKeyScopeUserSecretUpdate",
|
||||||
|
"APIKeyScopeUserSkillAll",
|
||||||
|
"APIKeyScopeUserSkillCreate",
|
||||||
|
"APIKeyScopeUserSkillDelete",
|
||||||
|
"APIKeyScopeUserSkillRead",
|
||||||
|
"APIKeyScopeUserSkillUpdate",
|
||||||
"APIKeyScopeWebpushSubscriptionAll",
|
"APIKeyScopeWebpushSubscriptionAll",
|
||||||
"APIKeyScopeWebpushSubscriptionCreate",
|
"APIKeyScopeWebpushSubscriptionCreate",
|
||||||
"APIKeyScopeWebpushSubscriptionDelete",
|
"APIKeyScopeWebpushSubscriptionDelete",
|
||||||
@@ -15752,6 +15961,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.CreateUserSkillRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.CreateWorkspaceBuildReason": {
|
"codersdk.CreateWorkspaceBuildReason": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -19749,6 +19967,7 @@
|
|||||||
"usage_event",
|
"usage_event",
|
||||||
"user",
|
"user",
|
||||||
"user_secret",
|
"user_secret",
|
||||||
|
"user_skill",
|
||||||
"webpush_subscription",
|
"webpush_subscription",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace_agent_devcontainers",
|
"workspace_agent_devcontainers",
|
||||||
@@ -19798,6 +20017,7 @@
|
|||||||
"ResourceUsageEvent",
|
"ResourceUsageEvent",
|
||||||
"ResourceUser",
|
"ResourceUser",
|
||||||
"ResourceUserSecret",
|
"ResourceUserSecret",
|
||||||
|
"ResourceUserSkill",
|
||||||
"ResourceWebpushSubscription",
|
"ResourceWebpushSubscription",
|
||||||
"ResourceWorkspace",
|
"ResourceWorkspace",
|
||||||
"ResourceWorkspaceAgentDevcontainers",
|
"ResourceWorkspaceAgentDevcontainers",
|
||||||
@@ -20013,7 +20233,8 @@
|
|||||||
"ai_provider_key",
|
"ai_provider_key",
|
||||||
"group_ai_budget",
|
"group_ai_budget",
|
||||||
"chat",
|
"chat",
|
||||||
"user_secret"
|
"user_secret",
|
||||||
|
"user_skill"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ResourceTypeTemplate",
|
"ResourceTypeTemplate",
|
||||||
@@ -20047,7 +20268,8 @@
|
|||||||
"ResourceTypeAIProviderKey",
|
"ResourceTypeAIProviderKey",
|
||||||
"ResourceTypeGroupAIBudget",
|
"ResourceTypeGroupAIBudget",
|
||||||
"ResourceTypeChat",
|
"ResourceTypeChat",
|
||||||
"ResourceTypeUserSecret"
|
"ResourceTypeUserSecret",
|
||||||
|
"ResourceTypeUserSkill"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Response": {
|
"codersdk.Response": {
|
||||||
@@ -21887,6 +22109,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UpdateUserSkillRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"description": "Content must be SKILL.md-format Markdown with YAML frontmatter. The\nfrontmatter must include name, may include description, and must be\nfollowed by a non-empty body.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UpdateWorkspaceACL": {
|
"codersdk.UpdateWorkspaceACL": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -22388,6 +22619,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UserSkill": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codersdk.UserSkillMetadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UserStatus": {
|
"codersdk.UserStatus": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["active", "dormant", "suspended"],
|
"enum": ["active", "dormant", "suspended"],
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ type Auditable interface {
|
|||||||
database.AIProviderKey |
|
database.AIProviderKey |
|
||||||
database.Chat |
|
database.Chat |
|
||||||
database.AuditableGroupAiBudget |
|
database.AuditableGroupAiBudget |
|
||||||
database.UserSecret
|
database.UserSecret |
|
||||||
|
database.UserSkill
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map is a map of changed fields in an audited resource. It maps field names to
|
// Map is a map of changed fields in an audited resource. It maps field names to
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ func ResourceTarget[T Auditable](tgt T) string {
|
|||||||
return typed.ID.String()[:8]
|
return typed.ID.String()[:8]
|
||||||
case database.UserSecret:
|
case database.UserSecret:
|
||||||
return typed.Name
|
return typed.Name
|
||||||
|
case database.UserSkill:
|
||||||
|
return typed.Name
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceTarget", tgt))
|
||||||
}
|
}
|
||||||
@@ -229,6 +231,8 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
|
|||||||
return typed.ID
|
return typed.ID
|
||||||
case database.UserSecret:
|
case database.UserSecret:
|
||||||
return typed.ID
|
return typed.ID
|
||||||
|
case database.UserSkill:
|
||||||
|
return typed.ID
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceID", tgt))
|
||||||
}
|
}
|
||||||
@@ -296,6 +300,8 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
|
|||||||
return database.ResourceTypeChat
|
return database.ResourceTypeChat
|
||||||
case database.UserSecret:
|
case database.UserSecret:
|
||||||
return database.ResourceTypeUserSecret
|
return database.ResourceTypeUserSecret
|
||||||
|
case database.UserSkill:
|
||||||
|
return database.ResourceTypeUserSkill
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
|
panic(fmt.Sprintf("unknown resource %T for ResourceType", typed))
|
||||||
}
|
}
|
||||||
@@ -372,6 +378,9 @@ func ResourceRequiresOrgID[T Auditable]() bool {
|
|||||||
case database.UserSecret:
|
case database.UserSecret:
|
||||||
// User secrets are global to the user across organizations.
|
// User secrets are global to the user across organizations.
|
||||||
return false
|
return false
|
||||||
|
case database.UserSkill:
|
||||||
|
// User skills are global to the user across organizations.
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
|
panic(fmt.Sprintf("unknown resource %T for ResourceRequiresOrgID", tgt))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1184,6 +1184,19 @@ func New(options *Options) *API {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
r.Route("/users/{user}/skills", func(r chi.Router) {
|
||||||
|
r.Use(
|
||||||
|
apiKeyMiddleware,
|
||||||
|
httpmw.ExtractUserParam(options.Database),
|
||||||
|
)
|
||||||
|
r.Post("/", api.postUserSkill)
|
||||||
|
r.Get("/", api.getUserSkills)
|
||||||
|
r.Route("/{skillName}", func(r chi.Router) {
|
||||||
|
r.Get("/", api.getUserSkill)
|
||||||
|
r.Patch("/", api.patchUserSkill)
|
||||||
|
r.Delete("/", api.deleteUserSkill)
|
||||||
|
})
|
||||||
|
})
|
||||||
r.Route("/chats", func(r chi.Router) {
|
r.Route("/chats", func(r chi.Router) {
|
||||||
r.Use(
|
r.Use(
|
||||||
apiKeyMiddleware,
|
apiKeyMiddleware,
|
||||||
|
|||||||
@@ -45,4 +45,8 @@ const (
|
|||||||
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
|
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
|
||||||
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
|
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
|
||||||
CheckUserChatProviderKeysAPIKeyCheck CheckConstraint = "user_chat_provider_keys_api_key_check" // user_chat_provider_keys
|
CheckUserChatProviderKeysAPIKeyCheck CheckConstraint = "user_chat_provider_keys_api_key_check" // user_chat_provider_keys
|
||||||
|
CheckUserSkillsContentSize CheckConstraint = "user_skills_content_size" // user_skills
|
||||||
|
CheckUserSkillsDescriptionSize CheckConstraint = "user_skills_description_size" // user_skills
|
||||||
|
CheckUserSkillsNameFormat CheckConstraint = "user_skills_name_format" // user_skills
|
||||||
|
CheckUserSkillsNameSize CheckConstraint = "user_skills_name_size" // user_skills
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2046,3 +2046,37 @@ func UserSecrets(secrets []database.ListUserSecretsRow) []codersdk.UserSecret {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserSkill converts a database UserSkill to an SDK UserSkill.
|
||||||
|
func UserSkill(skill database.UserSkill) codersdk.UserSkill {
|
||||||
|
return codersdk.UserSkill{
|
||||||
|
UserSkillMetadata: codersdk.UserSkillMetadata{
|
||||||
|
ID: skill.ID,
|
||||||
|
Name: skill.Name,
|
||||||
|
Description: skill.Description,
|
||||||
|
CreatedAt: skill.CreatedAt,
|
||||||
|
UpdatedAt: skill.UpdatedAt,
|
||||||
|
},
|
||||||
|
Content: skill.Content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSkillMetadata converts database user skill metadata to an SDK UserSkillMetadata.
|
||||||
|
func UserSkillMetadata(skill database.ListUserSkillMetadataByUserIDRow) codersdk.UserSkillMetadata {
|
||||||
|
return codersdk.UserSkillMetadata{
|
||||||
|
ID: skill.ID,
|
||||||
|
Name: skill.Name,
|
||||||
|
Description: skill.Description,
|
||||||
|
CreatedAt: skill.CreatedAt,
|
||||||
|
UpdatedAt: skill.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSkillMetadataList converts database user skill metadata rows to SDK values.
|
||||||
|
func UserSkillMetadataList(rows []database.ListUserSkillMetadataByUserIDRow) []codersdk.UserSkillMetadata {
|
||||||
|
metadata := make([]codersdk.UserSkillMetadata, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
metadata = append(metadata, UserSkillMetadata(row))
|
||||||
|
}
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|||||||
@@ -2290,6 +2290,14 @@ func (q *querier) DeleteUserSecretByUserIDAndName(ctx context.Context, arg datab
|
|||||||
return q.db.DeleteUserSecretByUserIDAndName(ctx, arg)
|
return q.db.DeleteUserSecretByUserIDAndName(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionDelete, obj); err != nil {
|
||||||
|
return database.UserSkill{}, err
|
||||||
|
}
|
||||||
|
return q.db.DeleteUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
func (q *querier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -4687,6 +4695,14 @@ func (q *querier) GetUserShellToolDisplayMode(ctx context.Context, userID uuid.U
|
|||||||
return q.db.GetUserShellToolDisplayMode(ctx, userID)
|
return q.db.GetUserShellToolDisplayMode(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil {
|
||||||
|
return database.UserSkill{}, err
|
||||||
|
}
|
||||||
|
return q.db.GetUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -5780,6 +5796,14 @@ func (q *querier) InsertUserLink(ctx context.Context, arg database.InsertUserLin
|
|||||||
return q.db.InsertUserLink(ctx, arg)
|
return q.db.InsertUserLink(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||||
|
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil {
|
||||||
|
return database.UserSkill{}, err
|
||||||
|
}
|
||||||
|
return q.db.InsertUserSkill(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
func (q *querier) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
||||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil {
|
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceWorkspaceAgentResourceMonitor); err != nil {
|
||||||
return database.WorkspaceAgentVolumeResourceMonitor{}, err
|
return database.WorkspaceAgentVolumeResourceMonitor{}, err
|
||||||
@@ -6139,6 +6163,14 @@ func (q *querier) ListUserSecretsWithValues(ctx context.Context, userID uuid.UUI
|
|||||||
return q.db.ListUserSecretsWithValues(ctx, userID)
|
return q.db.ListUserSecretsWithValues(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]database.ListUserSkillMetadataByUserIDRow, error) {
|
||||||
|
obj := rbac.ResourceUserSkill.WithOwner(userID.String())
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return q.db.ListUserSkillMetadataByUserID(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
|
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -7446,6 +7478,14 @@ func (q *querier) UpdateUserShellToolDisplayMode(ctx context.Context, arg databa
|
|||||||
return q.db.UpdateUserShellToolDisplayMode(ctx, arg)
|
return q.db.UpdateUserShellToolDisplayMode(ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *querier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||||
|
if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil {
|
||||||
|
return database.UserSkill{}, err
|
||||||
|
}
|
||||||
|
return q.db.UpdateUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
||||||
fetch := func(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
fetch := func(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
||||||
return q.db.GetUserByID(ctx, arg.ID)
|
return q.db.GetUserByID(ctx, arg.ID)
|
||||||
|
|||||||
@@ -6046,6 +6046,61 @@ func (s *MethodTestSuite) TestUserSecrets() {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *MethodTestSuite) TestUserSkills() {
|
||||||
|
s.Run("GetUserSkillByUserIDAndName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
|
user := testutil.Fake(s.T(), faker, database.User{})
|
||||||
|
skill := testutil.Fake(s.T(), faker, database.UserSkill{UserID: user.ID})
|
||||||
|
arg := database.GetUserSkillByUserIDAndNameParams{UserID: user.ID, Name: skill.Name}
|
||||||
|
dbm.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), arg).Return(skill, nil).AnyTimes()
|
||||||
|
check.Args(arg).
|
||||||
|
Asserts(rbac.ResourceUserSkill.WithOwner(user.ID.String()), policy.ActionRead).
|
||||||
|
Returns(skill)
|
||||||
|
}))
|
||||||
|
s.Run("ListUserSkillMetadataByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
|
user := testutil.Fake(s.T(), faker, database.User{})
|
||||||
|
row := testutil.Fake(s.T(), faker, database.ListUserSkillMetadataByUserIDRow{UserID: user.ID})
|
||||||
|
dbm.EXPECT().ListUserSkillMetadataByUserID(gomock.Any(), user.ID).Return([]database.ListUserSkillMetadataByUserIDRow{row}, nil).AnyTimes()
|
||||||
|
check.Args(user.ID).
|
||||||
|
Asserts(rbac.ResourceUserSkill.WithOwner(user.ID.String()), policy.ActionRead).
|
||||||
|
Returns([]database.ListUserSkillMetadataByUserIDRow{row})
|
||||||
|
}))
|
||||||
|
s.Run("InsertUserSkill", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
|
user := testutil.Fake(s.T(), faker, database.User{})
|
||||||
|
arg := database.InsertUserSkillParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: "test",
|
||||||
|
}
|
||||||
|
ret := testutil.Fake(s.T(), faker, database.UserSkill{
|
||||||
|
ID: arg.ID,
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: arg.Name,
|
||||||
|
})
|
||||||
|
dbm.EXPECT().InsertUserSkill(gomock.Any(), arg).Return(ret, nil).AnyTimes()
|
||||||
|
check.Args(arg).
|
||||||
|
Asserts(rbac.ResourceUserSkill.WithOwner(user.ID.String()), policy.ActionCreate).
|
||||||
|
Returns(ret)
|
||||||
|
}))
|
||||||
|
s.Run("UpdateUserSkillByUserIDAndName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
|
user := testutil.Fake(s.T(), faker, database.User{})
|
||||||
|
arg := database.UpdateUserSkillByUserIDAndNameParams{UserID: user.ID, Name: "test"}
|
||||||
|
updated := testutil.Fake(s.T(), faker, database.UserSkill{UserID: user.ID, Name: arg.Name})
|
||||||
|
dbm.EXPECT().UpdateUserSkillByUserIDAndName(gomock.Any(), arg).Return(updated, nil).AnyTimes()
|
||||||
|
check.Args(arg).
|
||||||
|
Asserts(rbac.ResourceUserSkill.WithOwner(user.ID.String()), policy.ActionUpdate).
|
||||||
|
Returns(updated)
|
||||||
|
}))
|
||||||
|
s.Run("DeleteUserSkillByUserIDAndName", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
|
user := testutil.Fake(s.T(), faker, database.User{})
|
||||||
|
arg := database.DeleteUserSkillByUserIDAndNameParams{UserID: user.ID, Name: "test"}
|
||||||
|
deleted := testutil.Fake(s.T(), faker, database.UserSkill{UserID: user.ID, Name: arg.Name})
|
||||||
|
dbm.EXPECT().DeleteUserSkillByUserIDAndName(gomock.Any(), arg).Return(deleted, nil).AnyTimes()
|
||||||
|
check.Args(arg).
|
||||||
|
Asserts(rbac.ResourceUserSkill.WithOwner(user.ID.String()), policy.ActionDelete).
|
||||||
|
Returns(deleted)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MethodTestSuite) TestUsageEvents() {
|
func (s *MethodTestSuite) TestUsageEvents() {
|
||||||
s.Run("InsertUsageEvent", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
s.Run("InsertUsageEvent", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||||
params := database.InsertUsageEventParams{
|
params := database.InsertUsageEventParams{
|
||||||
|
|||||||
@@ -809,6 +809,14 @@ func (m queryMetricsStore) DeleteUserSecretByUserIDAndName(ctx context.Context,
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m queryMetricsStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.DeleteUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("DeleteUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||||
|
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteUserSkillByUserIDAndName").Inc()
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m queryMetricsStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
func (m queryMetricsStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0 := m.s.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg)
|
r0 := m.s.DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx, arg)
|
||||||
@@ -3073,6 +3081,14 @@ func (m queryMetricsStore) GetUserShellToolDisplayMode(ctx context.Context, user
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m queryMetricsStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.GetUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("GetUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||||
|
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetUserSkillByUserIDAndName").Inc()
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
func (m queryMetricsStore) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.GetUserStatusCounts(ctx, arg)
|
r0, r1 := m.s.GetUserStatusCounts(ctx, arg)
|
||||||
@@ -4097,6 +4113,14 @@ func (m queryMetricsStore) InsertUserLink(ctx context.Context, arg database.Inse
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m queryMetricsStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.InsertUserSkill(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("InsertUserSkill").Observe(time.Since(start).Seconds())
|
||||||
|
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertUserSkill").Inc()
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m queryMetricsStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
func (m queryMetricsStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.InsertVolumeResourceMonitor(ctx, arg)
|
r0, r1 := m.s.InsertVolumeResourceMonitor(ctx, arg)
|
||||||
@@ -4409,6 +4433,14 @@ func (m queryMetricsStore) ListUserSecretsWithValues(ctx context.Context, userID
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m queryMetricsStore) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]database.ListUserSkillMetadataByUserIDRow, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.ListUserSkillMetadataByUserID(ctx, userID)
|
||||||
|
m.queryLatencies.WithLabelValues("ListUserSkillMetadataByUserID").Observe(time.Since(start).Seconds())
|
||||||
|
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListUserSkillMetadataByUserID").Inc()
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m queryMetricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
func (m queryMetricsStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID)
|
r0, r1 := m.s.ListWorkspaceAgentPortShares(ctx, workspaceID)
|
||||||
@@ -5329,6 +5361,14 @@ func (m queryMetricsStore) UpdateUserShellToolDisplayMode(ctx context.Context, a
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m queryMetricsStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
start := time.Now()
|
||||||
|
r0, r1 := m.s.UpdateUserSkillByUserIDAndName(ctx, arg)
|
||||||
|
m.queryLatencies.WithLabelValues("UpdateUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||||
|
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateUserSkillByUserIDAndName").Inc()
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
func (m queryMetricsStore) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
r0, r1 := m.s.UpdateUserStatus(ctx, arg)
|
r0, r1 := m.s.UpdateUserStatus(ctx, arg)
|
||||||
|
|||||||
@@ -1377,6 +1377,21 @@ func (mr *MockStoreMockRecorder) DeleteUserSecretByUserIDAndName(ctx, arg any) *
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserSecretByUserIDAndName", reflect.TypeOf((*MockStore)(nil).DeleteUserSecretByUserIDAndName), ctx, arg)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserSecretByUserIDAndName", reflect.TypeOf((*MockStore)(nil).DeleteUserSecretByUserIDAndName), ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteUserSkillByUserIDAndName mocks base method.
|
||||||
|
func (m *MockStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DeleteUserSkillByUserIDAndName", ctx, arg)
|
||||||
|
ret0, _ := ret[0].(database.UserSkill)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserSkillByUserIDAndName indicates an expected call of DeleteUserSkillByUserIDAndName.
|
||||||
|
func (mr *MockStoreMockRecorder) DeleteUserSkillByUserIDAndName(ctx, arg any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserSkillByUserIDAndName", reflect.TypeOf((*MockStore)(nil).DeleteUserSkillByUserIDAndName), ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteWebpushSubscriptionByUserIDAndEndpoint mocks base method.
|
// DeleteWebpushSubscriptionByUserIDAndEndpoint mocks base method.
|
||||||
func (m *MockStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
func (m *MockStore) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -5761,6 +5776,21 @@ func (mr *MockStoreMockRecorder) GetUserShellToolDisplayMode(ctx, userID any) *g
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserShellToolDisplayMode", reflect.TypeOf((*MockStore)(nil).GetUserShellToolDisplayMode), ctx, userID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserShellToolDisplayMode", reflect.TypeOf((*MockStore)(nil).GetUserShellToolDisplayMode), ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserSkillByUserIDAndName mocks base method.
|
||||||
|
func (m *MockStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetUserSkillByUserIDAndName", ctx, arg)
|
||||||
|
ret0, _ := ret[0].(database.UserSkill)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserSkillByUserIDAndName indicates an expected call of GetUserSkillByUserIDAndName.
|
||||||
|
func (mr *MockStoreMockRecorder) GetUserSkillByUserIDAndName(ctx, arg any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserSkillByUserIDAndName", reflect.TypeOf((*MockStore)(nil).GetUserSkillByUserIDAndName), ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserStatusCounts mocks base method.
|
// GetUserStatusCounts mocks base method.
|
||||||
func (m *MockStore) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
func (m *MockStore) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -7685,6 +7715,21 @@ func (mr *MockStoreMockRecorder) InsertUserLink(ctx, arg any) *gomock.Call {
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserLink", reflect.TypeOf((*MockStore)(nil).InsertUserLink), ctx, arg)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserLink", reflect.TypeOf((*MockStore)(nil).InsertUserLink), ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InsertUserSkill mocks base method.
|
||||||
|
func (m *MockStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "InsertUserSkill", ctx, arg)
|
||||||
|
ret0, _ := ret[0].(database.UserSkill)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertUserSkill indicates an expected call of InsertUserSkill.
|
||||||
|
func (mr *MockStoreMockRecorder) InsertUserSkill(ctx, arg any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUserSkill", reflect.TypeOf((*MockStore)(nil).InsertUserSkill), ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
// InsertVolumeResourceMonitor mocks base method.
|
// InsertVolumeResourceMonitor mocks base method.
|
||||||
func (m *MockStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
func (m *MockStore) InsertVolumeResourceMonitor(ctx context.Context, arg database.InsertVolumeResourceMonitorParams) (database.WorkspaceAgentVolumeResourceMonitor, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -8340,6 +8385,21 @@ func (mr *MockStoreMockRecorder) ListUserSecretsWithValues(ctx, userID any) *gom
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUserSecretsWithValues", reflect.TypeOf((*MockStore)(nil).ListUserSecretsWithValues), ctx, userID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUserSecretsWithValues", reflect.TypeOf((*MockStore)(nil).ListUserSecretsWithValues), ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListUserSkillMetadataByUserID mocks base method.
|
||||||
|
func (m *MockStore) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]database.ListUserSkillMetadataByUserIDRow, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ListUserSkillMetadataByUserID", ctx, userID)
|
||||||
|
ret0, _ := ret[0].([]database.ListUserSkillMetadataByUserIDRow)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUserSkillMetadataByUserID indicates an expected call of ListUserSkillMetadataByUserID.
|
||||||
|
func (mr *MockStoreMockRecorder) ListUserSkillMetadataByUserID(ctx, userID any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUserSkillMetadataByUserID", reflect.TypeOf((*MockStore)(nil).ListUserSkillMetadataByUserID), ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
// ListWorkspaceAgentPortShares mocks base method.
|
// ListWorkspaceAgentPortShares mocks base method.
|
||||||
func (m *MockStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
func (m *MockStore) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -10048,6 +10108,21 @@ func (mr *MockStoreMockRecorder) UpdateUserShellToolDisplayMode(ctx, arg any) *g
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserShellToolDisplayMode", reflect.TypeOf((*MockStore)(nil).UpdateUserShellToolDisplayMode), ctx, arg)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserShellToolDisplayMode", reflect.TypeOf((*MockStore)(nil).UpdateUserShellToolDisplayMode), ctx, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserSkillByUserIDAndName mocks base method.
|
||||||
|
func (m *MockStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateUserSkillByUserIDAndName", ctx, arg)
|
||||||
|
ret0, _ := ret[0].(database.UserSkill)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserSkillByUserIDAndName indicates an expected call of UpdateUserSkillByUserIDAndName.
|
||||||
|
func (mr *MockStoreMockRecorder) UpdateUserSkillByUserIDAndName(ctx, arg any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserSkillByUserIDAndName", reflect.TypeOf((*MockStore)(nil).UpdateUserSkillByUserIDAndName), ctx, arg)
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUserStatus mocks base method.
|
// UpdateUserStatus mocks base method.
|
||||||
func (m *MockStore) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
func (m *MockStore) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
Generated
+107
-24
@@ -243,7 +243,12 @@ CREATE TYPE api_key_scope AS ENUM (
|
|||||||
'ai_provider:delete',
|
'ai_provider:delete',
|
||||||
'ai_provider:read',
|
'ai_provider:read',
|
||||||
'ai_provider:update',
|
'ai_provider:update',
|
||||||
'chat:share'
|
'chat:share',
|
||||||
|
'user_skill:create',
|
||||||
|
'user_skill:read',
|
||||||
|
'user_skill:update',
|
||||||
|
'user_skill:delete',
|
||||||
|
'user_skill:*'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE app_sharing_level AS ENUM (
|
CREATE TYPE app_sharing_level AS ENUM (
|
||||||
@@ -553,7 +558,8 @@ CREATE TYPE resource_type AS ENUM (
|
|||||||
'user_secret',
|
'user_secret',
|
||||||
'ai_provider',
|
'ai_provider',
|
||||||
'ai_provider_key',
|
'ai_provider_key',
|
||||||
'group_ai_budget'
|
'group_ai_budget',
|
||||||
|
'user_skill'
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE shareable_workspace_owners AS ENUM (
|
CREATE TYPE shareable_workspace_owners AS ENUM (
|
||||||
@@ -771,31 +777,37 @@ CREATE FUNCTION delete_deleted_user_resources() RETURNS trigger
|
|||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
BEGIN
|
BEGIN
|
||||||
IF (NEW.deleted) THEN
|
IF (NEW.deleted) THEN
|
||||||
-- Remove their api_keys
|
-- Remove their api_keys.
|
||||||
DELETE FROM api_keys
|
DELETE FROM api_keys
|
||||||
WHERE user_id = OLD.id;
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
-- Remove their user_links
|
-- Remove their user_links.
|
||||||
-- Their login_type is preserved in the users table.
|
-- Their login_type is preserved in the users table.
|
||||||
-- Matching this user back to the link can still be done by their
|
-- Matching this user back to the link can still be done by their
|
||||||
-- email if the account is undeleted. Although that is not a guarantee.
|
-- email if the account is undeleted. Although that is not a guarantee.
|
||||||
DELETE FROM user_links
|
DELETE FROM user_links
|
||||||
WHERE user_id = OLD.id;
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
-- Remove their user_secrets.
|
-- Remove their user_secrets.
|
||||||
-- user_secrets.user_id has ON DELETE CASCADE, but soft-delete
|
-- user_secrets.user_id has ON DELETE CASCADE, but soft-delete
|
||||||
-- does not remove the users row so the FK cascade never fires.
|
-- does not remove the users row so the FK cascade never fires.
|
||||||
DELETE FROM user_secrets
|
DELETE FROM user_secrets
|
||||||
WHERE user_id = OLD.id;
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
-- Remove their organization memberships.
|
-- Remove their organization memberships.
|
||||||
-- This also triggers group membership cleanup via
|
-- This also triggers group membership cleanup via
|
||||||
-- trigger_delete_group_members_on_org_member_delete.
|
-- trigger_delete_group_members_on_org_member_delete.
|
||||||
DELETE FROM organization_members
|
DELETE FROM organization_members
|
||||||
WHERE user_id = OLD.id;
|
WHERE user_id = OLD.id;
|
||||||
END IF;
|
|
||||||
RETURN NEW;
|
-- Remove their user_skills.
|
||||||
|
-- user_skills.user_id has ON DELETE CASCADE, but soft-delete
|
||||||
|
-- does not remove the users row so the FK cascade never fires.
|
||||||
|
DELETE FROM user_skills
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
@@ -818,6 +830,32 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION enforce_user_skills_per_user_limit() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
skill_count int;
|
||||||
|
skill_limit constant int := 100;
|
||||||
|
BEGIN
|
||||||
|
-- Serialize skill-cap checks per user so concurrent inserts cannot all
|
||||||
|
-- observe the same pre-insert count and exceed the hard limit.
|
||||||
|
PERFORM 1
|
||||||
|
FROM users
|
||||||
|
WHERE id = NEW.user_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
SELECT count(*) INTO skill_count
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = NEW.user_id;
|
||||||
|
IF skill_count >= skill_limit THEN
|
||||||
|
RAISE EXCEPTION 'user has reached the personal skill limit'
|
||||||
|
USING ERRCODE = 'check_violation',
|
||||||
|
CONSTRAINT = 'user_skills_per_user_limit';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE FUNCTION inhibit_enqueue_if_disabled() RETURNS trigger
|
CREATE FUNCTION inhibit_enqueue_if_disabled() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
@@ -935,6 +973,25 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION insert_user_skill_fail_if_user_deleted() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
PERFORM 1
|
||||||
|
FROM users
|
||||||
|
WHERE id = NEW.user_id
|
||||||
|
AND deleted = true
|
||||||
|
LIMIT 1;
|
||||||
|
IF FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Cannot create user_skill for deleted user'
|
||||||
|
USING ERRCODE = 'check_violation',
|
||||||
|
CONSTRAINT = 'user_skill_user_deleted';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE FUNCTION nullify_next_start_at_on_workspace_autostart_modification() RETURNS trigger
|
CREATE FUNCTION nullify_next_start_at_on_workspace_autostart_modification() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
@@ -3015,6 +3072,20 @@ CREATE TABLE user_secrets (
|
|||||||
value_key_id text
|
value_key_id text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE user_skills (
|
||||||
|
id uuid NOT NULL,
|
||||||
|
user_id uuid NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text DEFAULT ''::text NOT NULL,
|
||||||
|
content text NOT NULL,
|
||||||
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT user_skills_content_size CHECK ((octet_length(content) <= 65536)),
|
||||||
|
CONSTRAINT user_skills_description_size CHECK ((octet_length(description) <= 4096)),
|
||||||
|
CONSTRAINT user_skills_name_format CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text)),
|
||||||
|
CONSTRAINT user_skills_name_size CHECK ((octet_length(name) <= 256))
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE user_status_changes (
|
CREATE TABLE user_status_changes (
|
||||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||||
user_id uuid NOT NULL,
|
user_id uuid NOT NULL,
|
||||||
@@ -3806,6 +3877,9 @@ ALTER TABLE ONLY user_links
|
|||||||
ALTER TABLE ONLY user_secrets
|
ALTER TABLE ONLY user_secrets
|
||||||
ADD CONSTRAINT user_secrets_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT user_secrets_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY user_skills
|
||||||
|
ADD CONSTRAINT user_skills_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
ALTER TABLE ONLY user_status_changes
|
ALTER TABLE ONLY user_status_changes
|
||||||
ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
@@ -4142,6 +4216,8 @@ CREATE UNIQUE INDEX user_secrets_user_file_path_idx ON user_secrets USING btree
|
|||||||
|
|
||||||
CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
|
CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX user_skills_user_id_name_idx ON user_skills USING btree (user_id, name);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
|
CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
|
||||||
|
|
||||||
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
|
CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
|
||||||
@@ -4266,6 +4342,10 @@ CREATE TRIGGER trigger_upsert_user_links BEFORE INSERT OR UPDATE ON user_links F
|
|||||||
|
|
||||||
CREATE TRIGGER trigger_upsert_user_secrets BEFORE INSERT OR UPDATE ON user_secrets FOR EACH ROW EXECUTE FUNCTION insert_user_secret_fail_if_user_deleted();
|
CREATE TRIGGER trigger_upsert_user_secrets BEFORE INSERT OR UPDATE ON user_secrets FOR EACH ROW EXECUTE FUNCTION insert_user_secret_fail_if_user_deleted();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_upsert_user_skills BEFORE INSERT OR UPDATE ON user_skills FOR EACH ROW EXECUTE FUNCTION insert_user_skill_fail_if_user_deleted();
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_user_skills_per_user_limit BEFORE INSERT ON user_skills FOR EACH ROW EXECUTE FUNCTION enforce_user_skills_per_user_limit();
|
||||||
|
|
||||||
CREATE TRIGGER update_notification_message_dedupe_hash BEFORE INSERT OR UPDATE ON notification_messages FOR EACH ROW EXECUTE FUNCTION compute_notification_message_dedupe_hash();
|
CREATE TRIGGER update_notification_message_dedupe_hash BEFORE INSERT OR UPDATE ON notification_messages FOR EACH ROW EXECUTE FUNCTION compute_notification_message_dedupe_hash();
|
||||||
|
|
||||||
CREATE TRIGGER user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION record_user_status_change();
|
CREATE TRIGGER user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION record_user_status_change();
|
||||||
@@ -4591,6 +4671,9 @@ ALTER TABLE ONLY user_secrets
|
|||||||
ALTER TABLE ONLY user_secrets
|
ALTER TABLE ONLY user_secrets
|
||||||
ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY user_skills
|
||||||
|
ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE ONLY user_status_changes
|
ALTER TABLE ONLY user_status_changes
|
||||||
ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ const (
|
|||||||
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyUserLinksUserID ForeignKeyConstraint = "user_links_user_id_fkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyUserSecretsUserID ForeignKeyConstraint = "user_secrets_user_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
ForeignKeyUserSecretsValueKeyID ForeignKeyConstraint = "user_secrets_value_key_id_fkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_value_key_id_fkey FOREIGN KEY (value_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||||
|
ForeignKeyUserSkillsUserID ForeignKeyConstraint = "user_skills_user_id_fkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
ForeignKeyUserStatusChangesUserID ForeignKeyConstraint = "user_status_changes_user_id_fkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||||
ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
ForeignKeyWebpushSubscriptionsUserID ForeignKeyConstraint = "webpush_subscriptions_user_id_fkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||||
ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
ForeignKeyWorkspaceAgentDevcontainersSubagentID ForeignKeyConstraint = "workspace_agent_devcontainers_subagent_id_fkey" // ALTER TABLE ONLY workspace_agent_devcontainers ADD CONSTRAINT workspace_agent_devcontainers_subagent_id_fkey FOREIGN KEY (subagent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
-- Enum additions to resource_type and api_key_scope are intentionally not
|
||||||
|
-- reverted because Postgres cannot drop enum values safely.
|
||||||
|
DROP TRIGGER IF EXISTS trigger_upsert_user_skills ON user_skills;
|
||||||
|
DROP FUNCTION IF EXISTS insert_user_skill_fail_if_user_deleted;
|
||||||
|
|
||||||
|
-- Restore the previous body of delete_deleted_user_resources() from
|
||||||
|
-- migration 000492 (without the user_skills cleanup).
|
||||||
|
CREATE OR REPLACE FUNCTION delete_deleted_user_resources() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
BEGIN
|
||||||
|
IF (NEW.deleted) THEN
|
||||||
|
-- Remove their api_keys.
|
||||||
|
DELETE FROM api_keys
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their user_links.
|
||||||
|
-- Their login_type is preserved in the users table.
|
||||||
|
-- Matching this user back to the link can still be done by their
|
||||||
|
-- email if the account is undeleted. Although that is not a guarantee.
|
||||||
|
DELETE FROM user_links
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their user_secrets.
|
||||||
|
-- user_secrets.user_id has ON DELETE CASCADE, but soft-delete
|
||||||
|
-- does not remove the users row so the FK cascade never fires.
|
||||||
|
DELETE FROM user_secrets
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their organization memberships.
|
||||||
|
-- This also triggers group membership cleanup via
|
||||||
|
-- trigger_delete_group_members_on_org_member_delete.
|
||||||
|
DELETE FROM organization_members
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trigger_user_skills_per_user_limit ON user_skills;
|
||||||
|
DROP FUNCTION IF EXISTS enforce_user_skills_per_user_limit();
|
||||||
|
DROP TABLE user_skills;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
-- Creates the user_skills table and indexes.
|
||||||
|
CREATE TABLE user_skills (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
description text NOT NULL DEFAULT '',
|
||||||
|
content text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT user_skills_name_size CHECK (octet_length(name) <= 256),
|
||||||
|
CONSTRAINT user_skills_name_format CHECK (name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
|
||||||
|
CONSTRAINT user_skills_description_size CHECK (octet_length(description) <= 4096),
|
||||||
|
CONSTRAINT user_skills_content_size CHECK (octet_length(content) <= 65536)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX user_skills_user_id_name_idx ON user_skills (user_id, name);
|
||||||
|
|
||||||
|
-- Enforces the per-user personal-skill cap at the schema level so the
|
||||||
|
-- invariant survives any future refactor of InsertUserSkill. The cap
|
||||||
|
-- value must stay in sync with skills.MaxPersonalSkillsPerUser in Go.
|
||||||
|
CREATE FUNCTION enforce_user_skills_per_user_limit() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
skill_count int;
|
||||||
|
skill_limit constant int := 100;
|
||||||
|
BEGIN
|
||||||
|
-- Serialize skill-cap checks per user so concurrent inserts cannot all
|
||||||
|
-- observe the same pre-insert count and exceed the hard limit.
|
||||||
|
PERFORM 1
|
||||||
|
FROM users
|
||||||
|
WHERE id = NEW.user_id
|
||||||
|
FOR UPDATE;
|
||||||
|
|
||||||
|
SELECT count(*) INTO skill_count
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = NEW.user_id;
|
||||||
|
IF skill_count >= skill_limit THEN
|
||||||
|
RAISE EXCEPTION 'user has reached the personal skill limit'
|
||||||
|
USING ERRCODE = 'check_violation',
|
||||||
|
CONSTRAINT = 'user_skills_per_user_limit';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_user_skills_per_user_limit
|
||||||
|
BEFORE INSERT ON user_skills
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE enforce_user_skills_per_user_limit();
|
||||||
|
|
||||||
|
-- Extend the soft-delete cleanup trigger to also wipe user_skills.
|
||||||
|
-- user_skills.user_id has ON DELETE CASCADE, but Coder soft-deletes
|
||||||
|
-- users by flipping users.deleted instead of removing the row, so the
|
||||||
|
-- FK cascade never fires and skills would otherwise survive deletion.
|
||||||
|
DELETE FROM
|
||||||
|
user_skills
|
||||||
|
WHERE
|
||||||
|
user_id
|
||||||
|
IN (
|
||||||
|
SELECT id FROM users WHERE deleted
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION delete_deleted_user_resources() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
BEGIN
|
||||||
|
IF (NEW.deleted) THEN
|
||||||
|
-- Remove their api_keys.
|
||||||
|
DELETE FROM api_keys
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their user_links.
|
||||||
|
-- Their login_type is preserved in the users table.
|
||||||
|
-- Matching this user back to the link can still be done by their
|
||||||
|
-- email if the account is undeleted. Although that is not a guarantee.
|
||||||
|
DELETE FROM user_links
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their user_secrets.
|
||||||
|
-- user_secrets.user_id has ON DELETE CASCADE, but soft-delete
|
||||||
|
-- does not remove the users row so the FK cascade never fires.
|
||||||
|
DELETE FROM user_secrets
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their organization memberships.
|
||||||
|
-- This also triggers group membership cleanup via
|
||||||
|
-- trigger_delete_group_members_on_org_member_delete.
|
||||||
|
DELETE FROM organization_members
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
|
||||||
|
-- Remove their user_skills.
|
||||||
|
-- user_skills.user_id has ON DELETE CASCADE, but soft-delete
|
||||||
|
-- does not remove the users row so the FK cascade never fires.
|
||||||
|
DELETE FROM user_skills
|
||||||
|
WHERE user_id = OLD.id;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Prevent adding new user_skills for soft-deleted users.
|
||||||
|
-- Closes the window between an in-flight CreateUserSkill request and
|
||||||
|
-- the soft-delete UPDATE committing.
|
||||||
|
CREATE FUNCTION insert_user_skill_fail_if_user_deleted() RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
PERFORM 1
|
||||||
|
FROM users
|
||||||
|
WHERE id = NEW.user_id
|
||||||
|
AND deleted = true
|
||||||
|
LIMIT 1;
|
||||||
|
IF FOUND THEN
|
||||||
|
RAISE EXCEPTION 'Cannot create user_skill for deleted user'
|
||||||
|
USING ERRCODE = 'check_violation',
|
||||||
|
CONSTRAINT = 'user_skill_user_deleted';
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER trigger_upsert_user_skills
|
||||||
|
BEFORE INSERT OR UPDATE ON user_skills
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE insert_user_skill_fail_if_user_deleted();
|
||||||
|
|
||||||
|
-- Adds the user skill audit resource type.
|
||||||
|
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'user_skill';
|
||||||
|
|
||||||
|
-- Adds API key scopes for managing user skills.
|
||||||
|
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_skill:create';
|
||||||
|
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_skill:read';
|
||||||
|
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_skill:update';
|
||||||
|
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_skill:delete';
|
||||||
|
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'user_skill:*';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- Inserts a user skill fixture so migration coverage includes the table.
|
||||||
|
INSERT INTO user_skills (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
content,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
'7f070eb2-991e-4f7f-b780-40c4e0f49001',
|
||||||
|
'30095c71-380b-457a-8995-97b8ee6e5307',
|
||||||
|
'example-skill',
|
||||||
|
'Example skill fixture.',
|
||||||
|
'Example content.',
|
||||||
|
'2026-05-07 00:00:00+00',
|
||||||
|
'2026-05-07 00:00:00+00'
|
||||||
|
);
|
||||||
@@ -896,6 +896,10 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce(
|
|||||||
return m.DebouncedUntil, false
|
return m.DebouncedUntil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s UserSkill) RBACObject() rbac.Object {
|
||||||
|
return rbac.ResourceUserSkill.WithID(s.ID).WithOwner(s.UserID.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (s UserSecret) RBACObject() rbac.Object {
|
func (s UserSecret) RBACObject() rbac.Object {
|
||||||
return rbac.ResourceUserSecret.WithID(s.ID).WithOwner(s.UserID.String())
|
return rbac.ResourceUserSecret.WithID(s.ID).WithOwner(s.UserID.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,11 @@ const (
|
|||||||
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
ApiKeyScopeAiProviderRead APIKeyScope = "ai_provider:read"
|
||||||
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
ApiKeyScopeAiProviderUpdate APIKeyScope = "ai_provider:update"
|
||||||
ApiKeyScopeChatShare APIKeyScope = "chat:share"
|
ApiKeyScopeChatShare APIKeyScope = "chat:share"
|
||||||
|
ApiKeyScopeUserSkillCreate APIKeyScope = "user_skill:create"
|
||||||
|
ApiKeyScopeUserSkillRead APIKeyScope = "user_skill:read"
|
||||||
|
ApiKeyScopeUserSkillUpdate APIKeyScope = "user_skill:update"
|
||||||
|
ApiKeyScopeUserSkillDelete APIKeyScope = "user_skill:delete"
|
||||||
|
ApiKeyScopeUserSkill APIKeyScope = "user_skill:*"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||||
@@ -567,7 +572,12 @@ func (e APIKeyScope) Valid() bool {
|
|||||||
ApiKeyScopeAiProviderDelete,
|
ApiKeyScopeAiProviderDelete,
|
||||||
ApiKeyScopeAiProviderRead,
|
ApiKeyScopeAiProviderRead,
|
||||||
ApiKeyScopeAiProviderUpdate,
|
ApiKeyScopeAiProviderUpdate,
|
||||||
ApiKeyScopeChatShare:
|
ApiKeyScopeChatShare,
|
||||||
|
ApiKeyScopeUserSkillCreate,
|
||||||
|
ApiKeyScopeUserSkillRead,
|
||||||
|
ApiKeyScopeUserSkillUpdate,
|
||||||
|
ApiKeyScopeUserSkillDelete,
|
||||||
|
ApiKeyScopeUserSkill:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -792,6 +802,11 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
|||||||
ApiKeyScopeAiProviderRead,
|
ApiKeyScopeAiProviderRead,
|
||||||
ApiKeyScopeAiProviderUpdate,
|
ApiKeyScopeAiProviderUpdate,
|
||||||
ApiKeyScopeChatShare,
|
ApiKeyScopeChatShare,
|
||||||
|
ApiKeyScopeUserSkillCreate,
|
||||||
|
ApiKeyScopeUserSkillRead,
|
||||||
|
ApiKeyScopeUserSkillUpdate,
|
||||||
|
ApiKeyScopeUserSkillDelete,
|
||||||
|
ApiKeyScopeUserSkill,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3322,6 +3337,7 @@ const (
|
|||||||
ResourceTypeAiProvider ResourceType = "ai_provider"
|
ResourceTypeAiProvider ResourceType = "ai_provider"
|
||||||
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
|
ResourceTypeAiProviderKey ResourceType = "ai_provider_key"
|
||||||
ResourceTypeGroupAiBudget ResourceType = "group_ai_budget"
|
ResourceTypeGroupAiBudget ResourceType = "group_ai_budget"
|
||||||
|
ResourceTypeUserSkill ResourceType = "user_skill"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *ResourceType) Scan(src interface{}) error {
|
func (e *ResourceType) Scan(src interface{}) error {
|
||||||
@@ -3392,7 +3408,8 @@ func (e ResourceType) Valid() bool {
|
|||||||
ResourceTypeUserSecret,
|
ResourceTypeUserSecret,
|
||||||
ResourceTypeAiProvider,
|
ResourceTypeAiProvider,
|
||||||
ResourceTypeAiProviderKey,
|
ResourceTypeAiProviderKey,
|
||||||
ResourceTypeGroupAiBudget:
|
ResourceTypeGroupAiBudget,
|
||||||
|
ResourceTypeUserSkill:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -3432,6 +3449,7 @@ func AllResourceTypeValues() []ResourceType {
|
|||||||
ResourceTypeAiProvider,
|
ResourceTypeAiProvider,
|
||||||
ResourceTypeAiProviderKey,
|
ResourceTypeAiProviderKey,
|
||||||
ResourceTypeGroupAiBudget,
|
ResourceTypeGroupAiBudget,
|
||||||
|
ResourceTypeUserSkill,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5725,6 +5743,16 @@ type UserSecret struct {
|
|||||||
ValueKeyID sql.NullString `db:"value_key_id" json:"value_key_id"`
|
ValueKeyID sql.NullString `db:"value_key_id" json:"value_key_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserSkill struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Description string `db:"description" json:"description"`
|
||||||
|
Content string `db:"content" json:"content"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
// Tracks the history of user status changes
|
// Tracks the history of user status changes
|
||||||
type UserStatusChange struct {
|
type UserStatusChange struct {
|
||||||
ID uuid.UUID `db:"id" json:"id"`
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ type sqlcQuerier interface {
|
|||||||
DeleteUserChatCompactionThreshold(ctx context.Context, arg DeleteUserChatCompactionThresholdParams) error
|
DeleteUserChatCompactionThreshold(ctx context.Context, arg DeleteUserChatCompactionThresholdParams) error
|
||||||
DeleteUserChatProviderKey(ctx context.Context, arg DeleteUserChatProviderKeyParams) error
|
DeleteUserChatProviderKey(ctx context.Context, arg DeleteUserChatProviderKeyParams) error
|
||||||
DeleteUserSecretByUserIDAndName(ctx context.Context, arg DeleteUserSecretByUserIDAndNameParams) (UserSecret, error)
|
DeleteUserSecretByUserIDAndName(ctx context.Context, arg DeleteUserSecretByUserIDAndNameParams) (UserSecret, error)
|
||||||
|
DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||||
DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error
|
DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error
|
||||||
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
|
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
|
||||||
DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error
|
DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error
|
||||||
@@ -791,6 +792,7 @@ type sqlcQuerier interface {
|
|||||||
// values rather than interpolating between rows.
|
// values rather than interpolating between rows.
|
||||||
GetUserSecretsTelemetrySummary(ctx context.Context) (GetUserSecretsTelemetrySummaryRow, error)
|
GetUserSecretsTelemetrySummary(ctx context.Context) (GetUserSecretsTelemetrySummaryRow, error)
|
||||||
GetUserShellToolDisplayMode(ctx context.Context, userID uuid.UUID) (string, error)
|
GetUserShellToolDisplayMode(ctx context.Context, userID uuid.UUID) (string, error)
|
||||||
|
GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||||
// GetUserStatusCounts returns the count of users in each status over time.
|
// GetUserStatusCounts returns the count of users in each status over time.
|
||||||
// The time range is inclusively defined by the start_time and end_time parameters.
|
// The time range is inclusively defined by the start_time and end_time parameters.
|
||||||
GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error)
|
GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error)
|
||||||
@@ -966,6 +968,7 @@ type sqlcQuerier interface {
|
|||||||
// If there is a conflict, the user is already a member
|
// If there is a conflict, the user is already a member
|
||||||
InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error)
|
InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error)
|
||||||
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
|
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
|
||||||
|
InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (UserSkill, error)
|
||||||
InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error)
|
InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error)
|
||||||
// Inserts or updates a webpush subscription. The (user_id, endpoint) pair
|
// Inserts or updates a webpush subscription. The (user_id, endpoint) pair
|
||||||
// is unique; re-subscribing the same endpoint replaces the keys instead of
|
// is unique; re-subscribing the same endpoint replaces the keys instead of
|
||||||
@@ -1033,6 +1036,7 @@ type sqlcQuerier interface {
|
|||||||
// provisioner (build-time injection) and the agent manifest
|
// provisioner (build-time injection) and the agent manifest
|
||||||
// (runtime injection).
|
// (runtime injection).
|
||||||
ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error)
|
ListUserSecretsWithValues(ctx context.Context, userID uuid.UUID) ([]UserSecret, error)
|
||||||
|
ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error)
|
||||||
ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
|
ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgentPortShare, error)
|
||||||
MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error
|
MarkAllInboxNotificationsAsRead(ctx context.Context, arg MarkAllInboxNotificationsAsReadParams) error
|
||||||
OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error)
|
OIDCClaimFieldValues(ctx context.Context, arg OIDCClaimFieldValuesParams) ([]string, error)
|
||||||
@@ -1271,6 +1275,7 @@ type sqlcQuerier interface {
|
|||||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||||
UpdateUserSecretByUserIDAndName(ctx context.Context, arg UpdateUserSecretByUserIDAndNameParams) (UserSecret, error)
|
UpdateUserSecretByUserIDAndName(ctx context.Context, arg UpdateUserSecretByUserIDAndNameParams) (UserSecret, error)
|
||||||
UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error)
|
UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error)
|
||||||
|
UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||||
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
||||||
UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error)
|
UpdateUserTaskNotificationAlertDismissed(ctx context.Context, arg UpdateUserTaskNotificationAlertDismissedParams) (bool, error)
|
||||||
UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error)
|
UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error)
|
||||||
|
|||||||
@@ -26894,6 +26894,177 @@ func (q *sqlQuerier) UpdateUserSecretByUserIDAndName(ctx context.Context, arg Up
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteUserSkillByUserIDAndName = `-- name: DeleteUserSkillByUserIDAndName :one
|
||||||
|
DELETE FROM user_skills
|
||||||
|
WHERE user_id = $1 AND name = $2
|
||||||
|
RETURNING id, user_id, name, description, content, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type DeleteUserSkillByUserIDAndNameParams struct {
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (UserSkill, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, deleteUserSkillByUserIDAndName, arg.UserID, arg.Name)
|
||||||
|
var i UserSkill
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserSkillByUserIDAndName = `-- name: GetUserSkillByUserIDAndName :one
|
||||||
|
SELECT id, user_id, name, description, content, created_at, updated_at
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = $1 AND name = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserSkillByUserIDAndNameParams struct {
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (UserSkill, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getUserSkillByUserIDAndName, arg.UserID, arg.Name)
|
||||||
|
var i UserSkill
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertUserSkill = `-- name: InsertUserSkill :one
|
||||||
|
INSERT INTO user_skills (id, user_id, name, description, content)
|
||||||
|
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::text)
|
||||||
|
RETURNING id, user_id, name, description, content, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserSkillParams struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Description string `db:"description" json:"description"`
|
||||||
|
Content string `db:"content" json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (UserSkill, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, insertUserSkill,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Content,
|
||||||
|
)
|
||||||
|
var i UserSkill
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listUserSkillMetadataByUserID = `-- name: ListUserSkillMetadataByUserID :many
|
||||||
|
SELECT
|
||||||
|
id, user_id, name, description, created_at, updated_at
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY name ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListUserSkillMetadataByUserIDRow struct {
|
||||||
|
ID uuid.UUID `db:"id" json:"id"`
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
Description string `db:"description" json:"description"`
|
||||||
|
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) ListUserSkillMetadataByUserID(ctx context.Context, userID uuid.UUID) ([]ListUserSkillMetadataByUserIDRow, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listUserSkillMetadataByUserID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListUserSkillMetadataByUserIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListUserSkillMetadataByUserIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserSkillByUserIDAndName = `-- name: UpdateUserSkillByUserIDAndName :one
|
||||||
|
UPDATE user_skills
|
||||||
|
SET
|
||||||
|
description = $1,
|
||||||
|
content = $2,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE user_id = $3 AND name = $4
|
||||||
|
RETURNING id, user_id, name, description, content, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateUserSkillByUserIDAndNameParams struct {
|
||||||
|
Description string `db:"description" json:"description"`
|
||||||
|
Content string `db:"content" json:"content"`
|
||||||
|
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||||
|
Name string `db:"name" json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sqlQuerier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (UserSkill, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateUserSkillByUserIDAndName,
|
||||||
|
arg.Description,
|
||||||
|
arg.Content,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Name,
|
||||||
|
)
|
||||||
|
var i UserSkill
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Content,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteUserChatProviderKey = `-- name: DeleteUserChatProviderKey :exec
|
const deleteUserChatProviderKey = `-- name: DeleteUserChatProviderKey :exec
|
||||||
DELETE FROM user_chat_provider_keys WHERE user_id = $1 AND chat_provider_id = $2
|
DELETE FROM user_chat_provider_keys WHERE user_id = $1 AND chat_provider_id = $2
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- name: InsertUserSkill :one
|
||||||
|
INSERT INTO user_skills (id, user_id, name, description, content)
|
||||||
|
VALUES (@id::uuid, @user_id::uuid, @name::text, @description::text, @content::text)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetUserSkillByUserIDAndName :one
|
||||||
|
SELECT *
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = @user_id AND name = @name;
|
||||||
|
|
||||||
|
-- name: ListUserSkillMetadataByUserID :many
|
||||||
|
SELECT
|
||||||
|
id, user_id, name, description, created_at, updated_at
|
||||||
|
FROM user_skills
|
||||||
|
WHERE user_id = @user_id
|
||||||
|
ORDER BY name ASC;
|
||||||
|
|
||||||
|
-- name: UpdateUserSkillByUserIDAndName :one
|
||||||
|
UPDATE user_skills
|
||||||
|
SET
|
||||||
|
description = @description,
|
||||||
|
content = @content,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE user_id = @user_id AND name = @name
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteUserSkillByUserIDAndName :one
|
||||||
|
DELETE FROM user_skills
|
||||||
|
WHERE user_id = @user_id AND name = @name
|
||||||
|
RETURNING *;
|
||||||
@@ -103,6 +103,7 @@ const (
|
|||||||
UniqueUserDeletedPkey UniqueConstraint = "user_deleted_pkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_pkey PRIMARY KEY (id);
|
UniqueUserDeletedPkey UniqueConstraint = "user_deleted_pkey" // ALTER TABLE ONLY user_deleted ADD CONSTRAINT user_deleted_pkey PRIMARY KEY (id);
|
||||||
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
|
UniqueUserLinksPkey UniqueConstraint = "user_links_pkey" // ALTER TABLE ONLY user_links ADD CONSTRAINT user_links_pkey PRIMARY KEY (user_id, login_type);
|
||||||
UniqueUserSecretsPkey UniqueConstraint = "user_secrets_pkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_pkey PRIMARY KEY (id);
|
UniqueUserSecretsPkey UniqueConstraint = "user_secrets_pkey" // ALTER TABLE ONLY user_secrets ADD CONSTRAINT user_secrets_pkey PRIMARY KEY (id);
|
||||||
|
UniqueUserSkillsPkey UniqueConstraint = "user_skills_pkey" // ALTER TABLE ONLY user_skills ADD CONSTRAINT user_skills_pkey PRIMARY KEY (id);
|
||||||
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
UniqueUserStatusChangesPkey UniqueConstraint = "user_status_changes_pkey" // ALTER TABLE ONLY user_status_changes ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
||||||
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
UniqueUsersPkey UniqueConstraint = "users_pkey" // ALTER TABLE ONLY users ADD CONSTRAINT users_pkey PRIMARY KEY (id);
|
||||||
UniqueWebpushSubscriptionsPkey UniqueConstraint = "webpush_subscriptions_pkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
|
UniqueWebpushSubscriptionsPkey UniqueConstraint = "webpush_subscriptions_pkey" // ALTER TABLE ONLY webpush_subscriptions ADD CONSTRAINT webpush_subscriptions_pkey PRIMARY KEY (id);
|
||||||
@@ -156,6 +157,7 @@ const (
|
|||||||
UniqueUserSecretsUserEnvNameIndex UniqueConstraint = "user_secrets_user_env_name_idx" // CREATE UNIQUE INDEX user_secrets_user_env_name_idx ON user_secrets USING btree (user_id, env_name) WHERE (env_name <> ''::text);
|
UniqueUserSecretsUserEnvNameIndex UniqueConstraint = "user_secrets_user_env_name_idx" // CREATE UNIQUE INDEX user_secrets_user_env_name_idx ON user_secrets USING btree (user_id, env_name) WHERE (env_name <> ''::text);
|
||||||
UniqueUserSecretsUserFilePathIndex UniqueConstraint = "user_secrets_user_file_path_idx" // CREATE UNIQUE INDEX user_secrets_user_file_path_idx ON user_secrets USING btree (user_id, file_path) WHERE (file_path <> ''::text);
|
UniqueUserSecretsUserFilePathIndex UniqueConstraint = "user_secrets_user_file_path_idx" // CREATE UNIQUE INDEX user_secrets_user_file_path_idx ON user_secrets USING btree (user_id, file_path) WHERE (file_path <> ''::text);
|
||||||
UniqueUserSecretsUserNameIndex UniqueConstraint = "user_secrets_user_name_idx" // CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
|
UniqueUserSecretsUserNameIndex UniqueConstraint = "user_secrets_user_name_idx" // CREATE UNIQUE INDEX user_secrets_user_name_idx ON user_secrets USING btree (user_id, name);
|
||||||
|
UniqueUserSkillsUserIDNameIndex UniqueConstraint = "user_skills_user_id_name_idx" // CREATE UNIQUE INDEX user_skills_user_id_name_idx ON user_skills USING btree (user_id, name);
|
||||||
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
|
UniqueUsersEmailLowerIndex UniqueConstraint = "users_email_lower_idx" // CREATE UNIQUE INDEX users_email_lower_idx ON users USING btree (lower(email)) WHERE ((deleted = false) AND (email <> ''::text));
|
||||||
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
|
UniqueUsersUsernameLowerIndex UniqueConstraint = "users_username_lower_idx" // CREATE UNIQUE INDEX users_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
|
||||||
UniqueWebpushSubscriptionsUserIDEndpointIndex UniqueConstraint = "webpush_subscriptions_user_id_endpoint_idx" // CREATE UNIQUE INDEX webpush_subscriptions_user_id_endpoint_idx ON webpush_subscriptions USING btree (user_id, endpoint);
|
UniqueWebpushSubscriptionsUserIDEndpointIndex UniqueConstraint = "webpush_subscriptions_user_id_endpoint_idx" // CREATE UNIQUE INDEX webpush_subscriptions_user_id_endpoint_idx ON webpush_subscriptions USING btree (user_id, endpoint);
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package database_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||||
|
"github.com/coder/coder/v2/coderd/x/skills"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserSkillSchemaConstants(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if testing.Short() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
_, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
||||||
|
var triggerDef string
|
||||||
|
err := sqlDB.QueryRowContext(ctx,
|
||||||
|
`SELECT pg_get_functiondef('enforce_user_skills_per_user_limit'::regproc)`,
|
||||||
|
).Scan(&triggerDef)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, triggerDef, fmt.Sprintf(
|
||||||
|
"skill_limit constant int := %d",
|
||||||
|
skills.MaxPersonalSkillsPerUser,
|
||||||
|
))
|
||||||
|
|
||||||
|
constraints := map[database.CheckConstraint]string{
|
||||||
|
database.CheckUserSkillsNameSize: fmt.Sprintf(
|
||||||
|
"octet_length(name) <= %d",
|
||||||
|
skills.MaxPersonalSkillNameBytes,
|
||||||
|
),
|
||||||
|
database.CheckUserSkillsNameFormat: "name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text",
|
||||||
|
database.CheckUserSkillsDescriptionSize: fmt.Sprintf(
|
||||||
|
"octet_length(description) <= %d",
|
||||||
|
skills.MaxPersonalSkillDescriptionBytes,
|
||||||
|
),
|
||||||
|
database.CheckUserSkillsContentSize: fmt.Sprintf(
|
||||||
|
"octet_length(content) <= %d",
|
||||||
|
skills.MaxPersonalSkillSizeBytes,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for constraint, expected := range constraints {
|
||||||
|
t.Run(string(constraint), func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
var constraintDef string
|
||||||
|
err := sqlDB.QueryRowContext(ctx,
|
||||||
|
`SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname = $1`,
|
||||||
|
constraint,
|
||||||
|
).Scan(&constraintDef)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, constraintDef, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -385,6 +385,16 @@ var (
|
|||||||
Type: "user_secret",
|
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
|
// ResourceWebpushSubscription
|
||||||
// Valid Actions
|
// Valid Actions
|
||||||
// - "ActionCreate" :: create webpush subscriptions
|
// - "ActionCreate" :: create webpush subscriptions
|
||||||
@@ -500,6 +510,7 @@ func AllResources() []Objecter {
|
|||||||
ResourceUsageEvent,
|
ResourceUsageEvent,
|
||||||
ResourceUser,
|
ResourceUser,
|
||||||
ResourceUserSecret,
|
ResourceUserSecret,
|
||||||
|
ResourceUserSkill,
|
||||||
ResourceWebpushSubscription,
|
ResourceWebpushSubscription,
|
||||||
ResourceWorkspace,
|
ResourceWorkspace,
|
||||||
ResourceWorkspaceAgentDevcontainers,
|
ResourceWorkspaceAgentDevcontainers,
|
||||||
|
|||||||
@@ -379,6 +379,14 @@ var RBACPermissions = map[string]PermissionDefinition{
|
|||||||
ActionDelete: "delete a user secret",
|
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": {
|
"usage_event": {
|
||||||
Actions: map[Action]ActionDefinition{
|
Actions: map[Action]ActionDefinition{
|
||||||
ActionCreate: "create a usage event",
|
ActionCreate: "create a usage event",
|
||||||
|
|||||||
@@ -301,12 +301,14 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||||||
Site: append(
|
Site: append(
|
||||||
// Workspace dormancy and workspace are omitted.
|
// Workspace dormancy and workspace are omitted.
|
||||||
// Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec.
|
// Workspace is specifically handled based on the opts.NoOwnerWorkspaceExec.
|
||||||
// Owners cannot access other users' secrets.
|
// Owners can inspect and delete personal skills for operability and
|
||||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUserSecret, ResourceUsageEvent, ResourceBoundaryUsage, ResourceAiSeat),
|
// 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.
|
// This adds back in the Workspace permissions.
|
||||||
Permissions(map[string][]policy.Action{
|
Permissions(map[string][]policy.Action{
|
||||||
ResourceWorkspace.Type: ownerWorkspaceActions,
|
ResourceWorkspace.Type: ownerWorkspaceActions,
|
||||||
ResourceWorkspaceDormant.Type: {policy.ActionRead, policy.ActionDelete, policy.ActionCreate, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent},
|
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.
|
// PrebuiltWorkspaces are a subset of Workspaces.
|
||||||
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
// Explicitly setting PrebuiltWorkspace permissions for clarity.
|
||||||
// Note: even without PrebuiltWorkspace permissions, access is still granted via Workspace permissions.
|
// 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",
|
Name: "UsageEvents",
|
||||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ var externalLowLevel = map[ScopeName]struct{}{
|
|||||||
"user_secret:delete": {},
|
"user_secret:delete": {},
|
||||||
"user_secret:*": {},
|
"user_secret:*": {},
|
||||||
|
|
||||||
|
// User skills
|
||||||
|
"user_skill:read": {},
|
||||||
|
"user_skill:create": {},
|
||||||
|
"user_skill:update": {},
|
||||||
|
"user_skill:delete": {},
|
||||||
|
"user_skill:*": {},
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
"task:create": {},
|
"task:create": {},
|
||||||
"task:read": {},
|
"task:read": {},
|
||||||
|
|||||||
@@ -134,6 +134,10 @@ const (
|
|||||||
ScopeUserSecretDelete ScopeName = "user_secret:delete"
|
ScopeUserSecretDelete ScopeName = "user_secret:delete"
|
||||||
ScopeUserSecretRead ScopeName = "user_secret:read"
|
ScopeUserSecretRead ScopeName = "user_secret:read"
|
||||||
ScopeUserSecretUpdate ScopeName = "user_secret:update"
|
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"
|
ScopeWebpushSubscriptionCreate ScopeName = "webpush_subscription:create"
|
||||||
ScopeWebpushSubscriptionDelete ScopeName = "webpush_subscription:delete"
|
ScopeWebpushSubscriptionDelete ScopeName = "webpush_subscription:delete"
|
||||||
ScopeWebpushSubscriptionRead ScopeName = "webpush_subscription:read"
|
ScopeWebpushSubscriptionRead ScopeName = "webpush_subscription:read"
|
||||||
@@ -307,6 +311,10 @@ func (e ScopeName) Valid() bool {
|
|||||||
ScopeUserSecretDelete,
|
ScopeUserSecretDelete,
|
||||||
ScopeUserSecretRead,
|
ScopeUserSecretRead,
|
||||||
ScopeUserSecretUpdate,
|
ScopeUserSecretUpdate,
|
||||||
|
ScopeUserSkillCreate,
|
||||||
|
ScopeUserSkillDelete,
|
||||||
|
ScopeUserSkillRead,
|
||||||
|
ScopeUserSkillUpdate,
|
||||||
ScopeWebpushSubscriptionCreate,
|
ScopeWebpushSubscriptionCreate,
|
||||||
ScopeWebpushSubscriptionDelete,
|
ScopeWebpushSubscriptionDelete,
|
||||||
ScopeWebpushSubscriptionRead,
|
ScopeWebpushSubscriptionRead,
|
||||||
@@ -481,6 +489,10 @@ func AllScopeNameValues() []ScopeName {
|
|||||||
ScopeUserSecretDelete,
|
ScopeUserSecretDelete,
|
||||||
ScopeUserSecretRead,
|
ScopeUserSecretRead,
|
||||||
ScopeUserSecretUpdate,
|
ScopeUserSecretUpdate,
|
||||||
|
ScopeUserSkillCreate,
|
||||||
|
ScopeUserSkillDelete,
|
||||||
|
ScopeUserSkillRead,
|
||||||
|
ScopeUserSkillUpdate,
|
||||||
ScopeWebpushSubscriptionCreate,
|
ScopeWebpushSubscriptionCreate,
|
||||||
ScopeWebpushSubscriptionDelete,
|
ScopeWebpushSubscriptionDelete,
|
||||||
ScopeWebpushSubscriptionRead,
|
ScopeWebpushSubscriptionRead,
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
package coderd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/audit"
|
||||||
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
"github.com/coder/coder/v2/coderd/httpmw"
|
||||||
|
"github.com/coder/coder/v2/coderd/x/skills"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// personalSkillJSONEscapeExpansion is the maximum expansion for one byte in a JSON string.
|
||||||
|
personalSkillJSONEscapeExpansion = 6
|
||||||
|
// personalSkillRequestEnvelopeBytes leaves room for the surrounding JSON object.
|
||||||
|
personalSkillRequestEnvelopeBytes = 1024
|
||||||
|
// maxPersonalSkillRequestBytes allows worst-case JSON string escaping for
|
||||||
|
// otherwise valid raw skill content.
|
||||||
|
maxPersonalSkillRequestBytes = skills.MaxPersonalSkillSizeBytes*personalSkillJSONEscapeExpansion + personalSkillRequestEnvelopeBytes
|
||||||
|
|
||||||
|
// These names are raised by trigger functions with USING CONSTRAINT.
|
||||||
|
// They are not table CHECK constraints, so dbgen does not emit them in
|
||||||
|
// check_constraint.go.
|
||||||
|
userSkillsPerUserLimitConstraint database.CheckConstraint = "user_skills_per_user_limit"
|
||||||
|
userSkillUserDeletedConstraint database.CheckConstraint = "user_skill_user_deleted"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Summary Create a user skill
|
||||||
|
// @ID create-a-user-skill
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Users
|
||||||
|
// @Param user path string true "User ID, username, or me"
|
||||||
|
// @Param request body codersdk.CreateUserSkillRequest true "Create user skill request"
|
||||||
|
// @Success 201 {object} codersdk.UserSkill
|
||||||
|
// @Router /api/experimental/users/{user}/skills [post]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func (api *API) postUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
user = httpmw.UserParam(r)
|
||||||
|
auditor = api.Auditor.Load()
|
||||||
|
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||||
|
Audit: *auditor,
|
||||||
|
Log: api.Logger,
|
||||||
|
Request: r,
|
||||||
|
Action: database.AuditActionCreate,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
defer commitAudit()
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(rw, r.Body, maxPersonalSkillRequestBytes)
|
||||||
|
|
||||||
|
var req codersdk.CreateUserSkillRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSkill, err := skills.ParsePersonalSkillMarkdown([]byte(req.Content))
|
||||||
|
if err != nil {
|
||||||
|
writeInvalidUserSkillContent(ctx, rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := database.InsertUserSkillParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: parsedSkill.Name,
|
||||||
|
Description: parsedSkill.Description,
|
||||||
|
Content: req.Content,
|
||||||
|
}
|
||||||
|
skill, err := api.Database.InsertUserSkill(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
if httpapi.IsUnauthorizedError(err) {
|
||||||
|
httpapi.Forbidden(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if database.IsCheckViolation(err, userSkillUserDeletedConstraint) {
|
||||||
|
writeCannotCreateUserSkillForDeletedUser(ctx, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if database.IsCheckViolation(err, userSkillsPerUserLimitConstraint) {
|
||||||
|
writeUserSkillLimitReached(ctx, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if database.IsUniqueViolation(err, database.UniqueUserSkillsUserIDNameIndex) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||||
|
Message: "A skill with that name already exists.",
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aReq.New = skill
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.UserSkill(skill))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary List user skills
|
||||||
|
// @ID list-user-skills
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Users
|
||||||
|
// @Param user path string true "User ID, username, or me"
|
||||||
|
// @Success 200 {array} codersdk.UserSkillMetadata
|
||||||
|
// @Router /api/experimental/users/{user}/skills [get]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func (api *API) getUserSkills(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
|
||||||
|
ctx := r.Context()
|
||||||
|
user := httpmw.UserParam(r)
|
||||||
|
|
||||||
|
rows, err := api.Database.ListUserSkillMetadataByUserID(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.UserSkillMetadataList(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get a user skill by name
|
||||||
|
// @ID get-a-user-skill-by-name
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Users
|
||||||
|
// @Param user path string true "User ID, username, or me"
|
||||||
|
// @Param skillName path string true "Skill name"
|
||||||
|
// @Success 200 {object} codersdk.UserSkill
|
||||||
|
// @Router /api/experimental/users/{user}/skills/{skillName} [get]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func (api *API) getUserSkill(rw http.ResponseWriter, r *http.Request) { //nolint:revive // Method name matches route.
|
||||||
|
ctx := r.Context()
|
||||||
|
user := httpmw.UserParam(r)
|
||||||
|
name := chi.URLParam(r, "skillName")
|
||||||
|
|
||||||
|
skill, err := api.Database.GetUserSkillByUserIDAndName(ctx, database.GetUserSkillByUserIDAndNameParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.UserSkill(skill))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Update a user skill
|
||||||
|
// @ID update-a-user-skill
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Tags Users
|
||||||
|
// @Param user path string true "User ID, username, or me"
|
||||||
|
// @Param skillName path string true "Skill name"
|
||||||
|
// @Param request body codersdk.UpdateUserSkillRequest true "Update user skill request"
|
||||||
|
// @Success 200 {object} codersdk.UserSkill
|
||||||
|
// @Router /api/experimental/users/{user}/skills/{skillName} [patch]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func (api *API) patchUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
user = httpmw.UserParam(r)
|
||||||
|
name = chi.URLParam(r, "skillName")
|
||||||
|
auditor = api.Auditor.Load()
|
||||||
|
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||||
|
Audit: *auditor,
|
||||||
|
Log: api.Logger,
|
||||||
|
Request: r,
|
||||||
|
Action: database.AuditActionWrite,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
defer commitAudit()
|
||||||
|
|
||||||
|
r.Body = http.MaxBytesReader(rw, r.Body, maxPersonalSkillRequestBytes)
|
||||||
|
|
||||||
|
var req codersdk.UpdateUserSkillRequest
|
||||||
|
if !httpapi.Read(ctx, rw, r, &req) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedSkill, err := skills.ParsePersonalSkillMarkdown([]byte(req.Content))
|
||||||
|
if err != nil {
|
||||||
|
writeInvalidUserSkillContent(ctx, rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if parsedSkill.Name != name {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "Skill name in path does not match frontmatter name.",
|
||||||
|
Detail: fmt.Sprintf("path has %q, frontmatter has %q", name, parsedSkill.Name),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := database.UpdateUserSkillByUserIDAndNameParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: name,
|
||||||
|
Description: parsedSkill.Description,
|
||||||
|
Content: req.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
skill database.UserSkill
|
||||||
|
oldSkill database.UserSkill
|
||||||
|
)
|
||||||
|
err = api.Database.InTx(func(tx database.Store) error {
|
||||||
|
fetched, err := tx.GetUserSkillByUserIDAndName(ctx, database.GetUserSkillByUserIDAndNameParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("fetch user skill: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := tx.UpdateUserSkillByUserIDAndName(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("update user skill: %w", err)
|
||||||
|
}
|
||||||
|
oldSkill = fetched
|
||||||
|
skill = updated
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
if httpapi.IsUnauthorizedError(err) {
|
||||||
|
httpapi.Forbidden(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if database.IsCheckViolation(err, userSkillUserDeletedConstraint) {
|
||||||
|
writeCannotModifyUserSkillForDeletedUser(ctx, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign audit state after InTx returns so the audit log can never
|
||||||
|
// claim a rolled-back update was committed.
|
||||||
|
aReq.Old = oldSkill
|
||||||
|
aReq.New = skill
|
||||||
|
|
||||||
|
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.UserSkill(skill))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete a user skill
|
||||||
|
// @ID delete-a-user-skill
|
||||||
|
// @Security CoderSessionToken
|
||||||
|
// @Tags Users
|
||||||
|
// @Param user path string true "User ID, username, or me"
|
||||||
|
// @Param skillName path string true "Skill name"
|
||||||
|
// @Success 204
|
||||||
|
// @Router /api/experimental/users/{user}/skills/{skillName} [delete]
|
||||||
|
// @x-apidocgen {"skip": true}
|
||||||
|
func (api *API) deleteUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
user = httpmw.UserParam(r)
|
||||||
|
name = chi.URLParam(r, "skillName")
|
||||||
|
auditor = api.Auditor.Load()
|
||||||
|
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||||
|
Audit: *auditor,
|
||||||
|
Log: api.Logger,
|
||||||
|
Request: r,
|
||||||
|
Action: database.AuditActionDelete,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
defer commitAudit()
|
||||||
|
|
||||||
|
deleted, err := api.Database.DeleteUserSkillByUserIDAndName(ctx, database.DeleteUserSkillByUserIDAndNameParams{
|
||||||
|
UserID: user.ID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if httpapi.Is404Error(err) {
|
||||||
|
httpapi.ResourceNotFound(rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpapi.InternalServerError(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aReq.Old = deleted
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCannotCreateUserSkillForDeletedUser(ctx context.Context, rw http.ResponseWriter) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||||
|
Message: "Cannot create skills for deleted users.",
|
||||||
|
Detail: "This user has been deleted and cannot be modified.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCannotModifyUserSkillForDeletedUser(ctx context.Context, rw http.ResponseWriter) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||||
|
Message: "Cannot modify skills for deleted users.",
|
||||||
|
Detail: "This user has been deleted and cannot be modified.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUserSkillLimitReached(ctx context.Context, rw http.ResponseWriter) {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||||
|
Message: "Personal skill limit reached.",
|
||||||
|
Detail: fmt.Sprintf(
|
||||||
|
"Each user can have at most %d personal skills.",
|
||||||
|
skills.MaxPersonalSkillsPerUser,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeInvalidUserSkillContent(ctx context.Context, rw http.ResponseWriter, err error) {
|
||||||
|
message := "Invalid skill content."
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, skills.ErrInvalidSkillName):
|
||||||
|
message = "Invalid skill name."
|
||||||
|
case errors.Is(err, skills.ErrSkillBodyRequired):
|
||||||
|
message = "Skill body is required."
|
||||||
|
case errors.Is(err, skills.ErrSkillTooLarge):
|
||||||
|
message = "Skill content is too large."
|
||||||
|
case errors.Is(err, skills.ErrSkillDescriptionTooLarge):
|
||||||
|
message = "Skill description is too large."
|
||||||
|
}
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: message,
|
||||||
|
Detail: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package coderd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPatchUserSkill(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ownerRawClient := coderdtest.New(t, nil)
|
||||||
|
firstUser := coderdtest.CreateFirstUser(t, ownerRawClient)
|
||||||
|
memberRawClient, member := coderdtest.CreateAnotherUser(t, ownerRawClient, firstUser.OrganizationID)
|
||||||
|
memberClient := codersdk.NewExperimentalClient(memberRawClient)
|
||||||
|
auditorRawClient, _ := coderdtest.CreateAnotherUser(t, ownerRawClient, firstUser.OrganizationID, rbac.RoleAuditor())
|
||||||
|
auditorClient := codersdk.NewExperimentalClient(auditorRawClient)
|
||||||
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||||
|
|
||||||
|
_, err := memberClient.CreateUserSkill(ctx, codersdk.Me, codersdk.CreateUserSkillRequest{
|
||||||
|
Content: userSkillTestContent("forbidden-skill", "Original body."),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = auditorClient.UpdateUserSkill(ctx, member.ID.String(), "forbidden-skill", codersdk.UpdateUserSkillRequest{
|
||||||
|
Content: userSkillTestContent("forbidden-skill", "Updated body."),
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
var sdkErr *codersdk.Error
|
||||||
|
require.ErrorAs(t, err, &sdkErr)
|
||||||
|
assert.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSkillTestContent(name string, body string) string {
|
||||||
|
return "---\nname: " + name + "\ndescription: Test skill\n---\n" + body + "\n"
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
// and workspace/<name> for the workspace skill. One source must not silently
|
// and workspace/<name> for the workspace skill. One source must not silently
|
||||||
// override the other.
|
// override the other.
|
||||||
//
|
//
|
||||||
// Site admins can read and modify personal skill content. Personal skills are
|
// Site admins can read and delete personal skill content. Personal skills are
|
||||||
// user-authored instructions, not secret material. Audit records can include
|
// user-authored instructions, not secret material. Audit records can include
|
||||||
// raw Markdown content diffs alongside the actor, target user, and relevant
|
// raw Markdown content diffs alongside the actor, target user, and relevant
|
||||||
// metadata.
|
// metadata.
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ const MaxPersonalSkillSizeBytes = workspacesdk.MaxSkillMetaBytes
|
|||||||
// personal skill upload. Skill names are also used in URL paths.
|
// personal skill upload. Skill names are also used in URL paths.
|
||||||
const MaxPersonalSkillNameBytes = 256
|
const MaxPersonalSkillNameBytes = 256
|
||||||
|
|
||||||
|
// MaxPersonalSkillDescriptionBytes is the maximum frontmatter description size
|
||||||
|
// accepted for a personal skill upload.
|
||||||
|
const MaxPersonalSkillDescriptionBytes = 4096
|
||||||
|
|
||||||
|
// MaxPersonalSkillsPerUser is the maximum number of personal skills a user may
|
||||||
|
// create.
|
||||||
|
const MaxPersonalSkillsPerUser = 100
|
||||||
|
|
||||||
// Source identifies where a skill came from.
|
// Source identifies where a skill came from.
|
||||||
type Source string
|
type Source string
|
||||||
|
|
||||||
@@ -36,6 +44,8 @@ var (
|
|||||||
ErrSkillBodyRequired = xerrors.New("skill body is required")
|
ErrSkillBodyRequired = xerrors.New("skill body is required")
|
||||||
// ErrSkillTooLarge indicates that the raw skill Markdown is too large.
|
// ErrSkillTooLarge indicates that the raw skill Markdown is too large.
|
||||||
ErrSkillTooLarge = xerrors.New("skill is too large")
|
ErrSkillTooLarge = xerrors.New("skill is too large")
|
||||||
|
// ErrSkillDescriptionTooLarge indicates that the description is too large.
|
||||||
|
ErrSkillDescriptionTooLarge = xerrors.New("skill description is too large")
|
||||||
// ErrSkillNotFound indicates that a skill lookup did not match any alias.
|
// ErrSkillNotFound indicates that a skill lookup did not match any alias.
|
||||||
ErrSkillNotFound = xerrors.New("skill not found")
|
ErrSkillNotFound = xerrors.New("skill not found")
|
||||||
// ErrSkillAmbiguous indicates that a skill lookup matched multiple sources.
|
// ErrSkillAmbiguous indicates that a skill lookup matched multiple sources.
|
||||||
@@ -65,8 +75,9 @@ type ResolvedSkill struct {
|
|||||||
// ParsePersonalSkillMarkdown parses raw personal skill Markdown and enforces
|
// ParsePersonalSkillMarkdown parses raw personal skill Markdown and enforces
|
||||||
// the personal skill contract. The raw size must not exceed
|
// the personal skill contract. The raw size must not exceed
|
||||||
// MaxPersonalSkillSizeBytes, frontmatter must contain a valid kebab-case name,
|
// MaxPersonalSkillSizeBytes, frontmatter must contain a valid kebab-case name,
|
||||||
// the skill name must not exceed MaxPersonalSkillNameBytes, and the body after
|
// the skill name must not exceed MaxPersonalSkillNameBytes, the description must
|
||||||
// frontmatter must be non-empty.
|
// not exceed MaxPersonalSkillDescriptionBytes, and the body after frontmatter
|
||||||
|
// must be non-empty.
|
||||||
func ParsePersonalSkillMarkdown(raw []byte) (ParsedSkill, error) {
|
func ParsePersonalSkillMarkdown(raw []byte) (ParsedSkill, error) {
|
||||||
if len(raw) > MaxPersonalSkillSizeBytes {
|
if len(raw) > MaxPersonalSkillSizeBytes {
|
||||||
return ParsedSkill{}, xerrors.Errorf(
|
return ParsedSkill{}, xerrors.Errorf(
|
||||||
@@ -102,6 +113,15 @@ func ParsePersonalSkillMarkdown(raw []byte) (ParsedSkill, error) {
|
|||||||
MaxPersonalSkillNameBytes,
|
MaxPersonalSkillNameBytes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
descriptionBytes := len(description)
|
||||||
|
if descriptionBytes > MaxPersonalSkillDescriptionBytes {
|
||||||
|
return ParsedSkill{}, xerrors.Errorf(
|
||||||
|
"%w: got %d bytes, maximum is %d bytes",
|
||||||
|
ErrSkillDescriptionTooLarge,
|
||||||
|
descriptionBytes,
|
||||||
|
MaxPersonalSkillDescriptionBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
if strings.TrimSpace(body) == "" {
|
if strings.TrimSpace(body) == "" {
|
||||||
return ParsedSkill{}, xerrors.Errorf(
|
return ParsedSkill{}, xerrors.Errorf(
|
||||||
"%w: skill %q has no content after frontmatter",
|
"%w: skill %q has no content after frontmatter",
|
||||||
|
|||||||
@@ -91,6 +91,19 @@ func TestParsePersonalSkillMarkdown(t *testing.T) {
|
|||||||
require.ErrorContains(t, err, "maximum is 256 bytes")
|
require.ErrorContains(t, err, "maximum is 256 bytes")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("DescriptionTooLong", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := skills.ParsePersonalSkillMarkdown([]byte(personalSkillMarkdownForTest(
|
||||||
|
"my-skill",
|
||||||
|
strings.Repeat("a", skills.MaxPersonalSkillDescriptionBytes+1),
|
||||||
|
"Body.",
|
||||||
|
)))
|
||||||
|
|
||||||
|
require.ErrorIs(t, err, skills.ErrSkillDescriptionTooLarge)
|
||||||
|
require.ErrorContains(t, err, "maximum is 4096 bytes")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("EmptyBody", func(t *testing.T) {
|
t.Run("EmptyBody", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ const (
|
|||||||
APIKeyScopeUserSecretDelete APIKeyScope = "user_secret:delete"
|
APIKeyScopeUserSecretDelete APIKeyScope = "user_secret:delete"
|
||||||
APIKeyScopeUserSecretRead APIKeyScope = "user_secret:read"
|
APIKeyScopeUserSecretRead APIKeyScope = "user_secret:read"
|
||||||
APIKeyScopeUserSecretUpdate APIKeyScope = "user_secret:update"
|
APIKeyScopeUserSecretUpdate APIKeyScope = "user_secret:update"
|
||||||
|
APIKeyScopeUserSkillAll APIKeyScope = "user_skill:*"
|
||||||
|
APIKeyScopeUserSkillCreate APIKeyScope = "user_skill:create"
|
||||||
|
APIKeyScopeUserSkillDelete APIKeyScope = "user_skill:delete"
|
||||||
|
APIKeyScopeUserSkillRead APIKeyScope = "user_skill:read"
|
||||||
|
APIKeyScopeUserSkillUpdate APIKeyScope = "user_skill:update"
|
||||||
APIKeyScopeWebpushSubscriptionAll APIKeyScope = "webpush_subscription:*"
|
APIKeyScopeWebpushSubscriptionAll APIKeyScope = "webpush_subscription:*"
|
||||||
APIKeyScopeWebpushSubscriptionCreate APIKeyScope = "webpush_subscription:create"
|
APIKeyScopeWebpushSubscriptionCreate APIKeyScope = "webpush_subscription:create"
|
||||||
APIKeyScopeWebpushSubscriptionDelete APIKeyScope = "webpush_subscription:delete"
|
APIKeyScopeWebpushSubscriptionDelete APIKeyScope = "webpush_subscription:delete"
|
||||||
@@ -267,6 +272,11 @@ var PublicAPIKeyScopes = []APIKeyScope{
|
|||||||
APIKeyScopeUserSecretDelete,
|
APIKeyScopeUserSecretDelete,
|
||||||
APIKeyScopeUserSecretRead,
|
APIKeyScopeUserSecretRead,
|
||||||
APIKeyScopeUserSecretUpdate,
|
APIKeyScopeUserSecretUpdate,
|
||||||
|
APIKeyScopeUserSkillAll,
|
||||||
|
APIKeyScopeUserSkillCreate,
|
||||||
|
APIKeyScopeUserSkillDelete,
|
||||||
|
APIKeyScopeUserSkillRead,
|
||||||
|
APIKeyScopeUserSkillUpdate,
|
||||||
APIKeyScopeWorkspaceAll,
|
APIKeyScopeWorkspaceAll,
|
||||||
APIKeyScopeWorkspaceApplicationConnect,
|
APIKeyScopeWorkspaceApplicationConnect,
|
||||||
APIKeyScopeWorkspaceCreate,
|
APIKeyScopeWorkspaceCreate,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const (
|
|||||||
ResourceTypeGroupAIBudget ResourceType = "group_ai_budget"
|
ResourceTypeGroupAIBudget ResourceType = "group_ai_budget"
|
||||||
ResourceTypeChat ResourceType = "chat"
|
ResourceTypeChat ResourceType = "chat"
|
||||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||||
|
ResourceTypeUserSkill ResourceType = "user_skill"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r ResourceType) FriendlyString() string {
|
func (r ResourceType) FriendlyString() string {
|
||||||
@@ -121,6 +122,8 @@ func (r ResourceType) FriendlyString() string {
|
|||||||
return "chat"
|
return "chat"
|
||||||
case ResourceTypeUserSecret:
|
case ResourceTypeUserSecret:
|
||||||
return "user secret"
|
return "user secret"
|
||||||
|
case ResourceTypeUserSkill:
|
||||||
|
return "user skill"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const (
|
|||||||
ResourceUsageEvent RBACResource = "usage_event"
|
ResourceUsageEvent RBACResource = "usage_event"
|
||||||
ResourceUser RBACResource = "user"
|
ResourceUser RBACResource = "user"
|
||||||
ResourceUserSecret RBACResource = "user_secret"
|
ResourceUserSecret RBACResource = "user_secret"
|
||||||
|
ResourceUserSkill RBACResource = "user_skill"
|
||||||
ResourceWebpushSubscription RBACResource = "webpush_subscription"
|
ResourceWebpushSubscription RBACResource = "webpush_subscription"
|
||||||
ResourceWorkspace RBACResource = "workspace"
|
ResourceWorkspace RBACResource = "workspace"
|
||||||
ResourceWorkspaceAgentDevcontainers RBACResource = "workspace_agent_devcontainers"
|
ResourceWorkspaceAgentDevcontainers RBACResource = "workspace_agent_devcontainers"
|
||||||
@@ -120,6 +121,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
|
|||||||
ResourceUsageEvent: {ActionCreate, ActionRead, ActionUpdate},
|
ResourceUsageEvent: {ActionCreate, ActionRead, ActionUpdate},
|
||||||
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
ResourceUser: {ActionCreate, ActionDelete, ActionRead, ActionReadPersonal, ActionUpdate, ActionUpdatePersonal},
|
||||||
ResourceUserSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
ResourceUserSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
|
ResourceUserSkill: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||||
ResourceWebpushSubscription: {ActionCreate, ActionDelete, ActionRead},
|
ResourceWebpushSubscription: {ActionCreate, ActionDelete, ActionRead},
|
||||||
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent},
|
ResourceWorkspace: {ActionApplicationConnect, ActionCreate, ActionCreateAgent, ActionDelete, ActionDeleteAgent, ActionRead, ActionShare, ActionSSH, ActionWorkspaceStart, ActionWorkspaceStop, ActionUpdate, ActionUpdateAgent},
|
||||||
ResourceWorkspaceAgentDevcontainers: {ActionCreate},
|
ResourceWorkspaceAgentDevcontainers: {ActionCreate},
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package codersdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserSkillMetadata represents a user skill without its raw Markdown content.
|
||||||
|
type UserSkillMetadata struct {
|
||||||
|
ID uuid.UUID `json:"id" format:"uuid"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSkill represents a user skill with its raw Markdown content.
|
||||||
|
type UserSkill struct {
|
||||||
|
UserSkillMetadata
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserSkillRequest is the payload for creating a user skill.
|
||||||
|
type CreateUserSkillRequest struct {
|
||||||
|
// Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||||
|
// frontmatter must include name, may include description, and must be
|
||||||
|
// followed by a non-empty body.
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserSkillRequest is the payload for updating a user skill.
|
||||||
|
type UpdateUserSkillRequest struct {
|
||||||
|
// Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||||
|
// frontmatter must include name, may include description, and must be
|
||||||
|
// followed by a non-empty body.
|
||||||
|
Content string `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSkillsPath(user string) string {
|
||||||
|
return fmt.Sprintf("/api/experimental/users/%s/skills", url.PathEscape(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
func userSkillPath(user string, name string) string {
|
||||||
|
return fmt.Sprintf("%s/%s", userSkillsPath(user), url.PathEscape(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserSkill creates a user skill from raw Markdown content.
|
||||||
|
func (c *ExperimentalClient) CreateUserSkill(ctx context.Context, user string, req CreateUserSkillRequest) (UserSkill, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodPost, userSkillsPath(user), req)
|
||||||
|
if err != nil {
|
||||||
|
return UserSkill{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusCreated {
|
||||||
|
return UserSkill{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var skill UserSkill
|
||||||
|
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSkills lists user skill metadata for the specified user.
|
||||||
|
func (c *ExperimentalClient) UserSkills(ctx context.Context, user string) ([]UserSkillMetadata, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, userSkillsPath(user), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return nil, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var skills []UserSkillMetadata
|
||||||
|
return skills, json.NewDecoder(res.Body).Decode(&skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserSkillByName returns a user skill by name.
|
||||||
|
func (c *ExperimentalClient) UserSkillByName(ctx context.Context, user string, name string) (UserSkill, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodGet, userSkillPath(user, name), nil)
|
||||||
|
if err != nil {
|
||||||
|
return UserSkill{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return UserSkill{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var skill UserSkill
|
||||||
|
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserSkill replaces a user skill's raw Markdown content.
|
||||||
|
func (c *ExperimentalClient) UpdateUserSkill(ctx context.Context, user string, name string, req UpdateUserSkillRequest) (UserSkill, error) {
|
||||||
|
res, err := c.Request(ctx, http.MethodPatch, userSkillPath(user, name), req)
|
||||||
|
if err != nil {
|
||||||
|
return UserSkill{}, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
return UserSkill{}, ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
var skill UserSkill
|
||||||
|
return skill, json.NewDecoder(res.Body).Decode(&skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUserSkill deletes a user skill by name.
|
||||||
|
func (c *ExperimentalClient) DeleteUserSkill(ctx context.Context, user string, name string) error {
|
||||||
|
res, err := c.Request(ctx, http.MethodDelete, userSkillPath(user, name), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusNoContent {
|
||||||
|
return ReadBodyAsError(res)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ We track the following resources:
|
|||||||
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
| TemplateVersion<br><i>create, write</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>archived</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>created_by_avatar_url</td><td>false</td></tr><tr><td>created_by_name</td><td>false</td></tr><tr><td>created_by_username</td><td>false</td></tr><tr><td>external_auth_providers</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>true</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>message</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>readme</td><td>true</td></tr><tr><td>source_example_id</td><td>false</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
|
||||||
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_service_account</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
| User<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>avatar_url</td><td>false</td></tr><tr><td>chat_spend_limit_micros</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>true</td></tr><tr><td>email</td><td>true</td></tr><tr><td>github_com_user_id</td><td>false</td></tr><tr><td>hashed_one_time_passcode</td><td>false</td></tr><tr><td>hashed_password</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>is_service_account</td><td>true</td></tr><tr><td>is_system</td><td>true</td></tr><tr><td>last_seen_at</td><td>false</td></tr><tr><td>login_type</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>one_time_passcode_expires_at</td><td>true</td></tr><tr><td>quiet_hours_schedule</td><td>true</td></tr><tr><td>rbac_roles</td><td>true</td></tr><tr><td>status</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>username</td><td>true</td></tr></tbody></table> |
|
||||||
| UserSecret<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>env_name</td><td>true</td></tr><tr><td>file_path</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>value</td><td>true</td></tr><tr><td>value_key_id</td><td>false</td></tr></tbody></table> |
|
| UserSecret<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>env_name</td><td>true</td></tr><tr><td>file_path</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr><tr><td>value</td><td>true</td></tr><tr><td>value_key_id</td><td>false</td></tr></tbody></table> |
|
||||||
|
| UserSkill<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>content</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_id</td><td>true</td></tr></tbody></table> |
|
||||||
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
| WorkspaceBuild<br><i>start, stop</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>build_number</td><td>false</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>daily_cost</td><td>false</td></tr><tr><td>deadline</td><td>false</td></tr><tr><td>has_ai_task</td><td>false</td></tr><tr><td>has_external_agent</td><td>false</td></tr><tr><td>id</td><td>false</td></tr><tr><td>initiator_by_avatar_url</td><td>false</td></tr><tr><td>initiator_by_name</td><td>false</td></tr><tr><td>initiator_by_username</td><td>false</td></tr><tr><td>initiator_id</td><td>false</td></tr><tr><td>job_id</td><td>false</td></tr><tr><td>max_deadline</td><td>false</td></tr><tr><td>reason</td><td>false</td></tr><tr><td>template_version_id</td><td>true</td></tr><tr><td>template_version_preset_id</td><td>false</td></tr><tr><td>transition</td><td>false</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>workspace_id</td><td>false</td></tr></tbody></table> |
|
||||||
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
| WorkspaceProxy<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>created_at</td><td>true</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>derp_enabled</td><td>true</td></tr><tr><td>derp_only</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>region_id</td><td>true</td></tr><tr><td>token_hashed_secret</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>url</td><td>true</td></tr><tr><td>version</td><td>true</td></tr><tr><td>wildcard_hostname</td><td>true</td></tr></tbody></table> |
|
||||||
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
| WorkspaceTable<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>automatic_updates</td><td>true</td></tr><tr><td>autostart_schedule</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>deleted</td><td>false</td></tr><tr><td>deleting_at</td><td>true</td></tr><tr><td>dormant_at</td><td>true</td></tr><tr><td>favorite</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>id</td><td>true</td></tr><tr><td>last_used_at</td><td>false</td></tr><tr><td>name</td><td>true</td></tr><tr><td>next_start_at</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>owner_id</td><td>true</td></tr><tr><td>template_id</td><td>true</td></tr><tr><td>ttl</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
|
||||||
|
|||||||
Generated
+20
-20
@@ -193,10 +193,10 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -326,10 +326,10 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -459,10 +459,10 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -554,10 +554,10 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
@@ -960,9 +960,9 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
| `action` | `application_connect`, `assign`, `create`, `create_agent`, `delete`, `delete_agent`, `read`, `read_personal`, `share`, `ssh`, `start`, `stop`, `unassign`, `update`, `update_agent`, `update_personal`, `use`, `view_insights` |
|
||||||
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `resource_type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|||||||
Generated
+83
-9
File diff suppressed because one or more lines are too long
Generated
+5
-5
@@ -865,11 +865,11 @@ Status Code **200**
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Property | Value(s) |
|
| Property | Value(s) |
|
||||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
| `type` | `*`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_usage`, `chat`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `task`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_dormant`, `workspace_proxy` |
|
||||||
| `login_type` | `github`, `oidc`, `password`, `token` |
|
| `login_type` | `github`, `oidc`, `password`, `token` |
|
||||||
| `scope` | `all`, `application_connect` |
|
| `scope` | `all`, `application_connect` |
|
||||||
|
|
||||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,29 @@ func Test_diff(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
runDiffTests(t, []diffTest{
|
||||||
|
{
|
||||||
|
// User skill content is user-authored instruction text, not secret
|
||||||
|
// material, so audit diffs can include the content change.
|
||||||
|
name: "UserSkillContentTracked",
|
||||||
|
left: audit.Empty[database.UserSkill](),
|
||||||
|
right: database.UserSkill{
|
||||||
|
ID: uuid.UUID{1},
|
||||||
|
UserID: uuid.UUID{2},
|
||||||
|
Name: "review-guidance",
|
||||||
|
Description: "How to review private projects",
|
||||||
|
Content: "review markdown",
|
||||||
|
},
|
||||||
|
exp: audit.Map{
|
||||||
|
"id": audit.OldNew{Old: "", New: uuid.UUID{1}.String()},
|
||||||
|
"user_id": audit.OldNew{Old: "", New: uuid.UUID{2}.String()},
|
||||||
|
"name": audit.OldNew{Old: "", New: "review-guidance"},
|
||||||
|
"description": audit.OldNew{Old: "", New: "How to review private projects"},
|
||||||
|
"content": audit.OldNew{Old: "", New: "review markdown"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
runDiffTests(t, []diffTest{
|
runDiffTests(t, []diffTest{
|
||||||
{
|
{
|
||||||
name: "Create",
|
name: "Create",
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
|
|||||||
"AuditableGroupAiBudget": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
"AuditableGroupAiBudget": {codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||||
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
|
"Chat": {codersdk.AuditActionCreate, codersdk.AuditActionWrite}, // chats get 'archived' by users, not deleted.
|
||||||
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||||
|
"UserSkill": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action string
|
type Action string
|
||||||
@@ -446,6 +447,15 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
|||||||
"plan_mode": ActionIgnore, // Can flip back and forth during a session.
|
"plan_mode": ActionIgnore, // Can flip back and forth during a session.
|
||||||
"client_type": ActionIgnore, // Set at creation.
|
"client_type": ActionIgnore, // Set at creation.
|
||||||
},
|
},
|
||||||
|
&database.UserSkill{}: {
|
||||||
|
"id": ActionTrack,
|
||||||
|
"user_id": ActionTrack,
|
||||||
|
"name": ActionTrack,
|
||||||
|
"description": ActionTrack,
|
||||||
|
"content": ActionTrack,
|
||||||
|
"created_at": ActionIgnore,
|
||||||
|
"updated_at": ActionIgnore,
|
||||||
|
},
|
||||||
&database.UserSecret{}: {
|
&database.UserSecret{}: {
|
||||||
"id": ActionTrack,
|
"id": ActionTrack,
|
||||||
"user_id": ActionTrack,
|
"user_id": ActionTrack,
|
||||||
|
|||||||
@@ -215,6 +215,12 @@ export const RBACResourceActions: Partial<
|
|||||||
read: "read user secret metadata and value",
|
read: "read user secret metadata and value",
|
||||||
update: "update user secret metadata and value",
|
update: "update user secret metadata and value",
|
||||||
},
|
},
|
||||||
|
user_skill: {
|
||||||
|
create: "create a user skill",
|
||||||
|
delete: "delete a user skill",
|
||||||
|
read: "read user skill metadata and content",
|
||||||
|
update: "update user skill metadata and content",
|
||||||
|
},
|
||||||
webpush_subscription: {
|
webpush_subscription: {
|
||||||
create: "create webpush subscriptions",
|
create: "create webpush subscriptions",
|
||||||
delete: "delete webpush subscriptions",
|
delete: "delete webpush subscriptions",
|
||||||
|
|||||||
Generated
+60
@@ -653,6 +653,11 @@ export type APIKeyScope =
|
|||||||
| "user_secret:delete"
|
| "user_secret:delete"
|
||||||
| "user_secret:read"
|
| "user_secret:read"
|
||||||
| "user_secret:update"
|
| "user_secret:update"
|
||||||
|
| "user_skill:*"
|
||||||
|
| "user_skill:create"
|
||||||
|
| "user_skill:delete"
|
||||||
|
| "user_skill:read"
|
||||||
|
| "user_skill:update"
|
||||||
| "user:update"
|
| "user:update"
|
||||||
| "user:update_personal"
|
| "user:update_personal"
|
||||||
| "webpush_subscription:*"
|
| "webpush_subscription:*"
|
||||||
@@ -874,6 +879,11 @@ export const APIKeyScopes: APIKeyScope[] = [
|
|||||||
"user_secret:delete",
|
"user_secret:delete",
|
||||||
"user_secret:read",
|
"user_secret:read",
|
||||||
"user_secret:update",
|
"user_secret:update",
|
||||||
|
"user_skill:*",
|
||||||
|
"user_skill:create",
|
||||||
|
"user_skill:delete",
|
||||||
|
"user_skill:read",
|
||||||
|
"user_skill:update",
|
||||||
"user:update",
|
"user:update",
|
||||||
"user:update_personal",
|
"user:update_personal",
|
||||||
"webpush_subscription:*",
|
"webpush_subscription:*",
|
||||||
@@ -3581,6 +3591,19 @@ export interface CreateUserSecretRequest {
|
|||||||
readonly file_path?: string;
|
readonly file_path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/userskills.go
|
||||||
|
/**
|
||||||
|
* CreateUserSkillRequest is the payload for creating a user skill.
|
||||||
|
*/
|
||||||
|
export interface CreateUserSkillRequest {
|
||||||
|
/**
|
||||||
|
* Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||||
|
* frontmatter must include name, may include description, and must be
|
||||||
|
* followed by a non-empty body.
|
||||||
|
*/
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/workspaces.go
|
// From codersdk/workspaces.go
|
||||||
export type CreateWorkspaceBuildReason =
|
export type CreateWorkspaceBuildReason =
|
||||||
| "cli"
|
| "cli"
|
||||||
@@ -6718,6 +6741,7 @@ export type RBACResource =
|
|||||||
| "usage_event"
|
| "usage_event"
|
||||||
| "user"
|
| "user"
|
||||||
| "user_secret"
|
| "user_secret"
|
||||||
|
| "user_skill"
|
||||||
| "webpush_subscription"
|
| "webpush_subscription"
|
||||||
| "*"
|
| "*"
|
||||||
| "workspace"
|
| "workspace"
|
||||||
@@ -6767,6 +6791,7 @@ export const RBACResources: RBACResource[] = [
|
|||||||
"usage_event",
|
"usage_event",
|
||||||
"user",
|
"user",
|
||||||
"user_secret",
|
"user_secret",
|
||||||
|
"user_skill",
|
||||||
"webpush_subscription",
|
"webpush_subscription",
|
||||||
"*",
|
"*",
|
||||||
"workspace",
|
"workspace",
|
||||||
@@ -6914,6 +6939,7 @@ export type ResourceType =
|
|||||||
| "template_version"
|
| "template_version"
|
||||||
| "user"
|
| "user"
|
||||||
| "user_secret"
|
| "user_secret"
|
||||||
|
| "user_skill"
|
||||||
| "workspace"
|
| "workspace"
|
||||||
| "workspace_agent"
|
| "workspace_agent"
|
||||||
| "workspace_app"
|
| "workspace_app"
|
||||||
@@ -6948,6 +6974,7 @@ export const ResourceTypes: ResourceType[] = [
|
|||||||
"template_version",
|
"template_version",
|
||||||
"user",
|
"user",
|
||||||
"user_secret",
|
"user_secret",
|
||||||
|
"user_skill",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace_agent",
|
"workspace_agent",
|
||||||
"workspace_app",
|
"workspace_app",
|
||||||
@@ -8818,6 +8845,19 @@ export interface UpdateUserSecretRequest {
|
|||||||
readonly file_path?: string;
|
readonly file_path?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/userskills.go
|
||||||
|
/**
|
||||||
|
* UpdateUserSkillRequest is the payload for updating a user skill.
|
||||||
|
*/
|
||||||
|
export interface UpdateUserSkillRequest {
|
||||||
|
/**
|
||||||
|
* Content must be SKILL.md-format Markdown with YAML frontmatter. The
|
||||||
|
* frontmatter must include name, may include description, and must be
|
||||||
|
* followed by a non-empty body.
|
||||||
|
*/
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/workspaces.go
|
// From codersdk/workspaces.go
|
||||||
export interface UpdateWorkspaceACL {
|
export interface UpdateWorkspaceACL {
|
||||||
/**
|
/**
|
||||||
@@ -9235,6 +9275,26 @@ export interface UserSecret {
|
|||||||
readonly updated_at: string;
|
readonly updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/userskills.go
|
||||||
|
/**
|
||||||
|
* UserSkill represents a user skill with its raw Markdown content.
|
||||||
|
*/
|
||||||
|
export interface UserSkill extends UserSkillMetadata {
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From codersdk/userskills.go
|
||||||
|
/**
|
||||||
|
* UserSkillMetadata represents a user skill without its raw Markdown content.
|
||||||
|
*/
|
||||||
|
export interface UserSkillMetadata {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly created_at: string;
|
||||||
|
readonly updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/users.go
|
// From codersdk/users.go
|
||||||
export type UserStatus = "active" | "dormant" | "suspended";
|
export type UserStatus = "active" | "dormant" | "suspended";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user