mirror of
https://github.com/coder/coder.git
synced 2026-10-16 06:37:58 +08:00
feat: add the organization skills data model (#30609)
## Stack Part of [CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents). This stack adds organization skills to Coder Agents: admins publish a skill once per organization, share it with groups or members, and chats in that organization load it next to personal and workspace skills. It replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to top: 1. **#30609** data model: one migration, queries, RBAC and dbauthz (this PR) 2. #30610 organization skill API routes and the scope-neutral SDK 3. #30611 sharing ACL routes 4. #30612 settings UI: shared skills table, admin page, member section 5. #30613 chats: chatd loading, `/` menu, docs ## What changes This PR adds the data model and authorization for organization skills. Personal skills keep working unchanged: `git diff origin/main -- codersdk site` shows only generated RBAC and API key scope additions (`apikey_scopes_gen.go`, `rbacresources_gen.go`, `rbacresourcesGenerated.ts`, the RBAC and scope unions in `typesGenerated.ts`) and `codersdk.ResourceTypeOrganizationSkill` with its friendly string. No personal route, SDK type or site component changes, and `coderd/userskills_test.go` changes only by mechanical constant renames. **One migration, `000621_skills`** (please review it closely): - renames `user_skills` to `skills` with its constraints, index and triggers, and recreates the cap function and `delete_deleted_user_resources`, whose PL/pgSQL bodies name the table; - adds `organization_id` and `project_id` (`ON DELETE CASCADE`), `enabled` (default true), and `group_acl` / `user_acl` (`jsonb`, default `{}`), with CHECKs for exactly one owner, ACLs that are JSON objects, and ACLs that stay empty unless an organization owns the row, plus partial unique name indexes per owner kind; - replaces the cap trigger with `enforce_skills_per_owner_limit()`: 100 rows per owner, disabled rows included, locking the owner row `FOR NO KEY UPDATE` so unrelated foreign key checks are not blocked; - adds a `BEFORE DELETE` trigger on `organization_members` that removes a departing member from `user_acl` on that organization's skills, so rejoining doesn't restore a direct grant; - adds the `organization_skill` `resource_type` and `api_key_scope` enum values last; nothing in the migration uses them. The down migration undoes the three old migrations in reverse order: it drops organization and project rows and restores `user_skills`, its names, the cap function and `user_id NOT NULL`. Authorization: a new RBAC resource `organization_skill` (create, read, update, delete, share). Owner and org admin get it; site and org auditors get read; members read only through the skill's ACL, which applies only to members of its organization. `Skill.RBACObject()` returns the organization object with both ACLs; project rows stay denied to everyone. New sqlc queries cover organization insert, get, list (SQL-filtered through `GetAuthorizedOrganizationSkillMetadata` and a `regosql` converter), update and delete, with dbauthz wrappers and the audit table entries. No routes yet. ## Commits The first commit reproduces the reviewed code from the old stack; each later commit is a labeled trim or fix. - `baa3150e3b7` feat: add the organization skills data model. Reproduction: every non-site file this commit changes equals the reviewed old `authz` head `1e16b3004c4`, except that the old stack's migrations 000615, 000616 and 000617 are merged into one migration (then `000615_skills`, now `000621_skills`) with one fixture (`testdata/fixtures/000621_skills.up.sql`) and one migration test (now `testMigration000621Skills`). At that commit, `dump.sql` was byte-identical to the old head's. - `1b84e83463a` test(coderd/database): keep only the SQL paths of the org skill matrix. `TestOrganizationSkillAuthorization` becomes `TestOrganizationSkillQueries` instead of being deleted, because coverage showed it is this PR's only run of the `GetAuthorizedOrganizationSkillMetadata` row scan (80% to 65% without it) and of the organization update and delete queries (100% to 0%). Swapping `group_acl` and `user_acl` in the converter makes `GrantedMember` fail. The removed cases are decided by `TestRolePermissions`, `TestSkillRBACObjectOrganizationACL` and the dbauthz method suite. - `e2931b412e9` test(coderd/database): fold the skill cap cases into existing coverage. The user case is covered by `TestUserSkillLimit` and `TestUserSkillLimitConcurrentCreates`; the organization case folds into `ConcurrentOrganizationInsertsHonorCap`, which now also checks that disabled rows count and another organization is unaffected (mutation checked); the project case stays. - `be62aea9021` test(coderd/rbac): drop `TestOrganizationSkillReadScope`, covered by `TestExternalScopeNames` and the generated scope file. - `0c275ba37de` merges main at `454a2fcc697`, which took 000615 to 000618: it renumbers the migration to 000619 and resolves `dump.sql`, `check_constraint.go` and the generated API docs. `8f2b4b87ee2` merges main at `ad85646164e`, which took 000619: it renumbers the migration to 000620. - `5248d56b64f` merges main at `e7b76872098`, which took 000620 (`000620_audit_chat_project_instructions_resource_type`): it renumbers the migration, fixture and migration test to `000621_skills` (step `Migration000621Skills`, version 621), and resolves `enterprise/audit/table.go` (keeping main's `AuditableChatProjectInstructions` entries and the `Skill` entry), `dump.sql` and `models.go` (the `resource_type` enum gets `chat_project_instructions`, then `organization_skill`) and the regenerated `schemas.md`. The up and down SQL are unchanged by every renumbering. ## Size Production +549/-67, tests +728/-33, generated +881/-314; total +2158/-414. This is above the 500-line reviewer-friendly limit on purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this stack replaces. ## Replaces #30584 (rename `user_skills` to `skills`), #30585 (skill owners), #30586 (organization skill authorization). ## Validation - Migration test: up keeps the existing personal row and its ID; down restores the `user_skills` names, cap function and `NOT NULL`, and drops organization rows; soft-deleting a user removes their skills after up, after down, and after re-up. - `go test` passed on `be62aea9021`, before the main merges, for `coderd` (skill tests), `coderd/database`, `dbauthz`, `migrations`, `gentest`, `coderd/rbac/...`, `coderd/x/chatd`, `coderd/x/skills`, `enterprise/audit` and `enterprise/coderd`; `make lint/check-scopes` passed; the four personal skills Vitest files pass unchanged. - On `5248d56b64f`: build, vet, `TestMigrate`, `TestMigrationChain`, the `migrations`, `database`, `dbauthz`, `gentest`, `rbac` and `enterprise/audit` packages pass; `make gen` run twice is clean, and `make pre-commit` passes. - Remote UAT round 7 through Coder Agents passed on the whole stack; see #30613's Validation for scope and findings. > Xum opened this PR on behalf of @ibetitsmike.
This commit is contained in:
Generated
+16
@@ -19521,6 +19521,12 @@ const docTemplate = `{
|
||||
"organization_member:delete",
|
||||
"organization_member:read",
|
||||
"organization_member:update",
|
||||
"organization_skill:*",
|
||||
"organization_skill:create",
|
||||
"organization_skill:delete",
|
||||
"organization_skill:read",
|
||||
"organization_skill:share",
|
||||
"organization_skill:update",
|
||||
"prebuilt_workspace:*",
|
||||
"prebuilt_workspace:delete",
|
||||
"prebuilt_workspace:update",
|
||||
@@ -19777,6 +19783,12 @@ const docTemplate = `{
|
||||
"APIKeyScopeOrganizationMemberDelete",
|
||||
"APIKeyScopeOrganizationMemberRead",
|
||||
"APIKeyScopeOrganizationMemberUpdate",
|
||||
"APIKeyScopeOrganizationSkillAll",
|
||||
"APIKeyScopeOrganizationSkillCreate",
|
||||
"APIKeyScopeOrganizationSkillDelete",
|
||||
"APIKeyScopeOrganizationSkillRead",
|
||||
"APIKeyScopeOrganizationSkillShare",
|
||||
"APIKeyScopeOrganizationSkillUpdate",
|
||||
"APIKeyScopePrebuiltWorkspaceAll",
|
||||
"APIKeyScopePrebuiltWorkspaceDelete",
|
||||
"APIKeyScopePrebuiltWorkspaceUpdate",
|
||||
@@ -29020,6 +29032,7 @@ const docTemplate = `{
|
||||
"oauth2_app_secret",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"organization_skill",
|
||||
"prebuilt_workspace",
|
||||
"provisioner_daemon",
|
||||
"provisioner_jobs",
|
||||
@@ -29076,6 +29089,7 @@ const docTemplate = `{
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceOrganizationSkill",
|
||||
"ResourcePrebuiltWorkspace",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceProvisionerJobs",
|
||||
@@ -29319,6 +29333,7 @@ const docTemplate = `{
|
||||
"chat_automation",
|
||||
"user_secret",
|
||||
"user_skill",
|
||||
"organization_skill",
|
||||
"chat_instruction_settings",
|
||||
"chat_operational_settings",
|
||||
"chat_organization_system_prompt",
|
||||
@@ -29367,6 +29382,7 @@ const docTemplate = `{
|
||||
"ResourceTypeChatAutomation",
|
||||
"ResourceTypeUserSecret",
|
||||
"ResourceTypeUserSkill",
|
||||
"ResourceTypeOrganizationSkill",
|
||||
"ResourceTypeChatInstructionSettings",
|
||||
"ResourceTypeChatOperationalSettings",
|
||||
"ResourceTypeChatOrganizationSystemPrompt",
|
||||
|
||||
Generated
+16
@@ -17503,6 +17503,12 @@
|
||||
"organization_member:delete",
|
||||
"organization_member:read",
|
||||
"organization_member:update",
|
||||
"organization_skill:*",
|
||||
"organization_skill:create",
|
||||
"organization_skill:delete",
|
||||
"organization_skill:read",
|
||||
"organization_skill:share",
|
||||
"organization_skill:update",
|
||||
"prebuilt_workspace:*",
|
||||
"prebuilt_workspace:delete",
|
||||
"prebuilt_workspace:update",
|
||||
@@ -17759,6 +17765,12 @@
|
||||
"APIKeyScopeOrganizationMemberDelete",
|
||||
"APIKeyScopeOrganizationMemberRead",
|
||||
"APIKeyScopeOrganizationMemberUpdate",
|
||||
"APIKeyScopeOrganizationSkillAll",
|
||||
"APIKeyScopeOrganizationSkillCreate",
|
||||
"APIKeyScopeOrganizationSkillDelete",
|
||||
"APIKeyScopeOrganizationSkillRead",
|
||||
"APIKeyScopeOrganizationSkillShare",
|
||||
"APIKeyScopeOrganizationSkillUpdate",
|
||||
"APIKeyScopePrebuiltWorkspaceAll",
|
||||
"APIKeyScopePrebuiltWorkspaceDelete",
|
||||
"APIKeyScopePrebuiltWorkspaceUpdate",
|
||||
@@ -26662,6 +26674,7 @@
|
||||
"oauth2_app_secret",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"organization_skill",
|
||||
"prebuilt_workspace",
|
||||
"provisioner_daemon",
|
||||
"provisioner_jobs",
|
||||
@@ -26718,6 +26731,7 @@
|
||||
"ResourceOauth2AppSecret",
|
||||
"ResourceOrganization",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceOrganizationSkill",
|
||||
"ResourcePrebuiltWorkspace",
|
||||
"ResourceProvisionerDaemon",
|
||||
"ResourceProvisionerJobs",
|
||||
@@ -26951,6 +26965,7 @@
|
||||
"chat_automation",
|
||||
"user_secret",
|
||||
"user_skill",
|
||||
"organization_skill",
|
||||
"chat_instruction_settings",
|
||||
"chat_operational_settings",
|
||||
"chat_organization_system_prompt",
|
||||
@@ -26999,6 +27014,7 @@
|
||||
"ResourceTypeChatAutomation",
|
||||
"ResourceTypeUserSecret",
|
||||
"ResourceTypeUserSkill",
|
||||
"ResourceTypeOrganizationSkill",
|
||||
"ResourceTypeChatInstructionSettings",
|
||||
"ResourceTypeChatOperationalSettings",
|
||||
"ResourceTypeChatOrganizationSystemPrompt",
|
||||
|
||||
@@ -46,7 +46,7 @@ type Auditable interface {
|
||||
database.AuditableGroupAIBudget |
|
||||
database.AuditableUserAIBudgetOverride |
|
||||
database.UserSecret |
|
||||
database.UserSkill |
|
||||
database.Skill |
|
||||
database.ChatInstructionSettings |
|
||||
database.ChatOperationalSettings |
|
||||
database.ChatOrganizationSystemPrompt |
|
||||
|
||||
@@ -167,7 +167,7 @@ func ResourceTarget[T Auditable](tgt T) string {
|
||||
return cmp.Or(typed.DisplayName, typed.Slug, typed.ID.String())
|
||||
case database.UserSecret:
|
||||
return typed.Name
|
||||
case database.UserSkill:
|
||||
case database.Skill:
|
||||
return typed.Name
|
||||
case database.ChatInstructionSettings:
|
||||
return typed.Name
|
||||
@@ -285,7 +285,7 @@ func ResourceID[T Auditable](tgt T) uuid.UUID {
|
||||
return typed.ID
|
||||
case database.UserSecret:
|
||||
return typed.ID
|
||||
case database.UserSkill:
|
||||
case database.Skill:
|
||||
return typed.ID
|
||||
case database.ChatInstructionSettings:
|
||||
// Fixed ID per setting; see ChatInstructionSettings IDs.
|
||||
@@ -378,7 +378,7 @@ func ResourceType[T Auditable](tgt T) database.ResourceType {
|
||||
return database.ResourceTypeMCPServerConfig
|
||||
case database.UserSecret:
|
||||
return database.ResourceTypeUserSecret
|
||||
case database.UserSkill:
|
||||
case database.Skill:
|
||||
return database.ResourceTypeUserSkill
|
||||
case database.ChatInstructionSettings:
|
||||
return database.ResourceTypeChatInstructionSettings
|
||||
@@ -484,7 +484,7 @@ func ResourceRequiresOrgID[T Auditable]() bool {
|
||||
case database.UserSecret:
|
||||
// User secrets are global to the user across organizations.
|
||||
return false
|
||||
case database.UserSkill:
|
||||
case database.Skill:
|
||||
// User skills are global to the user across organizations.
|
||||
return false
|
||||
case database.ChatInstructionSettings:
|
||||
|
||||
Generated
+8
-4
@@ -66,6 +66,14 @@ const (
|
||||
CheckOauth2ProviderAppsClientTypeCheck CheckConstraint = "oauth2_provider_apps_client_type_check" // oauth2_provider_apps
|
||||
CheckMaxProvisionerLogsLength CheckConstraint = "max_provisioner_logs_length" // provisioner_jobs
|
||||
CheckNatsPortValidTcp CheckConstraint = "nats_port_valid_tcp" // replicas
|
||||
CheckSkillsAclOnlyOnOrganizationSkills CheckConstraint = "skills_acl_only_on_organization_skills" // skills
|
||||
CheckSkillsContentSize CheckConstraint = "skills_content_size" // skills
|
||||
CheckSkillsDescriptionSize CheckConstraint = "skills_description_size" // skills
|
||||
CheckSkillsGroupAclIsObject CheckConstraint = "skills_group_acl_is_object" // skills
|
||||
CheckSkillsNameFormat CheckConstraint = "skills_name_format" // skills
|
||||
CheckSkillsNameSize CheckConstraint = "skills_name_size" // skills
|
||||
CheckSkillsSingleOwner CheckConstraint = "skills_single_owner" // skills
|
||||
CheckSkillsUserAclIsObject CheckConstraint = "skills_user_acl_is_object" // skills
|
||||
CheckTelemetryLockEventTypeConstraint CheckConstraint = "telemetry_lock_event_type_constraint" // telemetry_locks
|
||||
CheckValidationMonotonicOrder CheckConstraint = "validation_monotonic_order" // template_version_parameters
|
||||
CheckUsageEventTypeCheck CheckConstraint = "usage_event_type_check" // usage_events
|
||||
@@ -73,10 +81,6 @@ const (
|
||||
CheckUserAIBudgetOverridesSpendLimitMicrosCheck CheckConstraint = "user_ai_budget_overrides_spend_limit_micros_check" // user_ai_budget_overrides
|
||||
CheckUserAIProviderKeysAPIKeyCheck CheckConstraint = "user_ai_provider_keys_api_key_check" // user_ai_provider_keys
|
||||
CheckUserSecretsEnabledRequiresTarget CheckConstraint = "user_secrets_enabled_requires_target" // user_secrets
|
||||
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
|
||||
CheckWorkspaceAgentScriptsDependenciesIsArray CheckConstraint = "workspace_agent_scripts_dependencies_is_array" // workspace_agent_scripts
|
||||
CheckMaxLogsLength CheckConstraint = "max_logs_length" // workspace_agents
|
||||
CheckSubsystemsNotNone CheckConstraint = "subsystems_not_none" // workspace_agents
|
||||
|
||||
@@ -2427,7 +2427,7 @@ func UserSecrets(secrets []database.ListUserSecretsRow) []codersdk.UserSecret {
|
||||
}
|
||||
|
||||
// UserSkill converts a database UserSkill to an SDK UserSkill.
|
||||
func UserSkill(skill database.UserSkill) codersdk.UserSkill {
|
||||
func UserSkill(skill database.Skill) codersdk.UserSkill {
|
||||
return codersdk.UserSkill{
|
||||
UserSkillMetadata: codersdk.UserSkillMetadata{
|
||||
ID: skill.ID,
|
||||
|
||||
@@ -2623,6 +2623,13 @@ func (q *querier) DeleteOrganizationMember(ctx context.Context, arg database.Del
|
||||
}, q.db.DeleteOrganizationMember)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.DeleteOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
fetch := func(ctx context.Context, arg database.DeleteOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
return q.db.GetOrganizationSkillByOrganizationIDAndName(ctx, database.GetOrganizationSkillByOrganizationIDAndNameParams(arg))
|
||||
}
|
||||
return fetchAndQuery(q.log, q.auth, policy.ActionDelete, fetch, q.db.DeleteOrganizationSkillByOrganizationIDAndName)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetProvisionerKeyByID, q.db.DeleteProvisionerKey)(ctx, id)
|
||||
}
|
||||
@@ -2742,10 +2749,10 @@ func (q *querier) DeleteUserSecretByUserIDAndName(ctx context.Context, arg datab
|
||||
return q.db.DeleteUserSecretByUserIDAndName(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (q *querier) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, obj); err != nil {
|
||||
return database.UserSkill{}, err
|
||||
return database.Skill{}, err
|
||||
}
|
||||
return q.db.DeleteUserSkillByUserIDAndName(ctx, arg)
|
||||
}
|
||||
@@ -4644,6 +4651,10 @@ func (q *querier) GetOrganizationResourceCountByID(ctx context.Context, organiza
|
||||
return q.db.GetOrganizationResourceCountByID(ctx, organizationID)
|
||||
}
|
||||
|
||||
func (q *querier) GetOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.GetOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetOrganizationSkillByOrganizationIDAndName)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetOrganizations(ctx context.Context, args database.GetOrganizationsParams) ([]database.Organization, error) {
|
||||
fetch := func(ctx context.Context, _ interface{}) ([]database.Organization, error) {
|
||||
return q.db.GetOrganizations(ctx, args)
|
||||
@@ -5605,10 +5616,10 @@ func (q *querier) GetUserShellToolDisplayMode(ctx context.Context, userID uuid.U
|
||||
return q.db.GetUserShellToolDisplayMode(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (q *querier) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil {
|
||||
return database.UserSkill{}, err
|
||||
return database.Skill{}, err
|
||||
}
|
||||
return q.db.GetUserSkillByUserIDAndName(ctx, arg)
|
||||
}
|
||||
@@ -6620,6 +6631,13 @@ func (q *querier) InsertOrganizationMember(ctx context.Context, arg database.Ins
|
||||
return insert(q.log, q.auth, obj, q.db.InsertOrganizationMember)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertOrganizationSkill(ctx context.Context, arg database.InsertOrganizationSkillParams) (database.Skill, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceOrganizationSkill.InOrg(arg.OrganizationID)); err != nil {
|
||||
return database.Skill{}, err
|
||||
}
|
||||
return q.db.InsertOrganizationSkill(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) {
|
||||
err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceTemplate)
|
||||
if err != nil {
|
||||
@@ -6784,10 +6802,10 @@ func (q *querier) InsertUserLink(ctx context.Context, arg database.InsertUserLin
|
||||
return q.db.InsertUserLink(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||
func (q *querier) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.Skill, error) {
|
||||
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil {
|
||||
return database.UserSkill{}, err
|
||||
return database.Skill{}, err
|
||||
}
|
||||
return q.db.InsertUserSkill(ctx, arg)
|
||||
}
|
||||
@@ -7181,6 +7199,14 @@ func (q *querier) ListOrganizationAISpendUsers(ctx context.Context, arg database
|
||||
return q.db.ListOrganizationAISpendUsers(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ListOrganizationSkillMetadataByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
prepared, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceOrganizationSkill.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("prepare sql filter: %w", err)
|
||||
}
|
||||
return q.db.GetAuthorizedOrganizationSkillMetadata(ctx, organizationID, prepared)
|
||||
}
|
||||
|
||||
func (q *querier) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganization)(ctx, organizationID)
|
||||
}
|
||||
@@ -8190,6 +8216,16 @@ func (q *querier) UpdateOrganizationDeletedByID(ctx context.Context, arg databas
|
||||
return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, deleteF)(ctx, arg.ID)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.UpdateOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
return q.db.GetOrganizationSkillByOrganizationIDAndName(ctx, database.GetOrganizationSkillByOrganizationIDAndNameParams{
|
||||
OrganizationID: arg.OrganizationID,
|
||||
Name: arg.Name,
|
||||
})
|
||||
}
|
||||
return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOrganizationSkillByOrganizationIDAndName)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) {
|
||||
fetch := func(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) {
|
||||
return q.db.GetOrganizationByID(ctx, arg.ID)
|
||||
@@ -8742,10 +8778,10 @@ func (q *querier) UpdateUserShellToolDisplayMode(ctx context.Context, arg databa
|
||||
return q.db.UpdateUserShellToolDisplayMode(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (q *querier) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
obj := rbac.ResourceUserSkill.WithOwner(arg.UserID.String())
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, obj); err != nil {
|
||||
return database.UserSkill{}, err
|
||||
return database.Skill{}, err
|
||||
}
|
||||
return q.db.UpdateUserSkillByUserIDAndName(ctx, arg)
|
||||
}
|
||||
@@ -9767,3 +9803,7 @@ func (q *querier) GetAuthorizedChatModelConfigs(ctx context.Context, organizatio
|
||||
func (q *querier) GetAuthorizedMCPServerConfigs(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.MCPServerConfig, error) {
|
||||
return q.db.GetAuthorizedMCPServerConfigs(ctx, organizationID, prepared)
|
||||
}
|
||||
|
||||
func (q *querier) GetAuthorizedOrganizationSkillMetadata(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
return q.db.GetAuthorizedOrganizationSkillMetadata(ctx, organizationID, prepared)
|
||||
}
|
||||
|
||||
@@ -6855,7 +6855,7 @@ 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})
|
||||
skill := testutil.Fake(s.T(), faker, database.Skill{UserID: uuid.NullUUID{UUID: user.ID, Valid: true}})
|
||||
arg := database.GetUserSkillByUserIDAndNameParams{UserID: user.ID, Name: skill.Name}
|
||||
dbm.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), arg).Return(skill, nil).AnyTimes()
|
||||
check.Args(arg).
|
||||
@@ -6864,7 +6864,7 @@ func (s *MethodTestSuite) TestUserSkills() {
|
||||
}))
|
||||
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})
|
||||
row := testutil.Fake(s.T(), faker, database.ListUserSkillMetadataByUserIDRow{UserID: uuid.NullUUID{UUID: user.ID, Valid: true}})
|
||||
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).
|
||||
@@ -6877,9 +6877,9 @@ func (s *MethodTestSuite) TestUserSkills() {
|
||||
UserID: user.ID,
|
||||
Name: "test",
|
||||
}
|
||||
ret := testutil.Fake(s.T(), faker, database.UserSkill{
|
||||
ret := testutil.Fake(s.T(), faker, database.Skill{
|
||||
ID: arg.ID,
|
||||
UserID: user.ID,
|
||||
UserID: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
Name: arg.Name,
|
||||
})
|
||||
dbm.EXPECT().InsertUserSkill(gomock.Any(), arg).Return(ret, nil).AnyTimes()
|
||||
@@ -6890,7 +6890,7 @@ func (s *MethodTestSuite) TestUserSkills() {
|
||||
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})
|
||||
updated := testutil.Fake(s.T(), faker, database.Skill{UserID: uuid.NullUUID{UUID: user.ID, Valid: true}, 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).
|
||||
@@ -6899,7 +6899,7 @@ func (s *MethodTestSuite) TestUserSkills() {
|
||||
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})
|
||||
deleted := testutil.Fake(s.T(), faker, database.Skill{UserID: uuid.NullUUID{UUID: user.ID, Valid: true}, 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).
|
||||
@@ -6907,6 +6907,70 @@ func (s *MethodTestSuite) TestUserSkills() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestOrganizationSkills() {
|
||||
orgSkill := func(orgID uuid.UUID, name string) database.Skill {
|
||||
return database.Skill{
|
||||
ID: uuid.New(),
|
||||
OrganizationID: uuid.NullUUID{UUID: orgID, Valid: true},
|
||||
Name: name,
|
||||
GroupACL: database.ChatACL{orgID.String(): {Permissions: []policy.Action{policy.ActionRead}}},
|
||||
}
|
||||
}
|
||||
s.Run("InsertOrganizationSkill", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertOrganizationSkillParams{ID: uuid.New(), OrganizationID: uuid.New(), Name: "test"}
|
||||
ret := orgSkill(arg.OrganizationID, arg.Name)
|
||||
dbm.EXPECT().InsertOrganizationSkill(gomock.Any(), arg).Return(ret, nil).AnyTimes()
|
||||
check.Args(arg).
|
||||
Asserts(rbac.ResourceOrganizationSkill.InOrg(arg.OrganizationID), policy.ActionCreate).
|
||||
Returns(ret)
|
||||
}))
|
||||
s.Run("GetOrganizationSkillByOrganizationIDAndName", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetOrganizationSkillByOrganizationIDAndNameParams{OrganizationID: uuid.New(), Name: "test"}
|
||||
skill := orgSkill(arg.OrganizationID, arg.Name)
|
||||
dbm.EXPECT().GetOrganizationSkillByOrganizationIDAndName(gomock.Any(), arg).Return(skill, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(skill, policy.ActionRead).Returns(skill)
|
||||
}))
|
||||
s.Run("ListOrganizationSkillMetadataByOrganizationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
orgID := uuid.New()
|
||||
rows := []database.ListOrganizationSkillMetadataByOrganizationIDRow{
|
||||
testutil.Fake(s.T(), faker, database.ListOrganizationSkillMetadataByOrganizationIDRow{OrganizationID: uuid.NullUUID{UUID: orgID, Valid: true}}),
|
||||
}
|
||||
dbm.EXPECT().GetAuthorizedOrganizationSkillMetadata(gomock.Any(), orgID, gomock.Any()).Return(rows, nil).AnyTimes()
|
||||
check.Args(orgID).Asserts().Returns(rows)
|
||||
}))
|
||||
s.Run("GetAuthorizedOrganizationSkillMetadata", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
orgID := uuid.New()
|
||||
rows := []database.ListOrganizationSkillMetadataByOrganizationIDRow{
|
||||
testutil.Fake(s.T(), faker, database.ListOrganizationSkillMetadataByOrganizationIDRow{OrganizationID: uuid.NullUUID{UUID: orgID, Valid: true}}),
|
||||
}
|
||||
dbm.EXPECT().GetAuthorizedOrganizationSkillMetadata(gomock.Any(), orgID, gomock.Any()).Return(rows, nil).AnyTimes()
|
||||
check.Args(orgID, emptyPreparedAuthorized{}).Asserts().Returns(rows)
|
||||
}))
|
||||
s.Run("UpdateOrganizationSkillByOrganizationIDAndName", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.UpdateOrganizationSkillByOrganizationIDAndNameParams{
|
||||
OrganizationID: uuid.New(),
|
||||
Name: "test",
|
||||
Enabled: sql.NullBool{Bool: false, Valid: true},
|
||||
}
|
||||
skill := orgSkill(arg.OrganizationID, arg.Name)
|
||||
updated := skill
|
||||
updated.Enabled = false
|
||||
dbm.EXPECT().GetOrganizationSkillByOrganizationIDAndName(gomock.Any(), database.GetOrganizationSkillByOrganizationIDAndNameParams{
|
||||
OrganizationID: arg.OrganizationID,
|
||||
Name: arg.Name,
|
||||
}).Return(skill, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateOrganizationSkillByOrganizationIDAndName(gomock.Any(), arg).Return(updated, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(skill, policy.ActionUpdate).Returns(updated)
|
||||
}))
|
||||
s.Run("DeleteOrganizationSkillByOrganizationIDAndName", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.DeleteOrganizationSkillByOrganizationIDAndNameParams{OrganizationID: uuid.New(), Name: "test"}
|
||||
skill := orgSkill(arg.OrganizationID, arg.Name)
|
||||
dbm.EXPECT().GetOrganizationSkillByOrganizationIDAndName(gomock.Any(), database.GetOrganizationSkillByOrganizationIDAndNameParams(arg)).Return(skill, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteOrganizationSkillByOrganizationIDAndName(gomock.Any(), arg).Return(skill, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(skill, policy.ActionDelete).Returns(skill)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestUsageEvents() {
|
||||
s.Run("InsertUsageEvent", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.InsertUsageEventParams{
|
||||
|
||||
@@ -1175,6 +1175,27 @@ func Organization(t testing.TB, db database.Store, orig database.Organization) d
|
||||
return org
|
||||
}
|
||||
|
||||
// OrganizationSkill inserts a skill owned by seed.OrganizationID, which is
|
||||
// required. Inserted skills are enabled.
|
||||
func OrganizationSkill(t testing.TB, db database.Store, seed database.Skill) database.Skill {
|
||||
t.Helper()
|
||||
|
||||
require.True(t, seed.OrganizationID.Valid, "organization skill requires an organization ID")
|
||||
name := takeFirst(seed.Name, "skill-"+uuid.NewString())
|
||||
description := takeFirst(seed.Description, "Organization skill fixture.")
|
||||
skill, err := db.InsertOrganizationSkill(genCtx, database.InsertOrganizationSkillParams{
|
||||
ID: takeFirst(seed.ID, uuid.New()),
|
||||
OrganizationID: seed.OrganizationID.UUID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Content: takeFirst(seed.Content, fmt.Sprintf("---\nname: %s\ndescription: %s\n---\n\nFixture instructions.\n", name, description)),
|
||||
GroupACL: seed.GroupACL,
|
||||
UserACL: seed.UserACL,
|
||||
})
|
||||
require.NoError(t, err, "insert organization skill")
|
||||
return skill
|
||||
}
|
||||
|
||||
func OrganizationMember(t testing.TB, db database.Store, orig database.OrganizationMember) database.OrganizationMember {
|
||||
mem, err := db.InsertOrganizationMember(genCtx, database.InsertOrganizationMemberParams{
|
||||
OrganizationID: takeFirst(orig.OrganizationID, uuid.New()),
|
||||
|
||||
+52
-4
@@ -872,6 +872,14 @@ func (m queryMetricsStore) DeleteOrganizationMember(ctx context.Context, arg dat
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.DeleteOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteOrganizationSkillByOrganizationIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteOrganizationSkillByOrganizationIDAndName").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteOrganizationSkillByOrganizationIDAndName").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteProvisionerKey(ctx, id)
|
||||
@@ -976,7 +984,7 @@ func (m queryMetricsStore) DeleteUserSecretByUserIDAndName(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m queryMetricsStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteUserSkillByUserIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("DeleteUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||
@@ -2808,6 +2816,14 @@ func (m queryMetricsStore) GetOrganizationResourceCountByID(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.GetOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOrganizationSkillByOrganizationIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetOrganizationSkillByOrganizationIDAndName").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetOrganizationSkillByOrganizationIDAndName").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetOrganizations(ctx context.Context, arg database.GetOrganizationsParams) ([]database.Organization, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetOrganizations(ctx, arg)
|
||||
@@ -3632,7 +3648,7 @@ func (m queryMetricsStore) GetUserShellToolDisplayMode(ctx context.Context, user
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m queryMetricsStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserSkillByUserIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||
@@ -4568,6 +4584,14 @@ func (m queryMetricsStore) InsertOrganizationMember(ctx context.Context, arg dat
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertOrganizationSkill(ctx context.Context, arg database.InsertOrganizationSkillParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertOrganizationSkill(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertOrganizationSkill").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertOrganizationSkill").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertPreset(ctx, arg)
|
||||
@@ -4728,7 +4752,7 @@ func (m queryMetricsStore) InsertUserLink(ctx context.Context, arg database.Inse
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||
func (m queryMetricsStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertUserSkill(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertUserSkill").Observe(time.Since(start).Seconds())
|
||||
@@ -5024,6 +5048,14 @@ func (m queryMetricsStore) ListOrganizationAISpendUsers(ctx context.Context, arg
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListOrganizationSkillMetadataByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListOrganizationSkillMetadataByOrganizationID(ctx, organizationID)
|
||||
m.queryLatencies.WithLabelValues("ListOrganizationSkillMetadataByOrganizationID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListOrganizationSkillMetadataByOrganizationID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListProvisionerKeysByOrganization(ctx, organizationID)
|
||||
@@ -5785,6 +5817,14 @@ func (m queryMetricsStore) UpdateOrganizationDeletedByID(ctx context.Context, ar
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.UpdateOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateOrganizationSkillByOrganizationIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateOrganizationSkillByOrganizationIDAndName").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateOrganizationSkillByOrganizationIDAndName").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateOrganizationWorkspaceSharingSettings(ctx, arg)
|
||||
@@ -6153,7 +6193,7 @@ func (m queryMetricsStore) UpdateUserShellToolDisplayMode(ctx context.Context, a
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m queryMetricsStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateUserSkillByUserIDAndName(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateUserSkillByUserIDAndName").Observe(time.Since(start).Seconds())
|
||||
@@ -7088,3 +7128,11 @@ func (m queryMetricsStore) GetAuthorizedMCPServerConfigs(ctx context.Context, or
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedMCPServerConfigs").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAuthorizedOrganizationSkillMetadata(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedOrganizationSkillMetadata(ctx, organizationID, prepared)
|
||||
m.queryLatencies.WithLabelValues("GetAuthorizedOrganizationSkillMetadata").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedOrganizationSkillMetadata").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
Generated
+98
-8
@@ -1499,6 +1499,21 @@ func (mr *MockStoreMockRecorder) DeleteOrganizationMember(ctx, arg any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganizationMember", reflect.TypeOf((*MockStore)(nil).DeleteOrganizationMember), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteOrganizationSkillByOrganizationIDAndName mocks base method.
|
||||
func (m *MockStore) DeleteOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.DeleteOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteOrganizationSkillByOrganizationIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DeleteOrganizationSkillByOrganizationIDAndName indicates an expected call of DeleteOrganizationSkillByOrganizationIDAndName.
|
||||
func (mr *MockStoreMockRecorder) DeleteOrganizationSkillByOrganizationIDAndName(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganizationSkillByOrganizationIDAndName", reflect.TypeOf((*MockStore)(nil).DeleteOrganizationSkillByOrganizationIDAndName), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteProvisionerKey mocks base method.
|
||||
func (m *MockStore) DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1688,10 +1703,10 @@ func (mr *MockStoreMockRecorder) DeleteUserSecretByUserIDAndName(ctx, arg any) *
|
||||
}
|
||||
|
||||
// DeleteUserSkillByUserIDAndName mocks base method.
|
||||
func (m *MockStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m *MockStore) DeleteUserSkillByUserIDAndName(ctx context.Context, arg database.DeleteUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteUserSkillByUserIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserSkill)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -2681,6 +2696,21 @@ func (mr *MockStoreMockRecorder) GetAuthorizedMCPServerConfigs(ctx, organization
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedMCPServerConfigs", reflect.TypeOf((*MockStore)(nil).GetAuthorizedMCPServerConfigs), ctx, organizationID, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedOrganizationSkillMetadata mocks base method.
|
||||
func (m *MockStore) GetAuthorizedOrganizationSkillMetadata(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthorizedOrganizationSkillMetadata", ctx, organizationID, prepared)
|
||||
ret0, _ := ret[0].([]database.ListOrganizationSkillMetadataByOrganizationIDRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAuthorizedOrganizationSkillMetadata indicates an expected call of GetAuthorizedOrganizationSkillMetadata.
|
||||
func (mr *MockStoreMockRecorder) GetAuthorizedOrganizationSkillMetadata(ctx, organizationID, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedOrganizationSkillMetadata", reflect.TypeOf((*MockStore)(nil).GetAuthorizedOrganizationSkillMetadata), ctx, organizationID, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedTemplates mocks base method.
|
||||
func (m *MockStore) GetAuthorizedTemplates(ctx context.Context, arg database.GetTemplatesWithFilterParams, prepared rbac.PreparedAuthorized) ([]database.Template, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -5261,6 +5291,21 @@ func (mr *MockStoreMockRecorder) GetOrganizationResourceCountByID(ctx, organizat
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationResourceCountByID", reflect.TypeOf((*MockStore)(nil).GetOrganizationResourceCountByID), ctx, organizationID)
|
||||
}
|
||||
|
||||
// GetOrganizationSkillByOrganizationIDAndName mocks base method.
|
||||
func (m *MockStore) GetOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.GetOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetOrganizationSkillByOrganizationIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetOrganizationSkillByOrganizationIDAndName indicates an expected call of GetOrganizationSkillByOrganizationIDAndName.
|
||||
func (mr *MockStoreMockRecorder) GetOrganizationSkillByOrganizationIDAndName(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizationSkillByOrganizationIDAndName", reflect.TypeOf((*MockStore)(nil).GetOrganizationSkillByOrganizationIDAndName), ctx, arg)
|
||||
}
|
||||
|
||||
// GetOrganizations mocks base method.
|
||||
func (m *MockStore) GetOrganizations(ctx context.Context, arg database.GetOrganizationsParams) ([]database.Organization, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -6837,10 +6882,10 @@ func (mr *MockStoreMockRecorder) GetUserShellToolDisplayMode(ctx, userID any) *g
|
||||
}
|
||||
|
||||
// GetUserSkillByUserIDAndName mocks base method.
|
||||
func (m *MockStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m *MockStore) GetUserSkillByUserIDAndName(ctx context.Context, arg database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserSkillByUserIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserSkill)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -8600,6 +8645,21 @@ func (mr *MockStoreMockRecorder) InsertOrganizationMember(ctx, arg any) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganizationMember", reflect.TypeOf((*MockStore)(nil).InsertOrganizationMember), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertOrganizationSkill mocks base method.
|
||||
func (m *MockStore) InsertOrganizationSkill(ctx context.Context, arg database.InsertOrganizationSkillParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertOrganizationSkill", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertOrganizationSkill indicates an expected call of InsertOrganizationSkill.
|
||||
func (mr *MockStoreMockRecorder) InsertOrganizationSkill(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOrganizationSkill", reflect.TypeOf((*MockStore)(nil).InsertOrganizationSkill), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertPreset mocks base method.
|
||||
func (m *MockStore) InsertPreset(ctx context.Context, arg database.InsertPresetParams) (database.TemplateVersionPreset, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8895,10 +8955,10 @@ func (mr *MockStoreMockRecorder) InsertUserLink(ctx, arg any) *gomock.Call {
|
||||
}
|
||||
|
||||
// InsertUserSkill mocks base method.
|
||||
func (m *MockStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.UserSkill, error) {
|
||||
func (m *MockStore) InsertUserSkill(ctx context.Context, arg database.InsertUserSkillParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertUserSkill", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserSkill)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -9519,6 +9579,21 @@ func (mr *MockStoreMockRecorder) ListOrganizationAISpendUsers(ctx, arg any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganizationAISpendUsers", reflect.TypeOf((*MockStore)(nil).ListOrganizationAISpendUsers), ctx, arg)
|
||||
}
|
||||
|
||||
// ListOrganizationSkillMetadataByOrganizationID mocks base method.
|
||||
func (m *MockStore) ListOrganizationSkillMetadataByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]database.ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListOrganizationSkillMetadataByOrganizationID", ctx, organizationID)
|
||||
ret0, _ := ret[0].([]database.ListOrganizationSkillMetadataByOrganizationIDRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListOrganizationSkillMetadataByOrganizationID indicates an expected call of ListOrganizationSkillMetadataByOrganizationID.
|
||||
func (mr *MockStoreMockRecorder) ListOrganizationSkillMetadataByOrganizationID(ctx, organizationID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganizationSkillMetadataByOrganizationID", reflect.TypeOf((*MockStore)(nil).ListOrganizationSkillMetadataByOrganizationID), ctx, organizationID)
|
||||
}
|
||||
|
||||
// ListProvisionerKeysByOrganization mocks base method.
|
||||
func (m *MockStore) ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]database.ProvisionerKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -10946,6 +11021,21 @@ func (mr *MockStoreMockRecorder) UpdateOrganizationDeletedByID(ctx, arg any) *go
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganizationDeletedByID", reflect.TypeOf((*MockStore)(nil).UpdateOrganizationDeletedByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateOrganizationSkillByOrganizationIDAndName mocks base method.
|
||||
func (m *MockStore) UpdateOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg database.UpdateOrganizationSkillByOrganizationIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateOrganizationSkillByOrganizationIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateOrganizationSkillByOrganizationIDAndName indicates an expected call of UpdateOrganizationSkillByOrganizationIDAndName.
|
||||
func (mr *MockStoreMockRecorder) UpdateOrganizationSkillByOrganizationIDAndName(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganizationSkillByOrganizationIDAndName", reflect.TypeOf((*MockStore)(nil).UpdateOrganizationSkillByOrganizationIDAndName), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateOrganizationWorkspaceSharingSettings mocks base method.
|
||||
func (m *MockStore) UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg database.UpdateOrganizationWorkspaceSharingSettingsParams) (database.Organization, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -11613,10 +11703,10 @@ func (mr *MockStoreMockRecorder) UpdateUserShellToolDisplayMode(ctx, arg any) *g
|
||||
}
|
||||
|
||||
// UpdateUserSkillByUserIDAndName mocks base method.
|
||||
func (m *MockStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func (m *MockStore) UpdateUserSkillByUserIDAndName(ctx context.Context, arg database.UpdateUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserSkillByUserIDAndName", ctx, arg)
|
||||
ret0, _ := ret[0].(database.UserSkill)
|
||||
ret0, _ := ret[0].(database.Skill)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
Generated
+113
-57
@@ -294,7 +294,13 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'chat_project_memory:*',
|
||||
'chat_project_memory:create',
|
||||
'chat_project_memory:read',
|
||||
'chat_project_memory:delete'
|
||||
'chat_project_memory:delete',
|
||||
'organization_skill:*',
|
||||
'organization_skill:create',
|
||||
'organization_skill:read',
|
||||
'organization_skill:update',
|
||||
'organization_skill:delete',
|
||||
'organization_skill:share'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -637,7 +643,8 @@ CREATE TYPE resource_type AS ENUM (
|
||||
'chat_automation',
|
||||
'chat_project_memory',
|
||||
'chat_organization_system_prompt',
|
||||
'chat_project_instructions'
|
||||
'chat_project_instructions',
|
||||
'organization_skill'
|
||||
);
|
||||
|
||||
CREATE TYPE shareable_workspace_owners AS ENUM (
|
||||
@@ -991,10 +998,10 @@ BEGIN
|
||||
DELETE FROM organization_members
|
||||
WHERE user_id = OLD.id;
|
||||
|
||||
-- Remove their user_skills.
|
||||
-- user_skills.user_id has ON DELETE CASCADE, but soft-delete
|
||||
-- Remove their personal skills.
|
||||
-- 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
|
||||
DELETE FROM skills
|
||||
WHERE user_id = OLD.id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
@@ -1020,6 +1027,21 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION delete_skill_user_acl_on_org_member_delete() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Drop the departing member's direct grants so adding them back to the
|
||||
-- organization does not silently restore access.
|
||||
UPDATE skills
|
||||
SET
|
||||
user_acl = user_acl - OLD.user_id::text,
|
||||
updated_at = now()
|
||||
WHERE organization_id = OLD.organization_id AND user_acl ? OLD.user_id::text;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION delete_user_ai_budget_overrides_on_group_member_delete() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -1040,6 +1062,45 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION enforce_skills_per_owner_limit() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
skill_count int;
|
||||
skill_limit constant int := 100;
|
||||
BEGIN
|
||||
-- Serialize skill-cap checks per owner so concurrent inserts cannot all
|
||||
-- observe the same pre-insert count and exceed the hard limit. FOR NO KEY
|
||||
-- UPDATE does not block the KEY SHARE locks of unrelated foreign keys.
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM users WHERE id = NEW.user_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM 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 = 'skills_per_user_limit';
|
||||
END IF;
|
||||
ELSIF NEW.organization_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM organizations WHERE id = NEW.organization_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM skills WHERE organization_id = NEW.organization_id;
|
||||
IF skill_count >= skill_limit THEN
|
||||
RAISE EXCEPTION 'organization has reached the skill limit'
|
||||
USING ERRCODE = 'check_violation',
|
||||
CONSTRAINT = 'skills_per_organization_limit';
|
||||
END IF;
|
||||
ELSIF NEW.project_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM chat_projects WHERE id = NEW.project_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM skills WHERE project_id = NEW.project_id;
|
||||
IF skill_count >= skill_limit THEN
|
||||
RAISE EXCEPTION 'project has reached the skill limit'
|
||||
USING ERRCODE = 'check_violation',
|
||||
CONSTRAINT = 'skills_per_project_limit';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION enforce_user_ai_budget_override_membership() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -1117,32 +1178,6 @@ BEGIN
|
||||
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
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -3135,6 +3170,29 @@ CREATE TABLE site_configs (
|
||||
value text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE skills (
|
||||
id uuid NOT NULL,
|
||||
user_id uuid,
|
||||
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,
|
||||
organization_id uuid,
|
||||
project_id uuid,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
CONSTRAINT skills_acl_only_on_organization_skills CHECK (((organization_id IS NOT NULL) OR ((group_acl = '{}'::jsonb) AND (user_acl = '{}'::jsonb)))),
|
||||
CONSTRAINT skills_content_size CHECK ((octet_length(content) <= 65536)),
|
||||
CONSTRAINT skills_description_size CHECK ((octet_length(description) <= 4096)),
|
||||
CONSTRAINT skills_group_acl_is_object CHECK ((jsonb_typeof(group_acl) = 'object'::text)),
|
||||
CONSTRAINT skills_name_format CHECK ((name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text)),
|
||||
CONSTRAINT skills_name_size CHECK ((octet_length(name) <= 256)),
|
||||
CONSTRAINT skills_single_owner CHECK ((num_nonnulls(user_id, organization_id, project_id) = 1)),
|
||||
CONSTRAINT skills_user_acl_is_object CHECK ((jsonb_typeof(user_acl) = 'object'::text))
|
||||
);
|
||||
|
||||
CREATE UNLOGGED TABLE tailnet_coordinators (
|
||||
id uuid NOT NULL,
|
||||
heartbeat_at timestamp with time zone NOT NULL
|
||||
@@ -3613,20 +3671,6 @@ CREATE TABLE user_secrets (
|
||||
CONSTRAINT user_secrets_enabled_requires_target CHECK (((NOT enabled) OR (env_name <> ''::text) OR (file_path <> ''::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 (
|
||||
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
||||
user_id uuid NOT NULL,
|
||||
@@ -4615,6 +4659,9 @@ ALTER TABLE ONLY provisioner_keys
|
||||
ALTER TABLE ONLY site_configs
|
||||
ADD CONSTRAINT site_configs_key_key UNIQUE (key);
|
||||
|
||||
ALTER TABLE ONLY skills
|
||||
ADD CONSTRAINT skills_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY tailnet_coordinators
|
||||
ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id);
|
||||
|
||||
@@ -4696,9 +4743,6 @@ ALTER TABLE ONLY user_links
|
||||
ALTER TABLE ONLY user_secrets
|
||||
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
|
||||
ADD CONSTRAINT user_status_changes_pkey PRIMARY KEY (id);
|
||||
|
||||
@@ -5073,6 +5117,12 @@ COMMENT ON INDEX provisioner_jobs_worker_id_organization_id_completed_at_idx IS
|
||||
|
||||
CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text));
|
||||
|
||||
CREATE UNIQUE INDEX skills_organization_id_name_idx ON skills USING btree (organization_id, name) WHERE (organization_id IS NOT NULL);
|
||||
|
||||
CREATE UNIQUE INDEX skills_project_id_name_idx ON skills USING btree (project_id, name) WHERE (project_id IS NOT NULL);
|
||||
|
||||
CREATE UNIQUE INDEX skills_user_id_name_idx ON skills USING btree (user_id, name);
|
||||
|
||||
CREATE INDEX template_usage_stats_start_time_idx ON template_usage_stats USING btree (start_time DESC);
|
||||
|
||||
COMMENT ON INDEX template_usage_stats_start_time_idx IS 'Index for querying MAX(start_time).';
|
||||
@@ -5091,8 +5141,6 @@ 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_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_username_lower_idx ON users USING btree (lower(username)) WHERE (deleted = false);
|
||||
@@ -5215,6 +5263,8 @@ CREATE TRIGGER trigger_delete_group_members_on_org_member_delete BEFORE DELETE O
|
||||
|
||||
CREATE TRIGGER trigger_delete_oauth2_provider_app_token AFTER DELETE ON oauth2_provider_app_tokens FOR EACH ROW EXECUTE FUNCTION delete_deleted_oauth2_provider_app_token_api_key();
|
||||
|
||||
CREATE TRIGGER trigger_delete_skill_user_acl_on_org_member_delete BEFORE DELETE ON organization_members FOR EACH ROW EXECUTE FUNCTION delete_skill_user_acl_on_org_member_delete();
|
||||
|
||||
CREATE TRIGGER trigger_delete_user_ai_budget_overrides_on_group_member_delete BEFORE DELETE ON group_members FOR EACH ROW EXECUTE FUNCTION delete_user_ai_budget_overrides_on_group_member_delete();
|
||||
|
||||
CREATE TRIGGER trigger_delete_user_ai_budget_overrides_on_org_member_delete BEFORE DELETE ON organization_members FOR EACH ROW EXECUTE FUNCTION delete_user_ai_budget_overrides_on_org_member_delete();
|
||||
@@ -5231,6 +5281,8 @@ CREATE TRIGGER trigger_set_chat_message_revision_on_insert BEFORE INSERT ON chat
|
||||
|
||||
CREATE TRIGGER trigger_set_chat_message_revision_on_update BEFORE UPDATE ON chat_messages FOR EACH ROW EXECUTE FUNCTION set_chat_message_revision_before();
|
||||
|
||||
CREATE TRIGGER trigger_skills_per_owner_limit BEFORE INSERT ON skills FOR EACH ROW EXECUTE FUNCTION enforce_skills_per_owner_limit();
|
||||
|
||||
CREATE TRIGGER trigger_sync_chat_retry_state BEFORE UPDATE OF retry_state, retry_state_version, generation_attempt ON chats FOR EACH ROW EXECUTE FUNCTION sync_chat_retry_state();
|
||||
|
||||
CREATE TRIGGER trigger_update_chat_history_after_message_insert AFTER INSERT ON chat_messages REFERENCING NEW TABLE AS chat_message_history_new_rows FOR EACH STATEMENT EXECUTE FUNCTION update_chat_history_after_message_insert();
|
||||
@@ -5239,16 +5291,14 @@ CREATE TRIGGER trigger_update_chat_history_after_message_update AFTER UPDATE ON
|
||||
|
||||
CREATE TRIGGER trigger_update_users AFTER INSERT OR UPDATE ON users FOR EACH ROW WHEN ((new.deleted = true)) EXECUTE FUNCTION delete_deleted_user_resources();
|
||||
|
||||
CREATE TRIGGER trigger_upsert_skills BEFORE INSERT OR UPDATE ON skills FOR EACH ROW WHEN ((new.user_id IS NOT NULL)) EXECUTE FUNCTION insert_user_skill_fail_if_user_deleted();
|
||||
|
||||
CREATE TRIGGER trigger_upsert_user_links BEFORE INSERT OR UPDATE ON user_links FOR EACH ROW EXECUTE FUNCTION insert_user_links_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_secrets_per_user_limits BEFORE INSERT OR UPDATE ON user_secrets FOR EACH ROW EXECUTE FUNCTION enforce_user_secrets_per_user_limits();
|
||||
|
||||
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 user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION record_user_status_change();
|
||||
@@ -5553,6 +5603,15 @@ ALTER TABLE ONLY provisioner_jobs
|
||||
ALTER TABLE ONLY provisioner_keys
|
||||
ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY skills
|
||||
ADD CONSTRAINT skills_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY skills
|
||||
ADD CONSTRAINT skills_project_id_fkey FOREIGN KEY (project_id) REFERENCES chat_projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY skills
|
||||
ADD CONSTRAINT skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY tailnet_peers
|
||||
ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -5637,9 +5696,6 @@ 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);
|
||||
|
||||
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
|
||||
ADD CONSTRAINT user_status_changes_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
|
||||
+3
-1
@@ -104,6 +104,9 @@ const (
|
||||
ForeignKeyProvisionerJobTimingsJobID ForeignKeyConstraint = "provisioner_job_timings_job_id_fkey" // ALTER TABLE ONLY provisioner_job_timings ADD CONSTRAINT provisioner_job_timings_job_id_fkey FOREIGN KEY (job_id) REFERENCES provisioner_jobs(id) ON DELETE CASCADE;
|
||||
ForeignKeyProvisionerJobsOrganizationID ForeignKeyConstraint = "provisioner_jobs_organization_id_fkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyProvisionerKeysOrganizationID ForeignKeyConstraint = "provisioner_keys_organization_id_fkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeySkillsOrganizationID ForeignKeyConstraint = "skills_organization_id_fkey" // ALTER TABLE ONLY skills ADD CONSTRAINT skills_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeySkillsProjectID ForeignKeyConstraint = "skills_project_id_fkey" // ALTER TABLE ONLY skills ADD CONSTRAINT skills_project_id_fkey FOREIGN KEY (project_id) REFERENCES chat_projects(id) ON DELETE CASCADE;
|
||||
ForeignKeySkillsUserID ForeignKeyConstraint = "skills_user_id_fkey" // ALTER TABLE ONLY skills ADD CONSTRAINT skills_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyTailnetPeersCoordinatorID ForeignKeyConstraint = "tailnet_peers_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
|
||||
ForeignKeyTailnetTunnelsCoordinatorID ForeignKeyConstraint = "tailnet_tunnels_coordinator_id_fkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_coordinator_id_fkey FOREIGN KEY (coordinator_id) REFERENCES tailnet_coordinators(id) ON DELETE CASCADE;
|
||||
ForeignKeyTemplateUsageStatsSessionStartTimeTemplateIDUser ForeignKeyConstraint = "template_usage_stats_session__start_time_template_id_user__fkey" // ALTER TABLE ONLY template_usage_stats_session_apps ADD CONSTRAINT template_usage_stats_session__start_time_template_id_user__fkey FOREIGN KEY (start_time, template_id, user_id) REFERENCES template_usage_stats(start_time, template_id, user_id) ON DELETE CASCADE;
|
||||
@@ -132,7 +135,6 @@ 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;
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
ForeignKeyWorkspaceAgentContextResourcesWorkspaceAgentID ForeignKeyConstraint = "workspace_agent_context_resources_workspace_agent_id_fkey" // ALTER TABLE ONLY workspace_agent_context_resources ADD CONSTRAINT workspace_agent_context_resources_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
-- resource_type and api_key_scope enum values cannot be dropped.
|
||||
|
||||
-- Only personal skills fit the user_skills shape.
|
||||
DELETE FROM skills WHERE user_id IS NULL;
|
||||
|
||||
DROP TRIGGER trigger_delete_skill_user_acl_on_org_member_delete ON organization_members;
|
||||
DROP FUNCTION delete_skill_user_acl_on_org_member_delete();
|
||||
|
||||
DROP TRIGGER trigger_upsert_skills ON skills;
|
||||
DROP TRIGGER trigger_skills_per_owner_limit ON skills;
|
||||
DROP FUNCTION enforce_skills_per_owner_limit();
|
||||
|
||||
DROP INDEX skills_project_id_name_idx;
|
||||
DROP INDEX skills_organization_id_name_idx;
|
||||
|
||||
ALTER TABLE skills
|
||||
DROP CONSTRAINT skills_acl_only_on_organization_skills,
|
||||
DROP CONSTRAINT skills_user_acl_is_object,
|
||||
DROP CONSTRAINT skills_group_acl_is_object,
|
||||
DROP CONSTRAINT skills_single_owner,
|
||||
DROP COLUMN user_acl,
|
||||
DROP COLUMN group_acl,
|
||||
DROP COLUMN enabled,
|
||||
DROP COLUMN project_id,
|
||||
DROP COLUMN organization_id,
|
||||
ALTER COLUMN user_id SET NOT NULL;
|
||||
|
||||
ALTER INDEX skills_user_id_name_idx RENAME TO user_skills_user_id_name_idx;
|
||||
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_content_size TO user_skills_content_size;
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_description_size TO user_skills_description_size;
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_name_format TO user_skills_name_format;
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_name_size TO user_skills_name_size;
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_user_id_fkey TO user_skills_user_id_fkey;
|
||||
ALTER TABLE skills RENAME CONSTRAINT skills_pkey TO user_skills_pkey;
|
||||
|
||||
ALTER TABLE skills RENAME TO user_skills;
|
||||
|
||||
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 FUNCTION enforce_user_skills_per_user_limit();
|
||||
|
||||
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 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 user AI provider keys.
|
||||
-- user_ai_provider_keys.user_id has ON DELETE CASCADE, but soft-delete
|
||||
-- does not remove the users row so the FK cascade never fires.
|
||||
DELETE FROM user_ai_provider_keys
|
||||
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;
|
||||
$$;
|
||||
@@ -0,0 +1,160 @@
|
||||
-- Generalize personal skills into one skills table owned by a user, an
|
||||
-- organization, or a project.
|
||||
ALTER TABLE user_skills RENAME TO skills;
|
||||
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_pkey TO skills_pkey;
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_user_id_fkey TO skills_user_id_fkey;
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_name_size TO skills_name_size;
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_name_format TO skills_name_format;
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_description_size TO skills_description_size;
|
||||
ALTER TABLE skills RENAME CONSTRAINT user_skills_content_size TO skills_content_size;
|
||||
|
||||
ALTER INDEX user_skills_user_id_name_idx RENAME TO skills_user_id_name_idx;
|
||||
|
||||
-- The per-user cap and the deleted-user guard are replaced below with
|
||||
-- owner-aware versions.
|
||||
DROP TRIGGER trigger_user_skills_per_user_limit ON skills;
|
||||
DROP FUNCTION enforce_user_skills_per_user_limit();
|
||||
DROP TRIGGER trigger_upsert_user_skills ON skills;
|
||||
|
||||
ALTER TABLE skills
|
||||
ALTER COLUMN user_id DROP NOT NULL,
|
||||
ADD COLUMN organization_id uuid REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
ADD COLUMN project_id uuid REFERENCES chat_projects(id) ON DELETE CASCADE,
|
||||
ADD COLUMN enabled boolean NOT NULL DEFAULT true,
|
||||
ADD COLUMN group_acl jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD COLUMN user_acl jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
ADD CONSTRAINT skills_single_owner CHECK (num_nonnulls(user_id, organization_id, project_id) = 1),
|
||||
ADD CONSTRAINT skills_group_acl_is_object CHECK (jsonb_typeof(group_acl) = 'object'),
|
||||
ADD CONSTRAINT skills_user_acl_is_object CHECK (jsonb_typeof(user_acl) = 'object'),
|
||||
ADD CONSTRAINT skills_acl_only_on_organization_skills CHECK (
|
||||
organization_id IS NOT NULL OR (group_acl = '{}'::jsonb AND user_acl = '{}'::jsonb)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX skills_organization_id_name_idx ON skills (organization_id, name) WHERE organization_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX skills_project_id_name_idx ON skills (project_id, name) WHERE project_id IS NOT NULL;
|
||||
|
||||
CREATE FUNCTION enforce_skills_per_owner_limit() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
skill_count int;
|
||||
skill_limit constant int := 100;
|
||||
BEGIN
|
||||
-- Serialize skill-cap checks per owner so concurrent inserts cannot all
|
||||
-- observe the same pre-insert count and exceed the hard limit. FOR NO KEY
|
||||
-- UPDATE does not block the KEY SHARE locks of unrelated foreign keys.
|
||||
IF NEW.user_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM users WHERE id = NEW.user_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM 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 = 'skills_per_user_limit';
|
||||
END IF;
|
||||
ELSIF NEW.organization_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM organizations WHERE id = NEW.organization_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM skills WHERE organization_id = NEW.organization_id;
|
||||
IF skill_count >= skill_limit THEN
|
||||
RAISE EXCEPTION 'organization has reached the skill limit'
|
||||
USING ERRCODE = 'check_violation',
|
||||
CONSTRAINT = 'skills_per_organization_limit';
|
||||
END IF;
|
||||
ELSIF NEW.project_id IS NOT NULL THEN
|
||||
PERFORM 1 FROM chat_projects WHERE id = NEW.project_id FOR NO KEY UPDATE;
|
||||
SELECT count(*) INTO skill_count FROM skills WHERE project_id = NEW.project_id;
|
||||
IF skill_count >= skill_limit THEN
|
||||
RAISE EXCEPTION 'project has reached the skill limit'
|
||||
USING ERRCODE = 'check_violation',
|
||||
CONSTRAINT = 'skills_per_project_limit';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trigger_skills_per_owner_limit
|
||||
BEFORE INSERT ON skills
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION enforce_skills_per_owner_limit();
|
||||
|
||||
CREATE TRIGGER trigger_upsert_skills
|
||||
BEFORE INSERT OR UPDATE ON skills
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.user_id IS NOT NULL)
|
||||
EXECUTE FUNCTION insert_user_skill_fail_if_user_deleted();
|
||||
|
||||
-- PL/pgSQL bodies are stored as text, so table renames do not reach them.
|
||||
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 user AI provider keys.
|
||||
-- user_ai_provider_keys.user_id has ON DELETE CASCADE, but soft-delete
|
||||
-- does not remove the users row so the FK cascade never fires.
|
||||
DELETE FROM user_ai_provider_keys
|
||||
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 personal skills.
|
||||
-- skills.user_id has ON DELETE CASCADE, but soft-delete
|
||||
-- does not remove the users row so the FK cascade never fires.
|
||||
DELETE FROM skills
|
||||
WHERE user_id = OLD.id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE FUNCTION delete_skill_user_acl_on_org_member_delete() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Drop the departing member's direct grants so adding them back to the
|
||||
-- organization does not silently restore access.
|
||||
UPDATE skills
|
||||
SET
|
||||
user_acl = user_acl - OLD.user_id::text,
|
||||
updated_at = now()
|
||||
WHERE organization_id = OLD.organization_id AND user_acl ? OLD.user_id::text;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trigger_delete_skill_user_acl_on_org_member_delete
|
||||
BEFORE DELETE ON organization_members
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION delete_skill_user_acl_on_org_member_delete();
|
||||
|
||||
ALTER TYPE resource_type ADD VALUE IF NOT EXISTS 'organization_skill';
|
||||
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:*';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'organization_skill:share';
|
||||
@@ -405,6 +405,7 @@ func TestMigrationChain(t *testing.T) {
|
||||
{"Migration000595RemoveTaskPermissions", 595, testMigration000595RemoveTaskPermissions},
|
||||
{"Migration000602RestoreAgentsAccessDefaultRole", 602, testMigration000602RestoreAgentsAccessDefaultRole},
|
||||
{"Migration000606ChatDiffStatusOriginCredentials", 606, testMigration000606ChatDiffStatusOriginCredentials},
|
||||
{"Migration000621Skills", 621, testMigration000621Skills},
|
||||
}
|
||||
for _, step := range steps {
|
||||
stepTo(step.version - 1)
|
||||
@@ -1841,6 +1842,188 @@ func testMigration000606ChatDiffStatusOriginCredentials(t *testing.T, sqlDB *sql
|
||||
}
|
||||
}
|
||||
|
||||
func testMigration000621Skills(t *testing.T, sqlDB *sql.DB, next migrationStepper) {
|
||||
const migrationVersion = 621
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
insertUser := func() uuid.UUID {
|
||||
t.Helper()
|
||||
id := uuid.New()
|
||||
_, err := sqlDB.ExecContext(ctx,
|
||||
"INSERT INTO users (id, username, email, hashed_password, created_at, updated_at) VALUES ($1, $2, $3, '', NOW(), NOW())",
|
||||
id, id.String(), id.String()+"@test.com")
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
type skillTable struct{ name, insert, exists string }
|
||||
userSkillsTable := skillTable{
|
||||
name: "user_skills",
|
||||
insert: "INSERT INTO user_skills (id, user_id, name, description, content) VALUES ($1, $2, $3, 'desc', 'body')",
|
||||
exists: "SELECT EXISTS (SELECT 1 FROM user_skills WHERE id = $1)",
|
||||
}
|
||||
skillsTable := skillTable{
|
||||
name: "skills",
|
||||
insert: "INSERT INTO skills (id, user_id, name, description, content) VALUES ($1, $2, $3, 'desc', 'body')",
|
||||
exists: "SELECT EXISTS (SELECT 1 FROM skills WHERE id = $1)",
|
||||
}
|
||||
insertSkill := func(table skillTable, userID uuid.UUID) uuid.UUID {
|
||||
t.Helper()
|
||||
id := uuid.New()
|
||||
_, err := sqlDB.ExecContext(ctx, table.insert, id, userID, "skill-"+id.String())
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
skillExists := func(table skillTable, id uuid.UUID) bool {
|
||||
t.Helper()
|
||||
var exists bool
|
||||
err := sqlDB.QueryRowContext(ctx, table.exists, id).Scan(&exists)
|
||||
require.NoError(t, err)
|
||||
return exists
|
||||
}
|
||||
// Soft deletion runs delete_deleted_user_resources, whose body must name
|
||||
// the current table.
|
||||
softDeleteRemovesSkills := func(table skillTable) {
|
||||
t.Helper()
|
||||
userID := insertUser()
|
||||
skillID := insertSkill(table, userID)
|
||||
_, err := sqlDB.ExecContext(ctx, "UPDATE users SET deleted = true WHERE id = $1", userID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, skillExists(table, skillID))
|
||||
}
|
||||
capFunction := func(table skillTable) string {
|
||||
t.Helper()
|
||||
var def string
|
||||
err := sqlDB.QueryRowContext(ctx, `
|
||||
SELECT pg_get_functiondef(tgfoid) FROM pg_trigger
|
||||
WHERE tgrelid = $1::regclass AND tgname LIKE 'trigger_%skills_per_%'`, table.name).Scan(&def)
|
||||
require.NoError(t, err)
|
||||
return def
|
||||
}
|
||||
userNullable := func(table skillTable) string {
|
||||
t.Helper()
|
||||
var nullable string
|
||||
err := sqlDB.QueryRowContext(ctx,
|
||||
"SELECT is_nullable FROM information_schema.columns WHERE table_name = $1 AND column_name = 'user_id'",
|
||||
table.name).Scan(&nullable)
|
||||
require.NoError(t, err)
|
||||
return nullable
|
||||
}
|
||||
|
||||
oldNames := []string{
|
||||
"trigger_upsert_user_skills:insert_user_skill_fail_if_user_deleted",
|
||||
"trigger_user_skills_per_user_limit:enforce_user_skills_per_user_limit",
|
||||
"user_skills_content_size",
|
||||
"user_skills_description_size",
|
||||
"user_skills_name_format",
|
||||
"user_skills_name_size",
|
||||
"user_skills_pkey",
|
||||
"user_skills_user_id_fkey",
|
||||
"user_skills_user_id_name_idx",
|
||||
}
|
||||
require.Equal(t, oldNames, skillsSchemaNames(ctx, t, sqlDB, userSkillsTable.name))
|
||||
oldCapFunction := capFunction(userSkillsTable)
|
||||
require.Equal(t, "NO", userNullable(userSkillsTable))
|
||||
|
||||
userID := insertUser()
|
||||
personalSkillID := insertSkill(userSkillsTable, userID)
|
||||
|
||||
version, _, err := next()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, migrationVersion, version)
|
||||
|
||||
require.Equal(t, []string{
|
||||
"skills_acl_only_on_organization_skills",
|
||||
"skills_content_size",
|
||||
"skills_description_size",
|
||||
"skills_group_acl_is_object",
|
||||
"skills_name_format",
|
||||
"skills_name_size",
|
||||
"skills_organization_id_fkey",
|
||||
"skills_organization_id_name_idx",
|
||||
"skills_pkey",
|
||||
"skills_project_id_fkey",
|
||||
"skills_project_id_name_idx",
|
||||
"skills_single_owner",
|
||||
"skills_user_acl_is_object",
|
||||
"skills_user_id_fkey",
|
||||
"skills_user_id_name_idx",
|
||||
"trigger_skills_per_owner_limit:enforce_skills_per_owner_limit",
|
||||
"trigger_upsert_skills:insert_user_skill_fail_if_user_deleted",
|
||||
}, skillsSchemaNames(ctx, t, sqlDB, skillsTable.name))
|
||||
require.Equal(t, "YES", userNullable(skillsTable))
|
||||
|
||||
var (
|
||||
gotUserID, orgID, projectID uuid.NullUUID
|
||||
name, description, content string
|
||||
enabled bool
|
||||
groupACL, userACL string
|
||||
)
|
||||
err = sqlDB.QueryRowContext(ctx,
|
||||
"SELECT user_id, organization_id, project_id, name, description, content, enabled, group_acl::text, user_acl::text FROM skills WHERE id = $1",
|
||||
personalSkillID).Scan(&gotUserID, &orgID, &projectID, &name, &description, &content, &enabled, &groupACL, &userACL)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uuid.NullUUID{UUID: userID, Valid: true}, gotUserID)
|
||||
require.False(t, orgID.Valid)
|
||||
require.False(t, projectID.Valid)
|
||||
require.Equal(t, "skill-"+personalSkillID.String(), name)
|
||||
require.Equal(t, "desc", description)
|
||||
require.Equal(t, "body", content)
|
||||
require.True(t, enabled)
|
||||
require.Equal(t, "{}", groupACL)
|
||||
require.Equal(t, "{}", userACL)
|
||||
softDeleteRemovesSkills(skillsTable)
|
||||
|
||||
org := uuid.New()
|
||||
_, err = sqlDB.ExecContext(ctx,
|
||||
"INSERT INTO organizations (id, name, display_name, description, created_at, updated_at, default_org_member_roles) VALUES ($1, $2, $2, '', NOW(), NOW(), '{}')",
|
||||
org, "org-"+org.String())
|
||||
require.NoError(t, err)
|
||||
orgSkillID := uuid.New()
|
||||
_, err = sqlDB.ExecContext(ctx,
|
||||
"INSERT INTO skills (id, organization_id, name, description, content) VALUES ($1, $2, 'org', 'desc', 'body')",
|
||||
orgSkillID, org)
|
||||
require.NoError(t, err)
|
||||
|
||||
downSQL, err := os.ReadFile("000621_skills.down.sql")
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, string(downSQL))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, oldNames, skillsSchemaNames(ctx, t, sqlDB, userSkillsTable.name))
|
||||
require.Equal(t, oldCapFunction, capFunction(userSkillsTable))
|
||||
require.Equal(t, "NO", userNullable(userSkillsTable))
|
||||
require.True(t, skillExists(userSkillsTable, personalSkillID))
|
||||
require.False(t, skillExists(userSkillsTable, orgSkillID))
|
||||
softDeleteRemovesSkills(userSkillsTable)
|
||||
|
||||
upSQL, err := os.ReadFile("000621_skills.up.sql")
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, string(upSQL))
|
||||
require.NoError(t, err)
|
||||
require.True(t, skillExists(skillsTable, personalSkillID))
|
||||
softDeleteRemovesSkills(skillsTable)
|
||||
}
|
||||
|
||||
// skillsSchemaNames lists a table's constraints, indexes, and triggers with
|
||||
// their functions.
|
||||
func skillsSchemaNames(ctx context.Context, t *testing.T, sqlDB *sql.DB, table string) []string {
|
||||
t.Helper()
|
||||
rows, err := sqlDB.QueryContext(ctx, `
|
||||
SELECT conname::text FROM pg_constraint WHERE conrelid = $1::regclass
|
||||
UNION SELECT indexname::text FROM pg_indexes WHERE tablename = $1::text
|
||||
UNION SELECT tgname::text || ':' || tgfoid::regproc::text FROM pg_trigger WHERE tgrelid = $1::regclass AND NOT tgisinternal
|
||||
ORDER BY 1`, table)
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
var names []string
|
||||
for rows.Next() {
|
||||
var name string
|
||||
require.NoError(t, rows.Scan(&name))
|
||||
names = append(names, name)
|
||||
}
|
||||
require.NoError(t, rows.Err())
|
||||
return names
|
||||
}
|
||||
|
||||
func TestMigration000504AIProvidersBackfill(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
INSERT INTO skills (
|
||||
id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
content,
|
||||
enabled,
|
||||
group_acl,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'7f070eb2-991e-4f7f-b780-40c4e0f49002',
|
||||
'bb640d07-ca8a-4869-b6bc-ae61ebb2fda1',
|
||||
'example-skill',
|
||||
'Example organization skill fixture.',
|
||||
'Example content.',
|
||||
false,
|
||||
'{"bb640d07-ca8a-4869-b6bc-ae61ebb2fda1": {"permissions": ["read"]}}',
|
||||
'2026-10-09 00:00:00+00',
|
||||
'2026-10-09 00:00:00+00'
|
||||
);
|
||||
@@ -958,8 +958,20 @@ func (m WorkspaceAgentVolumeResourceMonitor) Debounce(
|
||||
return m.DebouncedUntil, false
|
||||
}
|
||||
|
||||
func (s UserSkill) RBACObject() rbac.Object {
|
||||
return rbac.ResourceUserSkill.WithID(s.ID).WithOwner(s.UserID.String())
|
||||
func (s Skill) RBACObject() rbac.Object {
|
||||
if s.UserID.Valid {
|
||||
return rbac.ResourceUserSkill.WithID(s.ID).WithOwner(s.UserID.UUID.String())
|
||||
}
|
||||
if s.OrganizationID.Valid {
|
||||
return rbac.ResourceOrganizationSkill.
|
||||
WithID(s.ID).
|
||||
InOrg(s.OrganizationID.UUID).
|
||||
WithGroupACL(s.GroupACL.RBACACL()).
|
||||
WithACLUserList(s.UserACL.RBACACL())
|
||||
}
|
||||
// No role grants the empty resource type, so rows without an RBAC
|
||||
// resource stay unauthorizable for every user, including site owners.
|
||||
return rbac.Object{ID: s.ID.String()}
|
||||
}
|
||||
|
||||
func (s UserSecret) RBACObject() rbac.Object {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
@@ -270,3 +272,48 @@ func requireAllowAll(t *testing.T, s rbac.Scope) {
|
||||
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].ID)
|
||||
require.Equal(t, policy.WildcardSymbol, s.AllowIDList[0].Type)
|
||||
}
|
||||
|
||||
func TestSkillRBACObjectProjectRowsDenied(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orgID := uuid.New()
|
||||
skill := Skill{ID: uuid.New(), ProjectID: uuid.NullUUID{UUID: uuid.New(), Valid: true}}
|
||||
authz := rbac.NewAuthorizer(prometheus.NewRegistry())
|
||||
for name, roles := range map[string]rbac.RoleIdentifiers{
|
||||
"SiteOwner": {rbac.RoleOwner(), rbac.RoleMember()},
|
||||
"OrgAdmin": {rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID)},
|
||||
"Member": {rbac.RoleMember(), rbac.ScopedRoleAgentsAccess(orgID)},
|
||||
} {
|
||||
subject := rbac.Subject{ID: uuid.NewString(), Roles: roles, Scope: rbac.ScopeAll}
|
||||
for _, action := range []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} {
|
||||
err := authz.Authorize(context.Background(), subject, action, skill.RBACObject())
|
||||
require.Error(t, err, "%s %s", name, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillRBACObjectOrganizationACL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
orgID, groupID, userID := uuid.New(), uuid.New(), uuid.New()
|
||||
read := ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}}
|
||||
authz := rbac.NewAuthorizer(prometheus.NewRegistry())
|
||||
for name, tc := range map[string]struct {
|
||||
skill Skill
|
||||
groups []string
|
||||
}{
|
||||
"UserGrant": {skill: Skill{UserACL: ChatACL{userID.String(): read}}},
|
||||
"GroupGrant": {skill: Skill{GroupACL: ChatACL{groupID.String(): read}}, groups: []string{groupID.String()}},
|
||||
} {
|
||||
tc.skill.ID = uuid.New()
|
||||
tc.skill.OrganizationID = uuid.NullUUID{UUID: orgID, Valid: true}
|
||||
subject := rbac.Subject{
|
||||
ID: userID.String(),
|
||||
Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleAgentsAccess(orgID)},
|
||||
Groups: tc.groups,
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
require.NoError(t, authz.Authorize(context.Background(), subject, policy.ActionRead, tc.skill.RBACObject()), name)
|
||||
require.Error(t, authz.Authorize(context.Background(), subject, policy.ActionUpdate, tc.skill.RBACObject()), name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ type customQuerier interface {
|
||||
chatQuerier
|
||||
chatModelConfigQuerier
|
||||
mcpServerConfigQuerier
|
||||
organizationSkillQuerier
|
||||
}
|
||||
|
||||
type chatModelConfigQuerier interface {
|
||||
@@ -1348,3 +1349,49 @@ func (q *sqlQuerier) GetAuthorizedMCPServerConfigs(ctx context.Context, organiza
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
type organizationSkillQuerier interface {
|
||||
GetAuthorizedOrganizationSkillMetadata(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]ListOrganizationSkillMetadataByOrganizationIDRow, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedOrganizationSkillMetadata(ctx context.Context, organizationID uuid.UUID, prepared rbac.PreparedAuthorized) ([]ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
|
||||
VariableConverter: regosql.OrganizationSkillConverter(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
}
|
||||
|
||||
filtered, err := insertAuthorizedFilter(listOrganizationSkillMetadataByOrganizationID, fmt.Sprintf(" AND %s", authorizedFilter))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert authorized filter: %w", err)
|
||||
}
|
||||
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedOrganizationSkillMetadata :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListOrganizationSkillMetadataByOrganizationIDRow
|
||||
for rows.Next() {
|
||||
var i ListOrganizationSkillMetadataByOrganizationIDRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.OrganizationID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Enabled,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
Generated
+38
-12
@@ -539,6 +539,12 @@ const (
|
||||
ApiKeyScopeChatProjectMemoryCreate APIKeyScope = "chat_project_memory:create"
|
||||
ApiKeyScopeChatProjectMemoryRead APIKeyScope = "chat_project_memory:read"
|
||||
ApiKeyScopeChatProjectMemoryDelete APIKeyScope = "chat_project_memory:delete"
|
||||
ApiKeyScopeOrganizationSkill APIKeyScope = "organization_skill:*"
|
||||
ApiKeyScopeOrganizationSkillCreate APIKeyScope = "organization_skill:create"
|
||||
ApiKeyScopeOrganizationSkillRead APIKeyScope = "organization_skill:read"
|
||||
ApiKeyScopeOrganizationSkillUpdate APIKeyScope = "organization_skill:update"
|
||||
ApiKeyScopeOrganizationSkillDelete APIKeyScope = "organization_skill:delete"
|
||||
ApiKeyScopeOrganizationSkillShare APIKeyScope = "organization_skill:share"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -829,7 +835,13 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeChatProjectMemory,
|
||||
ApiKeyScopeChatProjectMemoryCreate,
|
||||
ApiKeyScopeChatProjectMemoryRead,
|
||||
ApiKeyScopeChatProjectMemoryDelete:
|
||||
ApiKeyScopeChatProjectMemoryDelete,
|
||||
ApiKeyScopeOrganizationSkill,
|
||||
ApiKeyScopeOrganizationSkillCreate,
|
||||
ApiKeyScopeOrganizationSkillRead,
|
||||
ApiKeyScopeOrganizationSkillUpdate,
|
||||
ApiKeyScopeOrganizationSkillDelete,
|
||||
ApiKeyScopeOrganizationSkillShare:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -1089,6 +1101,12 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeChatProjectMemoryCreate,
|
||||
ApiKeyScopeChatProjectMemoryRead,
|
||||
ApiKeyScopeChatProjectMemoryDelete,
|
||||
ApiKeyScopeOrganizationSkill,
|
||||
ApiKeyScopeOrganizationSkillCreate,
|
||||
ApiKeyScopeOrganizationSkillRead,
|
||||
ApiKeyScopeOrganizationSkillUpdate,
|
||||
ApiKeyScopeOrganizationSkillDelete,
|
||||
ApiKeyScopeOrganizationSkillShare,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3756,6 +3774,7 @@ const (
|
||||
ResourceTypeChatProjectMemory ResourceType = "chat_project_memory"
|
||||
ResourceTypeChatOrganizationSystemPrompt ResourceType = "chat_organization_system_prompt"
|
||||
ResourceTypeChatProjectInstructions ResourceType = "chat_project_instructions"
|
||||
ResourceTypeOrganizationSkill ResourceType = "organization_skill"
|
||||
)
|
||||
|
||||
func (e *ResourceType) Scan(src interface{}) error {
|
||||
@@ -3840,7 +3859,8 @@ func (e ResourceType) Valid() bool {
|
||||
ResourceTypeChatAutomation,
|
||||
ResourceTypeChatProjectMemory,
|
||||
ResourceTypeChatOrganizationSystemPrompt,
|
||||
ResourceTypeChatProjectInstructions:
|
||||
ResourceTypeChatProjectInstructions,
|
||||
ResourceTypeOrganizationSkill:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3894,6 +3914,7 @@ func AllResourceTypeValues() []ResourceType {
|
||||
ResourceTypeChatProjectMemory,
|
||||
ResourceTypeChatOrganizationSystemPrompt,
|
||||
ResourceTypeChatProjectInstructions,
|
||||
ResourceTypeOrganizationSkill,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6074,6 +6095,21 @@ type SiteConfig struct {
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
type Skill struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UserID uuid.NullUUID `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"`
|
||||
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||
ProjectID uuid.NullUUID `db:"project_id" json:"project_id"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
|
||||
UserACL ChatACL `db:"user_acl" json:"user_acl"`
|
||||
}
|
||||
|
||||
// We keep this separate from replicas in case we need to break the coordinator out into its own service
|
||||
type TailnetCoordinator struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
@@ -6495,16 +6531,6 @@ type UserSecret struct {
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
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
|
||||
type UserStatusChange struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
|
||||
Generated
+9
-4
@@ -238,6 +238,7 @@ type sqlcQuerier interface {
|
||||
DeleteOldWorkspaceAgentStats(ctx context.Context) error
|
||||
DeleteOldWorkspaceBuildOrchestrations(ctx context.Context, arg DeleteOldWorkspaceBuildOrchestrationsParams) (int64, error)
|
||||
DeleteOrganizationMember(ctx context.Context, arg DeleteOrganizationMemberParams) error
|
||||
DeleteOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg DeleteOrganizationSkillByOrganizationIDAndNameParams) (Skill, error)
|
||||
DeleteProvisionerKey(ctx context.Context, id uuid.UUID) error
|
||||
DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error
|
||||
DeleteRuntimeConfig(ctx context.Context, key string) error
|
||||
@@ -254,7 +255,7 @@ type sqlcQuerier interface {
|
||||
DeleteUserAIProviderKeysByProviderID(ctx context.Context, aiProviderID uuid.UUID) error
|
||||
DeleteUserChatCompactionThreshold(ctx context.Context, arg DeleteUserChatCompactionThresholdParams) error
|
||||
DeleteUserSecretByUserIDAndName(ctx context.Context, arg DeleteUserSecretByUserIDAndNameParams) (UserSecret, error)
|
||||
DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||
DeleteUserSkillByUserIDAndName(ctx context.Context, arg DeleteUserSkillByUserIDAndNameParams) (Skill, error)
|
||||
DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg DeleteWebpushSubscriptionByUserIDAndEndpointParams) error
|
||||
DeleteWebpushSubscriptions(ctx context.Context, ids []uuid.UUID) error
|
||||
DeleteWorkspaceACLByID(ctx context.Context, id uuid.UUID) error
|
||||
@@ -789,6 +790,7 @@ type sqlcQuerier interface {
|
||||
GetOrganizationGroupsAISpend(ctx context.Context, arg GetOrganizationGroupsAISpendParams) ([]GetOrganizationGroupsAISpendRow, error)
|
||||
GetOrganizationIDsByMemberIDs(ctx context.Context, ids []uuid.UUID) ([]GetOrganizationIDsByMemberIDsRow, error)
|
||||
GetOrganizationResourceCountByID(ctx context.Context, organizationID uuid.UUID) (GetOrganizationResourceCountByIDRow, error)
|
||||
GetOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg GetOrganizationSkillByOrganizationIDAndNameParams) (Skill, error)
|
||||
GetOrganizations(ctx context.Context, arg GetOrganizationsParams) ([]Organization, error)
|
||||
GetOrganizationsByUserID(ctx context.Context, arg GetOrganizationsByUserIDParams) ([]Organization, error)
|
||||
// GetOrganizationsWithPrebuildStatus returns organizations with prebuilds configured and their
|
||||
@@ -1057,7 +1059,7 @@ type sqlcQuerier interface {
|
||||
// values rather than interpolating between rows.
|
||||
GetUserSecretsTelemetrySummary(ctx context.Context) (GetUserSecretsTelemetrySummaryRow, error)
|
||||
GetUserShellToolDisplayMode(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||
GetUserSkillByUserIDAndName(ctx context.Context, arg GetUserSkillByUserIDAndNameParams) (Skill, error)
|
||||
// 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.
|
||||
GetUserStatusCounts(ctx context.Context, arg GetUserStatusCountsParams) ([]GetUserStatusCountsRow, error)
|
||||
@@ -1247,6 +1249,7 @@ type sqlcQuerier interface {
|
||||
InsertOAuth2ProviderAppToken(ctx context.Context, arg InsertOAuth2ProviderAppTokenParams) (OAuth2ProviderAppToken, error)
|
||||
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
|
||||
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
|
||||
InsertOrganizationSkill(ctx context.Context, arg InsertOrganizationSkillParams) (Skill, error)
|
||||
InsertPreset(ctx context.Context, arg InsertPresetParams) (TemplateVersionPreset, error)
|
||||
InsertPresetParameters(ctx context.Context, arg InsertPresetParametersParams) ([]TemplateVersionPresetParameter, error)
|
||||
InsertPresetPrebuildSchedule(ctx context.Context, arg InsertPresetPrebuildScheduleParams) (TemplateVersionPresetPrebuildSchedule, error)
|
||||
@@ -1276,7 +1279,7 @@ type sqlcQuerier interface {
|
||||
// If there is a conflict, the user is already a member
|
||||
InsertUserGroupsByID(ctx context.Context, arg InsertUserGroupsByIDParams) ([]uuid.UUID, error)
|
||||
InsertUserLink(ctx context.Context, arg InsertUserLinkParams) (UserLink, error)
|
||||
InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (UserSkill, error)
|
||||
InsertUserSkill(ctx context.Context, arg InsertUserSkillParams) (Skill, error)
|
||||
InsertVolumeResourceMonitor(ctx context.Context, arg InsertVolumeResourceMonitorParams) (WorkspaceAgentVolumeResourceMonitor, error)
|
||||
// Inserts or updates a webpush subscription. The (user_id, endpoint) pair
|
||||
// is unique; re-subscribing the same endpoint replaces the keys instead of
|
||||
@@ -1373,6 +1376,7 @@ type sqlcQuerier interface {
|
||||
// totals over every matching user. It must keep the same joins and predicates as
|
||||
// ExportOrganizationAISpend so both report the same token usage.
|
||||
ListOrganizationAISpendUsers(ctx context.Context, arg ListOrganizationAISpendUsersParams) ([]ListOrganizationAISpendUsersRow, error)
|
||||
ListOrganizationSkillMetadataByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]ListOrganizationSkillMetadataByOrganizationIDRow, error)
|
||||
ListProvisionerKeysByOrganization(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error)
|
||||
ListProvisionerKeysByOrganizationExcludeReserved(ctx context.Context, organizationID uuid.UUID) ([]ProvisionerKey, error)
|
||||
// Used by the usage generator to find missing heartbeat buckets.
|
||||
@@ -1644,6 +1648,7 @@ type sqlcQuerier interface {
|
||||
UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error)
|
||||
UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error)
|
||||
UpdateOrganizationDeletedByID(ctx context.Context, arg UpdateOrganizationDeletedByIDParams) error
|
||||
UpdateOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg UpdateOrganizationSkillByOrganizationIDAndNameParams) (Skill, error)
|
||||
UpdateOrganizationWorkspaceSharingSettings(ctx context.Context, arg UpdateOrganizationWorkspaceSharingSettingsParams) (Organization, error)
|
||||
// Cancels all pending provisioner jobs for prebuilt workspaces on a specific preset from an
|
||||
// inactive template version.
|
||||
@@ -1696,7 +1701,7 @@ type sqlcQuerier interface {
|
||||
UpdateUserRoles(ctx context.Context, arg UpdateUserRolesParams) (User, error)
|
||||
UpdateUserSecretByUserIDAndName(ctx context.Context, arg UpdateUserSecretByUserIDAndNameParams) (UserSecret, error)
|
||||
UpdateUserShellToolDisplayMode(ctx context.Context, arg UpdateUserShellToolDisplayModeParams) (string, error)
|
||||
UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (UserSkill, error)
|
||||
UpdateUserSkillByUserIDAndName(ctx context.Context, arg UpdateUserSkillByUserIDAndNameParams) (Skill, error)
|
||||
UpdateUserStatus(ctx context.Context, arg UpdateUserStatusParams) (User, error)
|
||||
UpdateUserTerminalFont(ctx context.Context, arg UpdateUserTerminalFontParams) (UserConfig, error)
|
||||
UpdateUserThemeDark(ctx context.Context, arg UpdateUserThemeDarkParams) (UserConfig, error)
|
||||
|
||||
Generated
+393
-171
@@ -28447,6 +28447,399 @@ func (q *sqlQuerier) UpsertWebpushVAPIDKeys(ctx context.Context, arg UpsertWebpu
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteOrganizationSkillByOrganizationIDAndName = `-- name: DeleteOrganizationSkillByOrganizationIDAndName :one
|
||||
DELETE FROM skills
|
||||
WHERE organization_id = $1::uuid AND name = $2
|
||||
RETURNING id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
type DeleteOrganizationSkillByOrganizationIDAndNameParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) DeleteOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg DeleteOrganizationSkillByOrganizationIDAndNameParams) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, deleteOrganizationSkillByOrganizationIDAndName, arg.OrganizationID, arg.Name)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteUserSkillByUserIDAndName = `-- name: DeleteUserSkillByUserIDAndName :one
|
||||
DELETE FROM skills
|
||||
WHERE user_id = $1::uuid AND name = $2
|
||||
RETURNING id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
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) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, deleteUserSkillByUserIDAndName, arg.UserID, arg.Name)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getOrganizationSkillByOrganizationIDAndName = `-- name: GetOrganizationSkillByOrganizationIDAndName :one
|
||||
SELECT id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
FROM skills
|
||||
WHERE organization_id = $1::uuid AND name = $2
|
||||
`
|
||||
|
||||
type GetOrganizationSkillByOrganizationIDAndNameParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg GetOrganizationSkillByOrganizationIDAndNameParams) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, getOrganizationSkillByOrganizationIDAndName, arg.OrganizationID, arg.Name)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserSkillByUserIDAndName = `-- name: GetUserSkillByUserIDAndName :one
|
||||
SELECT id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
FROM skills
|
||||
WHERE user_id = $1::uuid 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) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserSkillByUserIDAndName, arg.UserID, arg.Name)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertOrganizationSkill = `-- name: InsertOrganizationSkill :one
|
||||
INSERT INTO skills (id, organization_id, name, description, content, group_acl, user_acl)
|
||||
VALUES ($1::uuid, $2::uuid, $3::text, $4::text, $5::text, $6, $7)
|
||||
RETURNING id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
type InsertOrganizationSkillParams struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Content string `db:"content" json:"content"`
|
||||
GroupACL ChatACL `db:"group_acl" json:"group_acl"`
|
||||
UserACL ChatACL `db:"user_acl" json:"user_acl"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertOrganizationSkill(ctx context.Context, arg InsertOrganizationSkillParams) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertOrganizationSkill,
|
||||
arg.ID,
|
||||
arg.OrganizationID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Content,
|
||||
arg.GroupACL,
|
||||
arg.UserACL,
|
||||
)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertUserSkill = `-- name: InsertUserSkill :one
|
||||
INSERT INTO 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, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
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) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, insertUserSkill,
|
||||
arg.ID,
|
||||
arg.UserID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Content,
|
||||
)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listOrganizationSkillMetadataByOrganizationID = `-- name: ListOrganizationSkillMetadataByOrganizationID :many
|
||||
SELECT
|
||||
id, organization_id, name, description, enabled, created_at, updated_at
|
||||
FROM skills
|
||||
WHERE organization_id = $1::uuid
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedOrganizationSkillMetadata
|
||||
-- @authorize_filter
|
||||
ORDER BY name ASC
|
||||
`
|
||||
|
||||
type ListOrganizationSkillMetadataByOrganizationIDRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
Enabled bool `db:"enabled" json:"enabled"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ListOrganizationSkillMetadataByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]ListOrganizationSkillMetadataByOrganizationIDRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listOrganizationSkillMetadataByOrganizationID, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListOrganizationSkillMetadataByOrganizationIDRow
|
||||
for rows.Next() {
|
||||
var i ListOrganizationSkillMetadataByOrganizationIDRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.OrganizationID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Enabled,
|
||||
&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 listUserSkillMetadataByUserID = `-- name: ListUserSkillMetadataByUserID :many
|
||||
SELECT
|
||||
id, user_id, name, description, created_at, updated_at
|
||||
FROM skills
|
||||
WHERE user_id = $1::uuid
|
||||
ORDER BY name ASC
|
||||
`
|
||||
|
||||
type ListUserSkillMetadataByUserIDRow struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
UserID uuid.NullUUID `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 updateOrganizationSkillByOrganizationIDAndName = `-- name: UpdateOrganizationSkillByOrganizationIDAndName :one
|
||||
UPDATE skills
|
||||
SET
|
||||
description = COALESCE($1::text, description),
|
||||
content = COALESCE($2::text, content),
|
||||
enabled = COALESCE($3::boolean, enabled),
|
||||
updated_at = now()
|
||||
WHERE organization_id = $4::uuid AND name = $5
|
||||
RETURNING id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
type UpdateOrganizationSkillByOrganizationIDAndNameParams struct {
|
||||
Description sql.NullString `db:"description" json:"description"`
|
||||
Content sql.NullString `db:"content" json:"content"`
|
||||
Enabled sql.NullBool `db:"enabled" json:"enabled"`
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateOrganizationSkillByOrganizationIDAndName(ctx context.Context, arg UpdateOrganizationSkillByOrganizationIDAndNameParams) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateOrganizationSkillByOrganizationIDAndName,
|
||||
arg.Description,
|
||||
arg.Content,
|
||||
arg.Enabled,
|
||||
arg.OrganizationID,
|
||||
arg.Name,
|
||||
)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUserSkillByUserIDAndName = `-- name: UpdateUserSkillByUserIDAndName :one
|
||||
UPDATE skills
|
||||
SET
|
||||
description = $1,
|
||||
content = $2,
|
||||
updated_at = now()
|
||||
WHERE user_id = $3::uuid AND name = $4
|
||||
RETURNING id, user_id, name, description, content, created_at, updated_at, organization_id, project_id, enabled, group_acl, user_acl
|
||||
`
|
||||
|
||||
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) (Skill, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateUserSkillByUserIDAndName,
|
||||
arg.Description,
|
||||
arg.Content,
|
||||
arg.UserID,
|
||||
arg.Name,
|
||||
)
|
||||
var i Skill
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Content,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.OrganizationID,
|
||||
&i.ProjectID,
|
||||
&i.Enabled,
|
||||
&i.GroupACL,
|
||||
&i.UserACL,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const cleanTailnetCoordinators = `-- name: CleanTailnetCoordinators :exec
|
||||
DELETE
|
||||
FROM tailnet_coordinators
|
||||
@@ -32266,177 +32659,6 @@ func (q *sqlQuerier) UpdateUserSecretByUserIDAndName(ctx context.Context, arg Up
|
||||
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 allUserIDs = `-- name: AllUserIDs :many
|
||||
SELECT DISTINCT id FROM USERS
|
||||
WHERE CASE WHEN $1::bool THEN TRUE ELSE is_system = false END
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
-- name: InsertUserSkill :one
|
||||
INSERT INTO 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 skills
|
||||
WHERE user_id = @user_id::uuid AND name = @name;
|
||||
|
||||
-- name: ListUserSkillMetadataByUserID :many
|
||||
SELECT
|
||||
id, user_id, name, description, created_at, updated_at
|
||||
FROM skills
|
||||
WHERE user_id = @user_id::uuid
|
||||
ORDER BY name ASC;
|
||||
|
||||
-- name: UpdateUserSkillByUserIDAndName :one
|
||||
UPDATE skills
|
||||
SET
|
||||
description = @description,
|
||||
content = @content,
|
||||
updated_at = now()
|
||||
WHERE user_id = @user_id::uuid AND name = @name
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteUserSkillByUserIDAndName :one
|
||||
DELETE FROM skills
|
||||
WHERE user_id = @user_id::uuid AND name = @name
|
||||
RETURNING *;
|
||||
|
||||
-- name: InsertOrganizationSkill :one
|
||||
INSERT INTO skills (id, organization_id, name, description, content, group_acl, user_acl)
|
||||
VALUES (@id::uuid, @organization_id::uuid, @name::text, @description::text, @content::text, @group_acl, @user_acl)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetOrganizationSkillByOrganizationIDAndName :one
|
||||
SELECT *
|
||||
FROM skills
|
||||
WHERE organization_id = @organization_id::uuid AND name = @name;
|
||||
|
||||
-- name: ListOrganizationSkillMetadataByOrganizationID :many
|
||||
SELECT
|
||||
id, organization_id, name, description, enabled, created_at, updated_at
|
||||
FROM skills
|
||||
WHERE organization_id = @organization_id::uuid
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedOrganizationSkillMetadata
|
||||
-- @authorize_filter
|
||||
ORDER BY name ASC;
|
||||
|
||||
-- name: UpdateOrganizationSkillByOrganizationIDAndName :one
|
||||
UPDATE skills
|
||||
SET
|
||||
description = COALESCE(sqlc.narg('description')::text, description),
|
||||
content = COALESCE(sqlc.narg('content')::text, content),
|
||||
enabled = COALESCE(sqlc.narg('enabled')::boolean, enabled),
|
||||
updated_at = now()
|
||||
WHERE organization_id = @organization_id::uuid AND name = @name
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteOrganizationSkillByOrganizationIDAndName :one
|
||||
DELETE FROM skills
|
||||
WHERE organization_id = @organization_id::uuid AND name = @name
|
||||
RETURNING *;
|
||||
@@ -1,30 +0,0 @@
|
||||
-- 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 *;
|
||||
@@ -95,6 +95,12 @@ sql:
|
||||
- column: "mcp_server_configs.group_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "skills.user_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "skills.group_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
- column: "chats_expanded.user_acl"
|
||||
go_type:
|
||||
type: "ChatACL"
|
||||
|
||||
Generated
+4
-2
@@ -92,6 +92,7 @@ const (
|
||||
UniqueProvisionerJobsPkey UniqueConstraint = "provisioner_jobs_pkey" // ALTER TABLE ONLY provisioner_jobs ADD CONSTRAINT provisioner_jobs_pkey PRIMARY KEY (id);
|
||||
UniqueProvisionerKeysPkey UniqueConstraint = "provisioner_keys_pkey" // ALTER TABLE ONLY provisioner_keys ADD CONSTRAINT provisioner_keys_pkey PRIMARY KEY (id);
|
||||
UniqueSiteConfigsKeyKey UniqueConstraint = "site_configs_key_key" // ALTER TABLE ONLY site_configs ADD CONSTRAINT site_configs_key_key UNIQUE (key);
|
||||
UniqueSkillsPkey UniqueConstraint = "skills_pkey" // ALTER TABLE ONLY skills ADD CONSTRAINT skills_pkey PRIMARY KEY (id);
|
||||
UniqueTailnetCoordinatorsPkey UniqueConstraint = "tailnet_coordinators_pkey" // ALTER TABLE ONLY tailnet_coordinators ADD CONSTRAINT tailnet_coordinators_pkey PRIMARY KEY (id);
|
||||
UniqueTailnetPeersPkey UniqueConstraint = "tailnet_peers_pkey" // ALTER TABLE ONLY tailnet_peers ADD CONSTRAINT tailnet_peers_pkey PRIMARY KEY (id, coordinator_id);
|
||||
UniqueTailnetTunnelsPkey UniqueConstraint = "tailnet_tunnels_pkey" // ALTER TABLE ONLY tailnet_tunnels ADD CONSTRAINT tailnet_tunnels_pkey PRIMARY KEY (coordinator_id, src_id, dst_id);
|
||||
@@ -119,7 +120,6 @@ const (
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
@@ -177,13 +177,15 @@ const (
|
||||
UniqueNotificationMessagesDedupeHashIndex UniqueConstraint = "notification_messages_dedupe_hash_idx" // CREATE UNIQUE INDEX notification_messages_dedupe_hash_idx ON notification_messages USING btree (dedupe_hash);
|
||||
UniqueOrganizationsSingleDefaultOrg UniqueConstraint = "organizations_single_default_org" // CREATE UNIQUE INDEX organizations_single_default_org ON organizations USING btree (is_default) WHERE (is_default = true);
|
||||
UniqueProvisionerKeysOrganizationIDNameIndex UniqueConstraint = "provisioner_keys_organization_id_name_idx" // CREATE UNIQUE INDEX provisioner_keys_organization_id_name_idx ON provisioner_keys USING btree (organization_id, lower((name)::text));
|
||||
UniqueSkillsOrganizationIDNameIndex UniqueConstraint = "skills_organization_id_name_idx" // CREATE UNIQUE INDEX skills_organization_id_name_idx ON skills USING btree (organization_id, name) WHERE (organization_id IS NOT NULL);
|
||||
UniqueSkillsProjectIDNameIndex UniqueConstraint = "skills_project_id_name_idx" // CREATE UNIQUE INDEX skills_project_id_name_idx ON skills USING btree (project_id, name) WHERE (project_id IS NOT NULL);
|
||||
UniqueSkillsUserIDNameIndex UniqueConstraint = "skills_user_id_name_idx" // CREATE UNIQUE INDEX skills_user_id_name_idx ON skills USING btree (user_id, name);
|
||||
UniqueTemplateUsageStatsStartTimeTemplateIDUserIDIndex UniqueConstraint = "template_usage_stats_start_time_template_id_user_id_idx" // CREATE UNIQUE INDEX template_usage_stats_start_time_template_id_user_id_idx ON template_usage_stats USING btree (start_time, template_id, user_id);
|
||||
UniqueTemplatesOrganizationIDNameIndex UniqueConstraint = "templates_organization_id_name_idx" // CREATE UNIQUE INDEX templates_organization_id_name_idx ON templates USING btree (organization_id, lower((name)::text)) WHERE (deleted = false);
|
||||
UniqueUserLinksLinkedIDLoginTypeIndex UniqueConstraint = "user_links_linked_id_login_type_idx" // CREATE UNIQUE INDEX user_links_linked_id_login_type_idx ON user_links USING btree (linked_id, login_type) WHERE (linked_id <> ''::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);
|
||||
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));
|
||||
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);
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
package database_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/x/skills"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
@@ -22,7 +33,7 @@ func TestUserSkillSchemaConstants(t *testing.T) {
|
||||
_, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
||||
var triggerDef string
|
||||
err := sqlDB.QueryRowContext(ctx,
|
||||
`SELECT pg_get_functiondef('enforce_user_skills_per_user_limit'::regproc)`,
|
||||
`SELECT pg_get_functiondef('enforce_skills_per_owner_limit'::regproc)`,
|
||||
).Scan(&triggerDef)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, triggerDef, fmt.Sprintf(
|
||||
@@ -31,16 +42,16 @@ func TestUserSkillSchemaConstants(t *testing.T) {
|
||||
))
|
||||
|
||||
constraints := map[database.CheckConstraint]string{
|
||||
database.CheckUserSkillsNameSize: fmt.Sprintf(
|
||||
database.CheckSkillsNameSize: fmt.Sprintf(
|
||||
"octet_length(name) <= %d",
|
||||
skills.MaxPersonalSkillNameBytes,
|
||||
),
|
||||
database.CheckUserSkillsNameFormat: "name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text",
|
||||
database.CheckUserSkillsDescriptionSize: fmt.Sprintf(
|
||||
database.CheckSkillsNameFormat: "name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text",
|
||||
database.CheckSkillsDescriptionSize: fmt.Sprintf(
|
||||
"octet_length(description) <= %d",
|
||||
skills.MaxPersonalSkillDescriptionBytes,
|
||||
),
|
||||
database.CheckUserSkillsContentSize: fmt.Sprintf(
|
||||
database.CheckSkillsContentSize: fmt.Sprintf(
|
||||
"octet_length(content) <= %d",
|
||||
skills.MaxPersonalSkillSizeBytes,
|
||||
),
|
||||
@@ -60,3 +71,312 @@ func TestUserSkillSchemaConstants(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillOwners(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
|
||||
|
||||
type owners struct {
|
||||
user, org, project uuid.NullUUID
|
||||
}
|
||||
type skillRow struct {
|
||||
owners
|
||||
name string
|
||||
enabled bool
|
||||
groupACL, userACL string
|
||||
}
|
||||
insertSkill := func(ctx context.Context, q interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
}, row skillRow,
|
||||
) error {
|
||||
if row.name == "" {
|
||||
row.name = "skill-" + uuid.NewString()
|
||||
}
|
||||
if row.groupACL == "" {
|
||||
row.groupACL = "{}"
|
||||
}
|
||||
if row.userACL == "" {
|
||||
row.userACL = "{}"
|
||||
}
|
||||
_, err := q.ExecContext(ctx, `
|
||||
INSERT INTO skills (id, user_id, organization_id, project_id, name, description, content, enabled, group_acl, user_acl)
|
||||
VALUES ($1, $2, $3, $4, $5, 'desc', 'body', $6, $7::jsonb, $8::jsonb)`,
|
||||
uuid.New(), row.user, row.org, row.project, row.name, row.enabled, row.groupACL, row.userACL)
|
||||
return err
|
||||
}
|
||||
countSkills := func(ctx context.Context, column string, id uuid.UUID) int {
|
||||
var count int
|
||||
//nolint:gosec // column is a fixed owner column name from this test.
|
||||
err := sqlDB.QueryRowContext(ctx, "SELECT count(*) FROM skills WHERE "+column+" = $1", id).Scan(&count)
|
||||
require.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
valid := func(id uuid.UUID) uuid.NullUUID { return uuid.NullUUID{UUID: id, Valid: true} }
|
||||
newUser := func() uuid.UUID { return dbgen.User(t, db, database.User{}).ID }
|
||||
newOrg := func() uuid.UUID { return dbgen.Organization(t, db, database.Organization{}).ID }
|
||||
newProject := func() uuid.UUID {
|
||||
org := newOrg()
|
||||
return dbgen.ChatProject(t, db, database.ChatProject{OrganizationID: org, OwnerID: newUser()}).ID
|
||||
}
|
||||
|
||||
t.Run("ExactlyOneOwner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
user, org, project := valid(newUser()), valid(newOrg()), valid(newProject())
|
||||
for _, o := range []owners{
|
||||
{},
|
||||
{user: user, org: org},
|
||||
{org: org, project: project},
|
||||
{user: user, project: project},
|
||||
} {
|
||||
err := insertSkill(ctx, sqlDB, skillRow{owners: o, enabled: true})
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckSkillsSingleOwner), "owners %+v: %v", o, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ACLOnlyOnOrganizationSkills", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
grant := `{"` + uuid.NewString() + `": {"permissions": ["read"]}}`
|
||||
for _, o := range []owners{{user: valid(newUser())}, {project: valid(newProject())}} {
|
||||
err := insertSkill(ctx, sqlDB, skillRow{owners: o, groupACL: grant})
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckSkillsAclOnlyOnOrganizationSkills), "group ACL %+v: %v", o, err)
|
||||
err = insertSkill(ctx, sqlDB, skillRow{owners: o, userACL: grant})
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckSkillsAclOnlyOnOrganizationSkills), "user ACL %+v: %v", o, err)
|
||||
}
|
||||
|
||||
org := owners{org: valid(newOrg())}
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: org, groupACL: grant, userACL: grant}))
|
||||
err := insertSkill(ctx, sqlDB, skillRow{owners: org, groupACL: "[]"})
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckSkillsGroupAclIsObject), err)
|
||||
err = insertSkill(ctx, sqlDB, skillRow{owners: org, userACL: "[]"})
|
||||
require.True(t, database.IsCheckViolation(err, database.CheckSkillsUserAclIsObject), err)
|
||||
})
|
||||
|
||||
// TestUserSkillLimit and TestUserSkillLimitConcurrentCreates in coderd
|
||||
// cover the user branch of the cap trigger.
|
||||
t.Run("CapPerProject", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
full := owners{project: valid(newProject())}
|
||||
for range skills.MaxPersonalSkillsPerUser {
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: full, enabled: true}))
|
||||
}
|
||||
err := insertSkill(ctx, sqlDB, skillRow{owners: full, enabled: true})
|
||||
require.True(t, database.IsCheckViolation(err, "skills_per_project_limit"), err)
|
||||
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: owners{project: valid(newProject())}, enabled: true}))
|
||||
})
|
||||
|
||||
t.Run("ConcurrentOrganizationInsertsHonorCap", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
org := valid(newOrg())
|
||||
for i := range skills.MaxPersonalSkillsPerUser - 1 {
|
||||
// Disabled rows count toward the cap.
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: owners{org: org}, enabled: i%2 == 0}))
|
||||
}
|
||||
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
require.NoError(t, insertSkill(ctx, tx, skillRow{owners: owners{org: org}, enabled: true}))
|
||||
|
||||
// Foreign key checks from unrelated inserts take KEY SHARE on the
|
||||
// organization row, which the cap lock must not block.
|
||||
_, err = sqlDB.ExecContext(ctx, "SELECT 1 FROM organizations WHERE id = $1 FOR KEY SHARE NOWAIT", org.UUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
concurrent := make(chan error, 1)
|
||||
go func() {
|
||||
concurrent <- insertSkill(ctx, sqlDB, skillRow{owners: owners{org: org}, enabled: true})
|
||||
}()
|
||||
// Commit only after the concurrent insert waits on the cap lock, so
|
||||
// it cannot finish before the transaction by chance.
|
||||
require.Eventually(t, func() bool {
|
||||
if len(concurrent) > 0 {
|
||||
return true
|
||||
}
|
||||
var waiting bool
|
||||
err := sqlDB.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM pg_stat_activity
|
||||
WHERE datname = current_database() AND wait_event_type = 'Lock' AND query LIKE '%INSERT INTO skills%'
|
||||
)`).Scan(&waiting)
|
||||
return err == nil && waiting
|
||||
}, testutil.WaitShort, testutil.IntervalFast)
|
||||
require.NoError(t, tx.Commit())
|
||||
|
||||
err = testutil.RequireReceive(ctx, t, concurrent)
|
||||
require.True(t, database.IsCheckViolation(err, "skills_per_organization_limit"), err)
|
||||
require.Equal(t, skills.MaxPersonalSkillsPerUser, countSkills(ctx, "organization_id", org.UUID))
|
||||
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: owners{org: valid(newOrg())}, enabled: true}))
|
||||
})
|
||||
|
||||
t.Run("NamesUniquePerOwner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
const name = "shared-name"
|
||||
user, org, otherOrg, project := valid(newUser()), valid(newOrg()), valid(newOrg()), valid(newProject())
|
||||
for _, o := range []owners{{user: user}, {org: org}, {org: otherOrg}, {project: project}} {
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: o, name: name}))
|
||||
}
|
||||
|
||||
err := insertSkill(ctx, sqlDB, skillRow{owners: owners{user: user}, name: name})
|
||||
require.True(t, database.IsUniqueViolation(err, database.UniqueSkillsUserIDNameIndex), err)
|
||||
err = insertSkill(ctx, sqlDB, skillRow{owners: owners{org: org}, name: name})
|
||||
require.True(t, database.IsUniqueViolation(err, database.UniqueSkillsOrganizationIDNameIndex), err)
|
||||
err = insertSkill(ctx, sqlDB, skillRow{owners: owners{project: project}, name: name})
|
||||
require.True(t, database.IsUniqueViolation(err, database.UniqueSkillsProjectIDNameIndex), err)
|
||||
})
|
||||
|
||||
t.Run("MemberRemovalDropsDirectGrants", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
org, otherOrg := newOrg(), newOrg()
|
||||
removed, kept := newUser(), newUser()
|
||||
for _, o := range []uuid.UUID{org, otherOrg} {
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{OrganizationID: o, UserID: removed})
|
||||
grants := fmt.Sprintf(`{%q: {"permissions": ["read"]}, %q: {"permissions": ["read"]}}`, removed, kept)
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: owners{org: valid(o)}, userACL: grants}))
|
||||
}
|
||||
|
||||
require.NoError(t, db.DeleteOrganizationMember(ctx, database.DeleteOrganizationMemberParams{OrganizationID: org, UserID: removed}))
|
||||
|
||||
hasGrant := func(org, user uuid.UUID) bool {
|
||||
t.Helper()
|
||||
var has bool
|
||||
err := sqlDB.QueryRowContext(ctx, "SELECT user_acl ? $2 FROM skills WHERE organization_id = $1", org, user.String()).Scan(&has)
|
||||
require.NoError(t, err)
|
||||
return has
|
||||
}
|
||||
require.False(t, hasGrant(org, removed))
|
||||
require.True(t, hasGrant(org, kept))
|
||||
require.True(t, hasGrant(otherOrg, removed))
|
||||
})
|
||||
|
||||
t.Run("OwnerDeletionRemovesOnlyOwnRows", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
user, deletedUser := newUser(), newUser()
|
||||
org, deletedOrg := newOrg(), newOrg()
|
||||
project := dbgen.ChatProject(t, db, database.ChatProject{OrganizationID: org, OwnerID: deletedUser}).ID
|
||||
deletedProject := dbgen.ChatProject(t, db, database.ChatProject{OrganizationID: org, OwnerID: deletedUser}).ID
|
||||
for _, o := range []owners{
|
||||
{user: valid(user)},
|
||||
{user: valid(deletedUser)},
|
||||
{org: valid(org)},
|
||||
{org: valid(deletedOrg)},
|
||||
{project: valid(project)},
|
||||
{project: valid(deletedProject)},
|
||||
} {
|
||||
require.NoError(t, insertSkill(ctx, sqlDB, skillRow{owners: o}))
|
||||
}
|
||||
|
||||
_, err := sqlDB.ExecContext(ctx, "UPDATE users SET deleted = true WHERE id = $1", deletedUser)
|
||||
require.NoError(t, err)
|
||||
_, err = sqlDB.ExecContext(ctx, "DELETE FROM organizations WHERE id = $1", deletedOrg)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.DeleteChatProjectByID(ctx, deletedProject))
|
||||
|
||||
require.Equal(t, 1, countSkills(ctx, "user_id", user))
|
||||
require.Equal(t, 0, countSkills(ctx, "user_id", deletedUser))
|
||||
require.Equal(t, 1, countSkills(ctx, "organization_id", org))
|
||||
require.Equal(t, 0, countSkills(ctx, "organization_id", deletedOrg))
|
||||
// The soft-deleted user's project keeps its skills.
|
||||
require.Equal(t, 1, countSkills(ctx, "project_id", project))
|
||||
require.Equal(t, 0, countSkills(ctx, "project_id", deletedProject))
|
||||
})
|
||||
}
|
||||
|
||||
// TestOrganizationSkillQueries runs the organization skill queries that only
|
||||
// execute against Postgres here: the SQL-filtered list, whose regosql
|
||||
// converter must agree with the Rego ACL rules, and the update and delete
|
||||
// statements.
|
||||
func TestOrganizationSkillQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
authzDB := dbauthz.New(db, rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry()), slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
|
||||
|
||||
org := dbgen.Organization(t, db, database.Organization{})
|
||||
otherOrg := dbgen.Organization(t, db, database.Organization{})
|
||||
group := dbgen.Group(t, db, database.Group{OrganizationID: org.ID})
|
||||
newUser := func(siteRoles []string, orgID uuid.UUID, orgRoles ...string) uuid.UUID {
|
||||
user := dbgen.User(t, db, database.User{RBACRoles: siteRoles})
|
||||
if orgID != uuid.Nil {
|
||||
dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: orgID, Roles: orgRoles})
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
var (
|
||||
siteOwner = newUser([]string{rbac.RoleOwner().String()}, uuid.Nil)
|
||||
orgAdmin = newUser(nil, org.ID, rbac.RoleOrgAdmin())
|
||||
groupMember = newUser(nil, org.ID)
|
||||
grantedMember = newUser(nil, org.ID)
|
||||
grantedOutside = newUser(nil, uuid.Nil)
|
||||
)
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{UserID: groupMember, GroupID: group.ID})
|
||||
|
||||
read := database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}}
|
||||
orgSkill := func(groupACL, userACL database.ChatACL) database.Skill {
|
||||
return dbgen.OrganizationSkill(t, db, database.Skill{
|
||||
OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true},
|
||||
GroupACL: groupACL,
|
||||
UserACL: userACL,
|
||||
})
|
||||
}
|
||||
everyoneSkill := orgSkill(database.ChatACL{org.ID.String(): read}, nil)
|
||||
groupSkill := orgSkill(database.ChatACL{group.ID.String(): read}, nil)
|
||||
userSkill := orgSkill(nil, database.ChatACL{grantedMember.String(): read, grantedOutside.String(): read})
|
||||
privateSkill := orgSkill(nil, nil)
|
||||
dbgen.OrganizationSkill(t, db, database.Skill{
|
||||
OrganizationID: uuid.NullUUID{UUID: otherOrg.ID, Valid: true},
|
||||
GroupACL: database.ChatACL{otherOrg.ID.String(): read},
|
||||
})
|
||||
|
||||
as := func(userID uuid.UUID) context.Context {
|
||||
subject, _, err := httpmw.UserRBACSubject(ctx, db, userID, rbac.ExpandableScope(rbac.ScopeAll))
|
||||
require.NoError(t, err)
|
||||
return dbauthz.As(ctx, subject)
|
||||
}
|
||||
all := []uuid.UUID{everyoneSkill.ID, groupSkill.ID, userSkill.ID, privateSkill.ID}
|
||||
for name, tc := range map[string]struct {
|
||||
user uuid.UUID
|
||||
want []uuid.UUID
|
||||
}{
|
||||
"SiteOwner": {siteOwner, all},
|
||||
"OrgAdmin": {orgAdmin, all},
|
||||
"GroupMember": {groupMember, []uuid.UUID{everyoneSkill.ID, groupSkill.ID}},
|
||||
"GrantedMember": {grantedMember, []uuid.UUID{everyoneSkill.ID, userSkill.ID}},
|
||||
"GrantedOutside": {grantedOutside, nil},
|
||||
} {
|
||||
rows, err := authzDB.ListOrganizationSkillMetadataByOrganizationID(as(tc.user), org.ID)
|
||||
require.NoError(t, err, name)
|
||||
got := make([]uuid.UUID, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
got = append(got, row.ID)
|
||||
}
|
||||
require.ElementsMatch(t, tc.want, got, name)
|
||||
}
|
||||
|
||||
updated, err := authzDB.UpdateOrganizationSkillByOrganizationIDAndName(as(orgAdmin), database.UpdateOrganizationSkillByOrganizationIDAndNameParams{
|
||||
OrganizationID: org.ID, Name: groupSkill.Name, Enabled: sql.NullBool{Bool: false, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, updated.Enabled)
|
||||
require.Equal(t, groupSkill.Content, updated.Content)
|
||||
deleted, err := authzDB.DeleteOrganizationSkillByOrganizationIDAndName(as(orgAdmin), database.DeleteOrganizationSkillByOrganizationIDAndNameParams{
|
||||
OrganizationID: org.ID, Name: groupSkill.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, groupSkill.ID, deleted.ID)
|
||||
}
|
||||
|
||||
@@ -335,6 +335,17 @@ var (
|
||||
Type: "organization_member",
|
||||
}
|
||||
|
||||
// ResourceOrganizationSkill
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create an organization skill
|
||||
// - "ActionDelete" :: delete an organization skill
|
||||
// - "ActionRead" :: read organization skill metadata and content
|
||||
// - "ActionShare" :: share an organization skill with other users or groups
|
||||
// - "ActionUpdate" :: update an organization skill
|
||||
ResourceOrganizationSkill = Object{
|
||||
Type: "organization_skill",
|
||||
}
|
||||
|
||||
// ResourcePrebuiltWorkspace
|
||||
// Valid Actions
|
||||
// - "ActionDelete" :: delete prebuilt workspace
|
||||
@@ -565,6 +576,7 @@ func AllResources() []Objecter {
|
||||
ResourceOauth2AppSecret,
|
||||
ResourceOrganization,
|
||||
ResourceOrganizationMember,
|
||||
ResourceOrganizationSkill,
|
||||
ResourcePrebuiltWorkspace,
|
||||
ResourceProvisionerDaemon,
|
||||
ResourceProvisionerJobs,
|
||||
|
||||
@@ -107,6 +107,14 @@ var chatModelConfigActions = map[Action]ActionDefinition{
|
||||
ActionShare: "share a chat model config with other users or groups",
|
||||
}
|
||||
|
||||
var organizationSkillActions = map[Action]ActionDefinition{
|
||||
ActionCreate: "create an organization skill",
|
||||
ActionRead: "read organization skill metadata and content",
|
||||
ActionUpdate: "update an organization skill",
|
||||
ActionDelete: "delete an organization skill",
|
||||
ActionShare: "share an organization skill with other users or groups",
|
||||
}
|
||||
|
||||
// RBACPermissions is indexed by the type
|
||||
var RBACPermissions = map[string]PermissionDefinition{
|
||||
// Wildcard is every object, and the action "*" provides all actions.
|
||||
@@ -273,6 +281,9 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
ActionDelete: "delete member",
|
||||
},
|
||||
},
|
||||
"organization_skill": {
|
||||
Actions: organizationSkillActions,
|
||||
},
|
||||
"debug_info": {
|
||||
Actions: map[Action]ActionDefinition{
|
||||
ActionRead: "access to debug routes",
|
||||
|
||||
@@ -87,6 +87,21 @@ func MCPServerConfigConverter() *sqltypes.VariableConverter {
|
||||
return matcher
|
||||
}
|
||||
|
||||
// OrganizationSkillConverter compiles organization skill authorization into
|
||||
// a filter over the skills table.
|
||||
func OrganizationSkillConverter() *sqltypes.VariableConverter {
|
||||
matcher := sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
sqltypes.StringVarMatcher("skills.organization_id :: text", []string{"input", "object", "org_owner"}),
|
||||
sqltypes.AlwaysFalse(userOwnerMatcher()),
|
||||
)
|
||||
matcher.RegisterMatcher(
|
||||
ACLMappingMatcher(matcher, "skills.group_acl", []string{"input", "object", "acl_group_list"}).UsingSubfield("permissions"),
|
||||
ACLMappingMatcher(matcher, "skills.user_acl", []string{"input", "object", "acl_user_list"}).UsingSubfield("permissions"),
|
||||
)
|
||||
return matcher
|
||||
}
|
||||
|
||||
func chatBaseConverter() *sqltypes.VariableConverter {
|
||||
return sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
chatResourceIDMatcher(),
|
||||
|
||||
@@ -475,9 +475,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
// Allow auditors to query deployment stats and insights.
|
||||
ResourceDeploymentStats.Type: {policy.ActionRead},
|
||||
ResourceDeploymentConfig.Type: {policy.ActionRead},
|
||||
ResourceChatModelConfig.Type: {policy.ActionRead},
|
||||
ResourceDeploymentStats.Type: {policy.ActionRead},
|
||||
ResourceDeploymentConfig.Type: {policy.ActionRead},
|
||||
ResourceChatModelConfig.Type: {policy.ActionRead},
|
||||
ResourceOrganizationSkill.Type: {policy.ActionRead},
|
||||
// Allow auditors to query AI Bridge interceptions.
|
||||
ResourceAibridgeInterception.Type: {policy.ActionRead},
|
||||
// Allow auditors to read boundary logs.
|
||||
@@ -614,6 +615,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ResourceOrganization.Type: {policy.ActionRead},
|
||||
ResourceOrganizationMember.Type: {policy.ActionRead},
|
||||
ResourceChatModelConfig.Type: {policy.ActionRead},
|
||||
ResourceOrganizationSkill.Type: {policy.ActionRead},
|
||||
}),
|
||||
Member: []Permission{},
|
||||
},
|
||||
|
||||
@@ -1561,6 +1561,66 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OrganizationSkillRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOrganizationSkill.WithID(uuid.New()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, auditor, orgAuditor},
|
||||
false: {
|
||||
memberMe, orgMemberMe, orgWorkspaceAccessUser, orgAgentsAccessUser,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
otherOrgAdmin, otherOrgAuditor,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OrganizationSkillCreateUpdateDeleteShare",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
|
||||
Resource: rbac.ResourceOrganizationSkill.WithID(uuid.New()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {
|
||||
memberMe, orgMemberMe, orgWorkspaceAccessUser, orgAgentsAccessUser,
|
||||
auditor, orgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
userAdmin, orgUserAdmin, otherOrgUserAdmin,
|
||||
otherOrgAdmin, otherOrgAuditor,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// memberMe shares orgMemberMe's ID but is not an org member, so
|
||||
// the user grant does not apply to it.
|
||||
Name: "OrganizationSkillUserACLRead",
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOrganizationSkill.WithID(uuid.New()).InOrg(orgID).WithACLUserList(map[string][]policy.Action{
|
||||
currentUser.String(): {policy.ActionRead},
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, auditor, orgAuditor, orgMemberMe, orgWorkspaceAccessUser},
|
||||
false: {
|
||||
memberMe, orgUserAdmin, orgTemplateAdmin,
|
||||
otherOrgAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
templateAdmin, userAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "OrganizationSkillUserACLWrite",
|
||||
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
|
||||
Resource: rbac.ResourceOrganizationSkill.WithID(uuid.New()).InOrg(orgID).WithACLUserList(map[string][]policy.Action{
|
||||
currentUser.String(): {policy.ActionRead},
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {
|
||||
orgMemberMe, orgWorkspaceAccessUser, memberMe, auditor, orgAuditor,
|
||||
orgUserAdmin, orgTemplateAdmin, setOtherOrg, templateAdmin, userAdmin,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ChatModelConfigCreateUpdateDeleteShare",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionShare},
|
||||
|
||||
@@ -71,6 +71,14 @@ var externalLowLevel = map[ScopeName]struct{}{
|
||||
"user_skill:delete": {},
|
||||
"user_skill:*": {},
|
||||
|
||||
// Organization skills
|
||||
"organization_skill:read": {},
|
||||
"organization_skill:create": {},
|
||||
"organization_skill:update": {},
|
||||
"organization_skill:delete": {},
|
||||
"organization_skill:share": {},
|
||||
"organization_skill:*": {},
|
||||
|
||||
// Organizations
|
||||
"organization:read": {},
|
||||
"organization:update": {},
|
||||
|
||||
@@ -117,6 +117,11 @@ const (
|
||||
ScopeOrganizationMemberDelete ScopeName = "organization_member:delete"
|
||||
ScopeOrganizationMemberRead ScopeName = "organization_member:read"
|
||||
ScopeOrganizationMemberUpdate ScopeName = "organization_member:update"
|
||||
ScopeOrganizationSkillCreate ScopeName = "organization_skill:create"
|
||||
ScopeOrganizationSkillDelete ScopeName = "organization_skill:delete"
|
||||
ScopeOrganizationSkillRead ScopeName = "organization_skill:read"
|
||||
ScopeOrganizationSkillShare ScopeName = "organization_skill:share"
|
||||
ScopeOrganizationSkillUpdate ScopeName = "organization_skill:update"
|
||||
ScopePrebuiltWorkspaceDelete ScopeName = "prebuilt_workspace:delete"
|
||||
ScopePrebuiltWorkspaceUpdate ScopeName = "prebuilt_workspace:update"
|
||||
ScopeProvisionerDaemonCreate ScopeName = "provisioner_daemon:create"
|
||||
@@ -318,6 +323,11 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeOrganizationMemberDelete,
|
||||
ScopeOrganizationMemberRead,
|
||||
ScopeOrganizationMemberUpdate,
|
||||
ScopeOrganizationSkillCreate,
|
||||
ScopeOrganizationSkillDelete,
|
||||
ScopeOrganizationSkillRead,
|
||||
ScopeOrganizationSkillShare,
|
||||
ScopeOrganizationSkillUpdate,
|
||||
ScopePrebuiltWorkspaceDelete,
|
||||
ScopePrebuiltWorkspaceUpdate,
|
||||
ScopeProvisionerDaemonCreate,
|
||||
@@ -520,6 +530,11 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeOrganizationMemberDelete,
|
||||
ScopeOrganizationMemberRead,
|
||||
ScopeOrganizationMemberUpdate,
|
||||
ScopeOrganizationSkillCreate,
|
||||
ScopeOrganizationSkillDelete,
|
||||
ScopeOrganizationSkillRead,
|
||||
ScopeOrganizationSkillShare,
|
||||
ScopeOrganizationSkillUpdate,
|
||||
ScopePrebuiltWorkspaceDelete,
|
||||
ScopePrebuiltWorkspaceUpdate,
|
||||
ScopeProvisionerDaemonCreate,
|
||||
|
||||
@@ -31,8 +31,8 @@ const (
|
||||
// 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"
|
||||
skillsPerUserLimitConstraint database.CheckConstraint = "skills_per_user_limit"
|
||||
userSkillUserDeletedConstraint database.CheckConstraint = "user_skill_user_deleted"
|
||||
)
|
||||
|
||||
// @Summary Create a user skill
|
||||
@@ -51,7 +51,7 @@ func (api *API) postUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx = r.Context()
|
||||
user = httpmw.UserParam(r)
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||
aReq, commitAudit = audit.InitRequest[database.Skill](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
@@ -92,11 +92,11 @@ func (api *API) postUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if database.IsCheckViolation(err, userSkillsPerUserLimitConstraint) {
|
||||
if database.IsCheckViolation(err, skillsPerUserLimitConstraint) {
|
||||
writeUserSkillLimitReached(ctx, rw)
|
||||
return
|
||||
}
|
||||
if database.IsUniqueViolation(err, database.UniqueUserSkillsUserIDNameIndex) {
|
||||
if database.IsUniqueViolation(err, database.UniqueSkillsUserIDNameIndex) {
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "A skill with that name already exists.",
|
||||
Detail: err.Error(),
|
||||
@@ -186,7 +186,7 @@ func (api *API) patchUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||
user = httpmw.UserParam(r)
|
||||
name = chi.URLParam(r, "skillName")
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||
aReq, commitAudit = audit.InitRequest[database.Skill](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
@@ -221,8 +221,8 @@ func (api *API) patchUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var (
|
||||
skill database.UserSkill
|
||||
oldSkill database.UserSkill
|
||||
skill database.Skill
|
||||
oldSkill database.Skill
|
||||
)
|
||||
err = api.Database.InTx(func(tx database.Store) error {
|
||||
fetched, err := tx.GetUserSkillByUserIDAndName(ctx, database.GetUserSkillByUserIDAndNameParams{
|
||||
@@ -281,7 +281,7 @@ func (api *API) deleteUserSkill(rw http.ResponseWriter, r *http.Request) {
|
||||
user = httpmw.UserParam(r)
|
||||
name = chi.URLParam(r, "skillName")
|
||||
auditor = api.Auditor.Load()
|
||||
aReq, commitAudit = audit.InitRequest[database.UserSkill](rw, &audit.RequestParams{
|
||||
aReq, commitAudit = audit.InitRequest[database.Skill](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
|
||||
@@ -474,7 +474,7 @@ func TestUserSkillDatabaseConstraints(t *testing.T) {
|
||||
Description: "Invalid",
|
||||
Content: userSkillMarkdown("not kebab", "Invalid", "Body."),
|
||||
},
|
||||
constraint: database.CheckUserSkillsNameFormat,
|
||||
constraint: database.CheckSkillsNameFormat,
|
||||
},
|
||||
{
|
||||
name: "NameSize",
|
||||
@@ -485,7 +485,7 @@ func TestUserSkillDatabaseConstraints(t *testing.T) {
|
||||
Description: "Invalid",
|
||||
Content: userSkillMarkdown("too-long-name", "Invalid", "Body."),
|
||||
},
|
||||
constraint: database.CheckUserSkillsNameSize,
|
||||
constraint: database.CheckSkillsNameSize,
|
||||
},
|
||||
{
|
||||
name: "ContentSize",
|
||||
@@ -496,7 +496,7 @@ func TestUserSkillDatabaseConstraints(t *testing.T) {
|
||||
Description: "Invalid",
|
||||
Content: strings.Repeat("a", skills.MaxPersonalSkillSizeBytes+1),
|
||||
},
|
||||
constraint: database.CheckUserSkillsContentSize,
|
||||
constraint: database.CheckSkillsContentSize,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -521,14 +521,14 @@ func TestUserSkillSchemaConstants(t *testing.T) {
|
||||
var triggerDef string
|
||||
require.NoError(t, sqlDB.QueryRowContext(
|
||||
ctx,
|
||||
`SELECT pg_get_functiondef('enforce_user_skills_per_user_limit'::regproc)`,
|
||||
`SELECT pg_get_functiondef('enforce_skills_per_owner_limit'::regproc)`,
|
||||
).Scan(&triggerDef))
|
||||
assert.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.CheckUserSkillsContentSize: fmt.Sprintf("octet_length(content) <= %d", skills.MaxPersonalSkillSizeBytes),
|
||||
database.CheckSkillsNameSize: fmt.Sprintf("octet_length(name) <= %d", skills.MaxPersonalSkillNameBytes),
|
||||
database.CheckSkillsNameFormat: "name ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text",
|
||||
database.CheckSkillsContentSize: fmt.Sprintf("octet_length(content) <= %d", skills.MaxPersonalSkillSizeBytes),
|
||||
}
|
||||
for constraint, expected := range constraints {
|
||||
t.Run(string(constraint), func(t *testing.T) {
|
||||
|
||||
@@ -2078,7 +2078,7 @@ func TestFetchPersonalSkillMetadata(t *testing.T) {
|
||||
requireUserSkillContextActor(ctx, t, userID)
|
||||
require.Equal(t, userID, gotUserID)
|
||||
return []database.ListUserSkillMetadataByUserIDRow{{
|
||||
UserID: userID,
|
||||
UserID: uuid.NullUUID{UUID: userID, Valid: true},
|
||||
Name: "personal-review",
|
||||
Description: "Personal review process",
|
||||
}}, nil
|
||||
@@ -2126,11 +2126,11 @@ func TestLoadPersonalSkillBody(t *testing.T) {
|
||||
}
|
||||
|
||||
db.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), params).DoAndReturn(
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
requireUserSkillContextActor(ctx, t, userID)
|
||||
require.Equal(t, params, gotParams)
|
||||
return database.UserSkill{
|
||||
UserID: userID,
|
||||
return database.Skill{
|
||||
UserID: uuid.NullUUID{UUID: userID, Valid: true},
|
||||
Name: "personal-review",
|
||||
Content: "---\nname: personal-review\ndescription: Personal review process\n---\n\nUpdated instructions.\n",
|
||||
}, nil
|
||||
@@ -2158,10 +2158,10 @@ func TestLoadPersonalSkillBody(t *testing.T) {
|
||||
}
|
||||
|
||||
db.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), params).DoAndReturn(
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
requireUserSkillContextActor(ctx, t, userID)
|
||||
require.Equal(t, params, gotParams)
|
||||
return database.UserSkill{}, sql.ErrNoRows
|
||||
return database.Skill{}, sql.ErrNoRows
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2184,10 +2184,10 @@ func TestLoadPersonalSkillBody(t *testing.T) {
|
||||
dbErr := xerrors.New("database unavailable")
|
||||
|
||||
db.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), params).DoAndReturn(
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
requireUserSkillContextActor(ctx, t, userID)
|
||||
require.Equal(t, params, gotParams)
|
||||
return database.UserSkill{}, dbErr
|
||||
return database.Skill{}, dbErr
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2216,11 +2216,11 @@ func TestLoadPersonalSkillBody(t *testing.T) {
|
||||
}
|
||||
|
||||
db.EXPECT().GetUserSkillByUserIDAndName(gomock.Any(), params).DoAndReturn(
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.UserSkill, error) {
|
||||
func(ctx context.Context, gotParams database.GetUserSkillByUserIDAndNameParams) (database.Skill, error) {
|
||||
requireUserSkillContextActor(ctx, t, userID)
|
||||
require.Equal(t, params, gotParams)
|
||||
return database.UserSkill{
|
||||
UserID: userID,
|
||||
return database.Skill{
|
||||
UserID: uuid.NullUUID{UUID: userID, Valid: true},
|
||||
Name: "broken-skill",
|
||||
Content: "---\nname: broken-skill\ndescription: Broken\n---\n\n \n",
|
||||
}, nil
|
||||
|
||||
@@ -160,6 +160,12 @@ const (
|
||||
APIKeyScopeOrganizationMemberDelete APIKeyScope = "organization_member:delete"
|
||||
APIKeyScopeOrganizationMemberRead APIKeyScope = "organization_member:read"
|
||||
APIKeyScopeOrganizationMemberUpdate APIKeyScope = "organization_member:update"
|
||||
APIKeyScopeOrganizationSkillAll APIKeyScope = "organization_skill:*"
|
||||
APIKeyScopeOrganizationSkillCreate APIKeyScope = "organization_skill:create"
|
||||
APIKeyScopeOrganizationSkillDelete APIKeyScope = "organization_skill:delete"
|
||||
APIKeyScopeOrganizationSkillRead APIKeyScope = "organization_skill:read"
|
||||
APIKeyScopeOrganizationSkillShare APIKeyScope = "organization_skill:share"
|
||||
APIKeyScopeOrganizationSkillUpdate APIKeyScope = "organization_skill:update"
|
||||
APIKeyScopePrebuiltWorkspaceAll APIKeyScope = "prebuilt_workspace:*"
|
||||
APIKeyScopePrebuiltWorkspaceDelete APIKeyScope = "prebuilt_workspace:delete"
|
||||
APIKeyScopePrebuiltWorkspaceUpdate APIKeyScope = "prebuilt_workspace:update"
|
||||
@@ -287,6 +293,12 @@ var PublicAPIKeyScopes = []APIKeyScope{
|
||||
APIKeyScopeOrganizationDelete,
|
||||
APIKeyScopeOrganizationRead,
|
||||
APIKeyScopeOrganizationUpdate,
|
||||
APIKeyScopeOrganizationSkillAll,
|
||||
APIKeyScopeOrganizationSkillCreate,
|
||||
APIKeyScopeOrganizationSkillDelete,
|
||||
APIKeyScopeOrganizationSkillRead,
|
||||
APIKeyScopeOrganizationSkillShare,
|
||||
APIKeyScopeOrganizationSkillUpdate,
|
||||
APIKeyScopeTemplateAll,
|
||||
APIKeyScopeTemplateCreate,
|
||||
APIKeyScopeTemplateDelete,
|
||||
|
||||
@@ -64,6 +64,7 @@ const (
|
||||
ResourceTypeChatAutomation ResourceType = "chat_automation"
|
||||
ResourceTypeUserSecret ResourceType = "user_secret"
|
||||
ResourceTypeUserSkill ResourceType = "user_skill"
|
||||
ResourceTypeOrganizationSkill ResourceType = "organization_skill"
|
||||
ResourceTypeChatInstructionSettings ResourceType = "chat_instruction_settings"
|
||||
ResourceTypeChatOperationalSettings ResourceType = "chat_operational_settings"
|
||||
ResourceTypeChatOrganizationSystemPrompt ResourceType = "chat_organization_system_prompt"
|
||||
@@ -157,6 +158,8 @@ func (r ResourceType) FriendlyString() string {
|
||||
return "user secret"
|
||||
case ResourceTypeUserSkill:
|
||||
return "user skill"
|
||||
case ResourceTypeOrganizationSkill:
|
||||
return "organization skill"
|
||||
case ResourceTypeChatInstructionSettings:
|
||||
return "chat instruction settings"
|
||||
case ResourceTypeChatOperationalSettings:
|
||||
|
||||
@@ -40,6 +40,7 @@ const (
|
||||
ResourceOauth2AppSecret RBACResource = "oauth2_app_secret"
|
||||
ResourceOrganization RBACResource = "organization"
|
||||
ResourceOrganizationMember RBACResource = "organization_member"
|
||||
ResourceOrganizationSkill RBACResource = "organization_skill"
|
||||
ResourcePrebuiltWorkspace RBACResource = "prebuilt_workspace"
|
||||
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
||||
ResourceProvisionerJobs RBACResource = "provisioner_jobs"
|
||||
@@ -122,6 +123,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
|
||||
ResourceOauth2AppSecret: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOrganization: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOrganizationMember: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceOrganizationSkill: {ActionCreate, ActionDelete, ActionRead, ActionShare, ActionUpdate},
|
||||
ResourcePrebuiltWorkspace: {ActionDelete, ActionUpdate},
|
||||
ResourceProvisionerDaemon: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
|
||||
ResourceProvisionerJobs: {ActionCreate, ActionRead, ActionUpdate},
|
||||
|
||||
@@ -649,6 +649,28 @@ Actions: `create`, `write`, `delete`
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Skill
|
||||
|
||||
Actions: `create`, `write`, `delete`
|
||||
|
||||
<table width="100%">
|
||||
<thead><tr><th width="75%">Field</th><th width="25%">Tracked</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>content</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>created_at</code></td><td>No</td></tr>
|
||||
<tr><td><code>description</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>enabled</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>group_acl</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>id</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>name</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>organization_id</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>project_id</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>updated_at</code></td><td>No</td></tr>
|
||||
<tr><td><code>user_acl</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>user_id</code></td><td>Yes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### Template
|
||||
|
||||
Actions: `write`, `delete`
|
||||
@@ -777,23 +799,6 @@ Actions: `create`, `write`, `delete`
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### UserSkill
|
||||
|
||||
Actions: `create`, `write`, `delete`
|
||||
|
||||
<table width="100%">
|
||||
<thead><tr><th width="75%">Field</th><th width="25%">Tracked</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>content</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>created_at</code></td><td>No</td></tr>
|
||||
<tr><td><code>description</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>id</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>name</code></td><td>Yes</td></tr>
|
||||
<tr><td><code>updated_at</code></td><td>No</td></tr>
|
||||
<tr><td><code>user_id</code></td><td>Yes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### WorkspaceBuild
|
||||
|
||||
Actions: `start`, `stop`
|
||||
|
||||
Generated
+11
@@ -137,6 +137,17 @@ The `resource:*` form grants every action on that resource, including actions no
|
||||
| `organization:read` | Read organizations. |
|
||||
| `organization:update` | Update an organization. |
|
||||
|
||||
### `organization_skill`
|
||||
|
||||
| Scope | Description |
|
||||
|-----------------------------|----------------------------------------------------------------------------------|
|
||||
| `organization_skill:*` | Every action on `organization_skill`, including actions not listed on this page. |
|
||||
| `organization_skill:create` | Create an organization skill. |
|
||||
| `organization_skill:delete` | Delete an organization skill. |
|
||||
| `organization_skill:read` | Read organization skill metadata and content. |
|
||||
| `organization_skill:share` | Share an organization skill with other users or groups. |
|
||||
| `organization_skill:update` | Update an organization skill. |
|
||||
|
||||
### `template`
|
||||
|
||||
| Scope | Description |
|
||||
|
||||
Generated
+20
-20
@@ -198,10 +198,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -331,10 +331,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -464,10 +464,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -559,10 +559,10 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
@@ -965,9 +965,9 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| 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` |
|
||||
| `resource_type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
Generated
+9
-9
File diff suppressed because one or more lines are too long
Generated
+5
-5
@@ -819,11 +819,11 @@ Status Code **200**
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `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`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| `login_type` | `github`, `oidc`, `password`, `token` |
|
||||
| `scope` | `all`, `application_connect` |
|
||||
| Property | Value(s) |
|
||||
|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `type` | `*`, `ai_gateway_key`, `ai_model_price`, `ai_provider`, `ai_seat`, `aibridge_interception`, `api_key`, `assign_org_role`, `assign_role`, `audit_log`, `boundary_log`, `boundary_usage`, `chat`, `chat_model_config`, `chat_project`, `chat_project_memory`, `connection_log`, `crypto_key`, `debug_info`, `deployment_config`, `deployment_stats`, `file`, `group`, `group_member`, `idpsync_settings`, `inbox_notification`, `license`, `mcp_server_config`, `notification_message`, `notification_preference`, `notification_template`, `oauth2_app`, `oauth2_app_code_token`, `oauth2_app_secret`, `organization`, `organization_member`, `organization_skill`, `prebuilt_workspace`, `provisioner_daemon`, `provisioner_jobs`, `replicas`, `system`, `tailnet_coordinator`, `template`, `usage_event`, `user`, `user_secret`, `user_skill`, `webpush_subscription`, `workspace`, `workspace_agent_devcontainers`, `workspace_agent_resource_monitor`, `workspace_build_orchestration`, `workspace_dormant`, `workspace_proxy` |
|
||||
| `login_type` | `github`, `oidc`, `password`, `token` |
|
||||
| `scope` | `all`, `application_connect` |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
||||
@@ -360,17 +360,17 @@ func Test_diff(t *testing.T) {
|
||||
// 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{
|
||||
left: audit.Empty[database.Skill](),
|
||||
right: database.Skill{
|
||||
ID: uuid.UUID{1},
|
||||
UserID: uuid.UUID{2},
|
||||
UserID: uuid.NullUUID{UUID: uuid.UUID{2}, Valid: true},
|
||||
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()},
|
||||
"user_id": audit.OldNew{Old: "null", 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"},
|
||||
|
||||
@@ -40,7 +40,7 @@ var AuditActionMap = map[string][]codersdk.AuditAction{
|
||||
"ChatModelConfig": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"MCPServerConfig": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"UserSecret": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"UserSkill": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"Skill": {codersdk.AuditActionCreate, codersdk.AuditActionWrite, codersdk.AuditActionDelete},
|
||||
"ChatInstructionSettings": {codersdk.AuditActionWrite},
|
||||
"ChatOperationalSettings": {codersdk.AuditActionWrite},
|
||||
"ChatOrganizationSystemPrompt": {codersdk.AuditActionWrite},
|
||||
@@ -590,14 +590,19 @@ var auditableResourcesTypes = map[any]map[string]Action{
|
||||
"oauth2_revocation_url": ActionTrack,
|
||||
"organization_id": ActionIgnore,
|
||||
},
|
||||
&database.UserSkill{}: {
|
||||
"id": ActionTrack,
|
||||
"user_id": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"description": ActionTrack,
|
||||
"content": ActionTrack,
|
||||
"created_at": ActionIgnore,
|
||||
"updated_at": ActionIgnore,
|
||||
&database.Skill{}: {
|
||||
"id": ActionTrack,
|
||||
"user_id": ActionTrack,
|
||||
"organization_id": ActionTrack,
|
||||
"project_id": ActionTrack,
|
||||
"name": ActionTrack,
|
||||
"description": ActionTrack,
|
||||
"content": ActionTrack,
|
||||
"enabled": ActionTrack,
|
||||
"group_acl": ActionTrack,
|
||||
"user_acl": ActionTrack,
|
||||
"created_at": ActionIgnore,
|
||||
"updated_at": ActionIgnore,
|
||||
},
|
||||
&database.ChatOperationalSettings{}: {
|
||||
"id": ActionIgnore,
|
||||
|
||||
@@ -188,6 +188,13 @@ export const RBACResourceActions: Partial<
|
||||
read: "read member",
|
||||
update: "update an organization member",
|
||||
},
|
||||
organization_skill: {
|
||||
create: "create an organization skill",
|
||||
delete: "delete an organization skill",
|
||||
read: "read organization skill metadata and content",
|
||||
share: "share an organization skill with other users or groups",
|
||||
update: "update an organization skill",
|
||||
},
|
||||
prebuilt_workspace: {
|
||||
delete: "delete prebuilt workspace",
|
||||
update: "update prebuilt workspace settings",
|
||||
|
||||
Generated
+16
@@ -862,6 +862,12 @@ export type APIKeyScope =
|
||||
| "organization_member:read"
|
||||
| "organization_member:update"
|
||||
| "organization:read"
|
||||
| "organization_skill:*"
|
||||
| "organization_skill:create"
|
||||
| "organization_skill:delete"
|
||||
| "organization_skill:read"
|
||||
| "organization_skill:share"
|
||||
| "organization_skill:update"
|
||||
| "organization:update"
|
||||
| "prebuilt_workspace:*"
|
||||
| "prebuilt_workspace:delete"
|
||||
@@ -1118,6 +1124,12 @@ export const APIKeyScopes: APIKeyScope[] = [
|
||||
"organization_member:read",
|
||||
"organization_member:update",
|
||||
"organization:read",
|
||||
"organization_skill:*",
|
||||
"organization_skill:create",
|
||||
"organization_skill:delete",
|
||||
"organization_skill:read",
|
||||
"organization_skill:share",
|
||||
"organization_skill:update",
|
||||
"organization:update",
|
||||
"prebuilt_workspace:*",
|
||||
"prebuilt_workspace:delete",
|
||||
@@ -8812,6 +8824,7 @@ export type RBACResource =
|
||||
| "oauth2_app_secret"
|
||||
| "organization"
|
||||
| "organization_member"
|
||||
| "organization_skill"
|
||||
| "prebuilt_workspace"
|
||||
| "provisioner_daemon"
|
||||
| "provisioner_jobs"
|
||||
@@ -8868,6 +8881,7 @@ export const RBACResources: RBACResource[] = [
|
||||
"oauth2_app_secret",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"organization_skill",
|
||||
"prebuilt_workspace",
|
||||
"provisioner_daemon",
|
||||
"provisioner_jobs",
|
||||
@@ -9027,6 +9041,7 @@ export type ResourceType =
|
||||
| "oauth2_provider_settings"
|
||||
| "organization"
|
||||
| "organization_member"
|
||||
| "organization_skill"
|
||||
| "prebuilds_settings"
|
||||
| "task"
|
||||
| "template"
|
||||
@@ -9075,6 +9090,7 @@ export const ResourceTypes: ResourceType[] = [
|
||||
"oauth2_provider_settings",
|
||||
"organization",
|
||||
"organization_member",
|
||||
"organization_skill",
|
||||
"prebuilds_settings",
|
||||
"task",
|
||||
"template",
|
||||
|
||||
Reference in New Issue
Block a user