mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add chat sharing API (#24968)
This commit is contained in:
Generated
+221
@@ -467,6 +467,88 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/acl": {
|
||||
"get": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Chats"
|
||||
],
|
||||
"summary": "Get chat ACLs",
|
||||
"operationId": "get-chat-acls",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chat ID",
|
||||
"name": "chat",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ChatACL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Chats"
|
||||
],
|
||||
"summary": "Update chat ACL",
|
||||
"operationId": "update-chat-acl",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chat ID",
|
||||
"name": "chat",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update chat ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateChatACL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/diff": {
|
||||
"get": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
@@ -16211,6 +16293,12 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"owner_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_username": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_chat_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -16247,6 +16335,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ChatGroup"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ChatUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatBusyBehavior": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -16452,6 +16557,61 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ReducedUser"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"quota_allowance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"enum": [
|
||||
"read"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/codersdk.GroupSource"
|
||||
},
|
||||
"total_member_count": {
|
||||
"description": "How many members are in this group. Shows the total count,\neven if the user is not authorized to read group member details.\nMay be greater than ` + "`" + `len(Group.Members)` + "`" + `.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatInputPart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16905,6 +17065,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatRole": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"read",
|
||||
""
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ChatRoleRead",
|
||||
"ChatRoleDeleted"
|
||||
]
|
||||
},
|
||||
"codersdk.ChatStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -17065,6 +17236,39 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatUser": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"username"
|
||||
],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"enum": [
|
||||
"read"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatWatchEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23994,6 +24198,23 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateChatACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateChatRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+200
@@ -408,6 +408,80 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/acl": {
|
||||
"get": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Chats"],
|
||||
"summary": "Get chat ACLs",
|
||||
"operationId": "get-chat-acls",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chat ID",
|
||||
"name": "chat",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.ChatACL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
"consumes": ["application/json"],
|
||||
"tags": ["Chats"],
|
||||
"summary": "Update chat ACL",
|
||||
"operationId": "update-chat-acl",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Chat ID",
|
||||
"name": "chat",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Update chat ACL request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpdateChatACL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"x-apidocgen": {
|
||||
"skip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/experimental/chats/{chat}/diff": {
|
||||
"get": {
|
||||
"description": "Experimental: this endpoint is subject to change.",
|
||||
@@ -14585,6 +14659,12 @@
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"owner_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"owner_username": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent_chat_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -14621,6 +14701,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ChatGroup"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ChatUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatBusyBehavior": {
|
||||
"type": "string",
|
||||
"enum": ["queue", "interrupt"],
|
||||
@@ -14814,6 +14911,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatGroup": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"members": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.ReducedUser"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"organization_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"quota_allowance": {
|
||||
"type": "integer"
|
||||
},
|
||||
"role": {
|
||||
"enum": ["read"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/codersdk.GroupSource"
|
||||
},
|
||||
"total_member_count": {
|
||||
"description": "How many members are in this group. Shows the total count,\neven if the user is not authorized to read group member details.\nMay be greater than `len(Group.Members)`.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatInputPart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15250,6 +15400,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatRole": {
|
||||
"type": "string",
|
||||
"enum": ["read", ""],
|
||||
"x-enum-varnames": ["ChatRoleRead", "ChatRoleDeleted"]
|
||||
},
|
||||
"codersdk.ChatStatus": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -15410,6 +15565,34 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatUser": {
|
||||
"type": "object",
|
||||
"required": ["id", "username"],
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"enum": ["read"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
]
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ChatWatchEvent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22068,6 +22251,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateChatACL": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"group_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/codersdk.ChatRole"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpdateChatRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1298,6 +1298,10 @@ func New(options *Options) *API {
|
||||
})
|
||||
r.Route("/{chat}", func(r chi.Router) {
|
||||
r.Use(httpmw.ExtractChatParam(options.Database))
|
||||
r.Route("/acl", func(r chi.Router) {
|
||||
r.Get("/", api.getChatACL)
|
||||
r.Patch("/", api.patchChatACL)
|
||||
})
|
||||
r.Get("/", api.getChat)
|
||||
r.Patch("/", api.patchChat)
|
||||
r.Get("/messages", api.getChatMessages)
|
||||
|
||||
@@ -976,6 +976,13 @@ func WorkspaceRoleActions(role codersdk.WorkspaceRole) []policy.Action {
|
||||
return []policy.Action{}
|
||||
}
|
||||
|
||||
func ChatRoleActions(role codersdk.ChatRole) []policy.Action {
|
||||
if role == codersdk.ChatRoleRead {
|
||||
return []policy.Action{policy.ActionRead}
|
||||
}
|
||||
return []policy.Action{}
|
||||
}
|
||||
|
||||
func ConnectionLogConnectionTypeFromAgentProtoConnectionType(typ agentproto.Connection_Type) (database.ConnectionType, error) {
|
||||
switch typ {
|
||||
case agentproto.Connection_SSH:
|
||||
@@ -1739,6 +1746,8 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
|
||||
ID: c.ID,
|
||||
OrganizationID: c.OrganizationID,
|
||||
OwnerID: c.OwnerID,
|
||||
OwnerUsername: c.OwnerUsername,
|
||||
OwnerName: c.OwnerName,
|
||||
LastModelConfigID: c.LastModelConfigID,
|
||||
Title: c.Title,
|
||||
Status: codersdk.ChatStatus(c.Status),
|
||||
|
||||
@@ -930,6 +930,8 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
|
||||
input := database.Chat{
|
||||
ID: uuid.New(),
|
||||
OwnerID: uuid.New(),
|
||||
OwnerUsername: "owner-username",
|
||||
OwnerName: "Owner Name",
|
||||
OrganizationID: uuid.New(),
|
||||
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
slog "cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/acl"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/util/slice"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
//
|
||||
// @Summary Get chat ACLs
|
||||
// @ID get-chat-acls
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Chats
|
||||
// @Produce json
|
||||
// @Param chat path string true "Chat ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.ChatACL
|
||||
// @Router /api/experimental/chats/{chat}/acl [get]
|
||||
// @x-apidocgen {"skip": true}
|
||||
// @Description Experimental: this endpoint is subject to change.
|
||||
//
|
||||
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
|
||||
func (api *API) getChatACL(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chat := httpmw.ChatParam(r)
|
||||
|
||||
if !api.allowChatSharing(ctx, rw) {
|
||||
return
|
||||
}
|
||||
if chat.IsSubChat() {
|
||||
resp := codersdk.Response{Message: "Chat ACLs can only be set on root chats."}
|
||||
if chat.RootChatID.Valid {
|
||||
resp.Detail = "Target the root chat (id: " + chat.RootChatID.UUID.String() + ") instead."
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
|
||||
chatACL, err := api.Database.GetChatACLByID(ctx, chat.ID)
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
users, ok := api.chatACLUsers(ctx, rw, chat, chatACL.Users)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
groups, ok := api.chatACLGroups(ctx, rw, chat, chatACL.Groups)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatACL{
|
||||
Users: users,
|
||||
Groups: groups,
|
||||
})
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
//
|
||||
// @Summary Update chat ACL
|
||||
// @ID update-chat-acl
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Chats
|
||||
// @Accept json
|
||||
// @Param chat path string true "Chat ID" format(uuid)
|
||||
// @Param request body codersdk.UpdateChatACL true "Update chat ACL request"
|
||||
// @Success 204
|
||||
// @Router /api/experimental/chats/{chat}/acl [patch]
|
||||
// @x-apidocgen {"skip": true}
|
||||
// @Description Experimental: this endpoint is subject to change.
|
||||
func (api *API) patchChatACL(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chat := httpmw.ChatParam(r)
|
||||
auditor := api.Auditor.Load()
|
||||
aReq, commitAudit := audit.InitRequest[database.Chat](rw, &audit.RequestParams{
|
||||
Audit: *auditor,
|
||||
Log: api.Logger,
|
||||
Request: r,
|
||||
Action: database.AuditActionWrite,
|
||||
OrganizationID: chat.OrganizationID,
|
||||
})
|
||||
defer commitAudit()
|
||||
aReq.Old = chat
|
||||
|
||||
if !api.allowChatSharing(ctx, rw) {
|
||||
return
|
||||
}
|
||||
if chat.IsSubChat() {
|
||||
resp := codersdk.Response{Message: "Chat ACLs can only be set on root chats."}
|
||||
if chat.RootChatID.Valid {
|
||||
resp.Detail = "Target the root chat (id: " + chat.RootChatID.UUID.String() + ") instead."
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, resp)
|
||||
return
|
||||
}
|
||||
if !api.Authorize(r, policy.ActionShare, chat.RBACObject()) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.UpdateChatACL
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := httpmw.APIKey(r)
|
||||
for userID := range req.UserRoles {
|
||||
parsed, err := uuid.Parse(userID)
|
||||
if err == nil && parsed == apiKey.UserID {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cannot change your own chat sharing role.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
validErrs := acl.Validate(ctx, api.Database, ChatACLUpdateValidator(req))
|
||||
if len(validErrs) > 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid request to update chat ACL.",
|
||||
Validations: validErrs,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := api.Database.InTx(func(tx database.Store) error {
|
||||
current, err := tx.GetChatByIDForUpdate(ctx, chat.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get chat by ID: %w", err)
|
||||
}
|
||||
if current.UserACL == nil {
|
||||
current.UserACL = database.ChatACL{}
|
||||
}
|
||||
if current.GroupACL == nil {
|
||||
current.GroupACL = database.ChatACL{}
|
||||
}
|
||||
|
||||
for id, role := range req.UserRoles {
|
||||
if role == codersdk.ChatRoleDeleted {
|
||||
delete(current.UserACL, id)
|
||||
continue
|
||||
}
|
||||
current.UserACL[id] = database.ChatACLEntry{
|
||||
Permissions: db2sdk.ChatRoleActions(role),
|
||||
}
|
||||
}
|
||||
for id, role := range req.GroupRoles {
|
||||
if role == codersdk.ChatRoleDeleted {
|
||||
delete(current.GroupACL, id)
|
||||
continue
|
||||
}
|
||||
current.GroupACL[id] = database.ChatACLEntry{
|
||||
Permissions: db2sdk.ChatRoleActions(role),
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.UpdateChatACLByID(ctx, database.UpdateChatACLByIDParams{
|
||||
ID: chat.ID,
|
||||
UserACL: current.UserACL,
|
||||
GroupACL: current.GroupACL,
|
||||
}); err != nil {
|
||||
return xerrors.Errorf("update chat ACL: %w", err)
|
||||
}
|
||||
updatedChat, err := tx.GetChatByID(ctx, chat.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get updated chat by ID: %w", err)
|
||||
}
|
||||
aReq.New = updatedChat
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *API) chatACLUsers(ctx context.Context, rw http.ResponseWriter, chat database.Chat, entries database.ChatACL) ([]codersdk.ChatUser, bool) {
|
||||
userIDs := make([]uuid.UUID, 0, len(entries))
|
||||
for userID := range entries {
|
||||
id, err := uuid.Parse(userID)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "found invalid user uuid in chat acl", slog.Error(err), slog.F("chat_id", chat.ID))
|
||||
continue
|
||||
}
|
||||
userIDs = append(userIDs, id)
|
||||
}
|
||||
|
||||
//nolint:gocritic // Users who can read the chat ACL should see shared users even without user read permission.
|
||||
dbUsers, err := api.Database.GetUsersByIDs(dbauthz.AsSystemRestricted(ctx), userIDs)
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
users := make([]codersdk.ChatUser, 0, len(dbUsers))
|
||||
for _, user := range dbUsers {
|
||||
entry := entries[user.ID.String()]
|
||||
users = append(users, codersdk.ChatUser{
|
||||
MinimalUser: db2sdk.MinimalUser(user),
|
||||
Role: convertToChatRole(entry.Permissions),
|
||||
})
|
||||
}
|
||||
return users, true
|
||||
}
|
||||
|
||||
func (api *API) chatACLGroups(ctx context.Context, rw http.ResponseWriter, chat database.Chat, entries database.ChatACL) ([]codersdk.ChatGroup, bool) {
|
||||
groupIDs := make([]uuid.UUID, 0, len(entries))
|
||||
for groupID := range entries {
|
||||
id, err := uuid.Parse(groupID)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "found invalid group uuid in chat acl", slog.Error(err), slog.F("chat_id", chat.ID))
|
||||
continue
|
||||
}
|
||||
groupIDs = append(groupIDs, id)
|
||||
}
|
||||
|
||||
dbGroups := make([]database.GetGroupsRow, 0)
|
||||
if len(groupIDs) > 0 {
|
||||
var err error
|
||||
//nolint:gocritic // Users who can read the chat ACL should see shared groups even without group read permission.
|
||||
dbGroups, err = api.Database.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{GroupIds: groupIDs})
|
||||
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([]codersdk.ChatGroup, 0, len(dbGroups))
|
||||
for _, group := range dbGroups {
|
||||
//nolint:gocritic // Users who can read the chat ACL should see shared group sizes even without group read permission.
|
||||
memberCount, err := api.Database.GetGroupMembersCountByGroupID(dbauthz.AsSystemRestricted(ctx), database.GetGroupMembersCountByGroupIDParams{
|
||||
GroupID: group.Group.ID,
|
||||
IncludeSystem: false,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return nil, false
|
||||
}
|
||||
entry := entries[group.Group.ID.String()]
|
||||
groups = append(groups, codersdk.ChatGroup{
|
||||
Group: db2sdk.Group(group, nil, int(memberCount)),
|
||||
Role: convertToChatRole(entry.Permissions),
|
||||
})
|
||||
}
|
||||
return groups, true
|
||||
}
|
||||
|
||||
func (api *API) allowChatSharing(ctx context.Context, rw http.ResponseWriter) bool {
|
||||
if !api.chatSharingDisabled() {
|
||||
return true
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Chat sharing is disabled for this deployment.",
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
func (api *API) chatSharingDisabled() bool {
|
||||
return rbac.ChatACLDisabled() || (api.DeploymentValues != nil && bool(api.DeploymentValues.DisableChatSharing))
|
||||
}
|
||||
|
||||
type ChatACLUpdateValidator codersdk.UpdateChatACL
|
||||
|
||||
var _ acl.UpdateValidator[codersdk.ChatRole] = ChatACLUpdateValidator{}
|
||||
|
||||
func (c ChatACLUpdateValidator) Users() (map[string]codersdk.ChatRole, string) {
|
||||
return c.UserRoles, "user_roles"
|
||||
}
|
||||
|
||||
func (c ChatACLUpdateValidator) Groups() (map[string]codersdk.ChatRole, string) {
|
||||
return c.GroupRoles, "group_roles"
|
||||
}
|
||||
|
||||
func (ChatACLUpdateValidator) ValidateRole(role codersdk.ChatRole) error {
|
||||
if role == codersdk.ChatRoleDeleted || role == codersdk.ChatRoleRead {
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("role %q is not a valid chat role", role)
|
||||
}
|
||||
|
||||
func convertToChatRole(actions []policy.Action) codersdk.ChatRole {
|
||||
if slice.SameElements(actions, db2sdk.ChatRoleActions(codersdk.ChatRoleRead)) {
|
||||
return codersdk.ChatRoleRead
|
||||
}
|
||||
|
||||
return codersdk.ChatRoleDeleted
|
||||
}
|
||||
@@ -0,0 +1,517 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/audit"
|
||||
"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/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestChatACLSharingLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
mAudit := audit.NewMock()
|
||||
client, db := newChatClientWithDatabase(t, func(opts *coderdtest.Options) {
|
||||
opts.Auditor = mAudit
|
||||
})
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
_ = createChatModelConfig(t, client)
|
||||
|
||||
sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
|
||||
sharedClientExp := codersdk.NewExperimentalClient(sharedClient)
|
||||
nonSharedClient, _ := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
|
||||
nonSharedClientExp := codersdk.NewExperimentalClient(nonSharedClient)
|
||||
groupMemberClient, groupMember := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
|
||||
groupMemberClientExp := codersdk.NewExperimentalClient(groupMemberClient)
|
||||
sharedGroup := dbgen.Group(t, db, database.Group{OrganizationID: firstUser.OrganizationID})
|
||||
dbgen.GroupMember(t, db, database.GroupMemberTable{GroupID: sharedGroup.ID, UserID: groupMember.ID})
|
||||
|
||||
data := []byte("chat sharing file")
|
||||
uploaded, err := client.UploadChatFile(ctx, firstUser.OrganizationID, "text/plain", "shared.txt", bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
chat := createChatForSharing(ctx, t, client, firstUser.OrganizationID, "shared chat", uploaded.ID)
|
||||
|
||||
_, err = sharedClientExp.GetChat(ctx, chat.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
_, _, err = nonSharedClientExp.GetChatFile(ctx, uploaded.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
|
||||
err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
sharedUser.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
GroupRoles: map[string]codersdk.ChatRole{
|
||||
sharedGroup.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, mAudit.Contains(t, database.AuditLog{
|
||||
Action: database.AuditActionWrite,
|
||||
ResourceType: database.ResourceTypeChat,
|
||||
ResourceID: chat.ID,
|
||||
UserID: firstUser.UserID,
|
||||
}))
|
||||
|
||||
acl, err := client.GetChatACL(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, acl.Users, 1)
|
||||
require.Equal(t, sharedUser.ID.String(), acl.Users[0].ID.String())
|
||||
require.Equal(t, map[uuid.UUID]codersdk.ChatRole{
|
||||
sharedUser.ID: codersdk.ChatRoleRead,
|
||||
}, chatUserRoles(acl.Users))
|
||||
require.Equal(t, map[uuid.UUID]codersdk.ChatRole{
|
||||
sharedGroup.ID: codersdk.ChatRoleRead,
|
||||
}, chatGroupRoles(acl.Groups))
|
||||
require.Len(t, acl.Groups, 1)
|
||||
require.Equal(t, sharedGroup.ID.String(), acl.Groups[0].ID.String())
|
||||
require.Empty(t, acl.Groups[0].Members)
|
||||
require.Equal(t, 1, acl.Groups[0].TotalMemberCount)
|
||||
|
||||
sharedACL, err := sharedClientExp.GetChatACL(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chatUserRoles(acl.Users), chatUserRoles(sharedACL.Users))
|
||||
require.Equal(t, chatGroupRoles(acl.Groups), chatGroupRoles(sharedACL.Groups))
|
||||
require.Len(t, sharedACL.Groups, 1)
|
||||
require.Empty(t, sharedACL.Groups[0].Members)
|
||||
require.Equal(t, 1, sharedACL.Groups[0].TotalMemberCount)
|
||||
|
||||
sharedChat, err := sharedClientExp.GetChat(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chat.ID, sharedChat.ID)
|
||||
require.Equal(t, coderdtest.FirstUserParams.Username, sharedChat.OwnerUsername)
|
||||
require.Equal(t, coderdtest.FirstUserParams.Name, sharedChat.OwnerName)
|
||||
require.Len(t, sharedChat.Files, 1)
|
||||
require.Equal(t, uploaded.ID, sharedChat.Files[0].ID)
|
||||
|
||||
messages, err := sharedClientExp.GetChatMessages(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, messages.Messages)
|
||||
|
||||
got, contentType, err := sharedClientExp.GetChatFile(ctx, uploaded.ID)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, contentType, "text/plain")
|
||||
require.Equal(t, data, got)
|
||||
_, _, err = nonSharedClientExp.GetChatFile(ctx, uploaded.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
|
||||
groupChat, err := groupMemberClientExp.GetChat(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, chat.ID, groupChat.ID)
|
||||
|
||||
_, err = sharedClientExp.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
|
||||
Content: []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: "should not send",
|
||||
}},
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
|
||||
err = sharedClientExp.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{
|
||||
Title: ptr.Ref("should not rename"),
|
||||
})
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
|
||||
err = sharedClientExp.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
groupMember.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
requireSDKError(t, err, http.StatusForbidden)
|
||||
|
||||
err = sharedClientExp.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
uuid.NewString(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
requireSDKError(t, err, http.StatusForbidden)
|
||||
|
||||
err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
strings.ToUpper(firstUser.UserID.String()): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
require.Equal(t, "Cannot change your own chat sharing role.", sdkErr.Message)
|
||||
|
||||
err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
sharedUser.ID.String(): codersdk.ChatRoleDeleted,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = sharedClientExp.GetChat(ctx, chat.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
_, err = groupMemberClientExp.GetChat(ctx, chat.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
mAudit.ResetLogs()
|
||||
err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
GroupRoles: map[string]codersdk.ChatRole{
|
||||
sharedGroup.ID.String(): codersdk.ChatRoleDeleted,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, mAudit.Contains(t, database.AuditLog{
|
||||
Action: database.AuditActionWrite,
|
||||
ResourceType: database.ResourceTypeChat,
|
||||
ResourceID: chat.ID,
|
||||
UserID: firstUser.UserID,
|
||||
}))
|
||||
_, err = groupMemberClientExp.GetChat(ctx, chat.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestChatACLSubChatInheritance(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client, db := newChatClientWithDatabase(t)
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
|
||||
sharedClientExp := codersdk.NewExperimentalClient(sharedClient)
|
||||
|
||||
root := createChatForSharing(ctx, t, client, firstUser.OrganizationID, "root chat")
|
||||
child := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: firstUser.OrganizationID,
|
||||
OwnerID: firstUser.UserID,
|
||||
ParentChatID: uuid.NullUUID{UUID: root.ID, Valid: true},
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "child chat",
|
||||
})
|
||||
|
||||
err := client.UpdateChatACL(ctx, root.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
sharedUser.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
sharedChild, err := sharedClientExp.GetChat(ctx, child.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, child.ID, sharedChild.ID)
|
||||
require.NotNil(t, sharedChild.RootChatID)
|
||||
require.Equal(t, root.ID, *sharedChild.RootChatID)
|
||||
|
||||
_, err = sharedClientExp.GetChat(ctx, root.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.UpdateChatACL(ctx, child.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
sharedUser.ID.String(): codersdk.ChatRoleDeleted,
|
||||
},
|
||||
})
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
require.Equal(t, "Chat ACLs can only be set on root chats.", sdkErr.Message)
|
||||
|
||||
_, err = client.GetChatACL(ctx, child.ID)
|
||||
sdkErr = requireSDKError(t, err, http.StatusBadRequest)
|
||||
require.Equal(t, "Chat ACLs can only be set on root chats.", sdkErr.Message)
|
||||
}
|
||||
|
||||
func TestChatACLValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client := newChatClient(t)
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
_ = createChatModelConfig(t, client)
|
||||
chat := createChatForSharing(ctx, t, client, firstUser.OrganizationID, "validation chat")
|
||||
missingUserID := uuid.New()
|
||||
missingGroupID := uuid.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req codersdk.UpdateChatACL
|
||||
wantValidation codersdk.ValidationError
|
||||
}{
|
||||
{
|
||||
name: "InvalidRole",
|
||||
req: codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
uuid.NewString(): codersdk.ChatRole("write"),
|
||||
},
|
||||
},
|
||||
wantValidation: codersdk.ValidationError{
|
||||
Field: "user_roles",
|
||||
Detail: `role "write" is not a valid chat role`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidUserUUID",
|
||||
req: codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
"not-a-uuid": codersdk.ChatRoleRead,
|
||||
},
|
||||
},
|
||||
wantValidation: codersdk.ValidationError{
|
||||
Field: "user_roles",
|
||||
Detail: "not-a-uuid is not a valid UUID.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidGroupUUID",
|
||||
req: codersdk.UpdateChatACL{
|
||||
GroupRoles: map[string]codersdk.ChatRole{
|
||||
"not-a-uuid": codersdk.ChatRoleRead,
|
||||
},
|
||||
},
|
||||
wantValidation: codersdk.ValidationError{
|
||||
Field: "group_roles",
|
||||
Detail: "not-a-uuid is not a valid UUID.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MissingUser",
|
||||
req: codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
missingUserID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
},
|
||||
wantValidation: codersdk.ValidationError{
|
||||
Field: "user_roles",
|
||||
Detail: "user with ID " + missingUserID.String() + " does not exist",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MissingGroup",
|
||||
req: codersdk.UpdateChatACL{
|
||||
GroupRoles: map[string]codersdk.ChatRole{
|
||||
missingGroupID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
},
|
||||
wantValidation: codersdk.ValidationError{
|
||||
Field: "group_roles",
|
||||
Detail: "group with ID " + missingGroupID.String() + " does not exist",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
err := client.UpdateChatACL(ctx, chat.ID, tt.req)
|
||||
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
|
||||
require.Equal(t, "Invalid request to update chat ACL.", sdkErr.Message)
|
||||
require.Contains(t, sdkErr.Validations, tt.wantValidation)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSharedReaderStreamChat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client, db := newChatClientWithDatabase(t)
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
sharedClient, sharedUser := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID)
|
||||
sharedClientExp := codersdk.NewExperimentalClient(sharedClient)
|
||||
chat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: firstUser.OrganizationID,
|
||||
OwnerID: firstUser.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "shared stream chat",
|
||||
})
|
||||
insertAssistantCostMessage(t, db, chat.ID, modelConfig.ID, 0)
|
||||
|
||||
err := client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
sharedUser.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
events, closer, err := sharedClientExp.StreamChat(ctx, chat.ID, nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = closer.Close() })
|
||||
|
||||
foundAssistantMessage := false
|
||||
for !foundAssistantMessage {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
require.FailNow(t, "timed out waiting for shared stream chat event")
|
||||
case event, ok := <-events:
|
||||
require.True(t, ok, "stream closed before expected event")
|
||||
require.Equal(t, chat.ID, event.ChatID)
|
||||
require.NotEqual(t, codersdk.ChatStreamEventTypeError, event.Type)
|
||||
if event.Type == codersdk.ChatStreamEventTypeMessage &&
|
||||
event.Message != nil &&
|
||||
event.Message.Role == codersdk.ChatMessageRoleAssistant {
|
||||
foundAssistantMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
require.NoError(t, closer.Close())
|
||||
|
||||
persisted, err := db.GetChatByID(dbauthz.AsSystemRestricted(ctx), chat.ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, persisted.LastReadMessageID.Valid)
|
||||
}
|
||||
|
||||
func TestListChatsExcludesSharedChats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
client, db := newChatClientWithDatabase(t)
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID))
|
||||
viewerClientExp := codersdk.NewExperimentalClient(viewerClient)
|
||||
sharedChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: firstUser.OrganizationID,
|
||||
OwnerID: firstUser.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "shared with viewer",
|
||||
})
|
||||
viewerChat := dbgen.Chat(t, db, database.Chat{
|
||||
OrganizationID: firstUser.OrganizationID,
|
||||
OwnerID: viewer.ID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "viewer owned",
|
||||
})
|
||||
|
||||
err := client.UpdateChatACL(ctx, sharedChat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
viewer.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ownedOnly, err := viewerClientExp.ListChats(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[uuid.UUID]struct{}{viewerChat.ID: {}}, chatIDSet(ownedOnly))
|
||||
}
|
||||
|
||||
//nolint:paralleltest // This test verifies a process-wide RBAC kill switch.
|
||||
func TestChatSharingDisabled(t *testing.T) {
|
||||
previous := rbac.ChatACLDisabled()
|
||||
rbac.SetChatACLDisabled(false)
|
||||
rbac.ReloadBuiltinRoles(nil)
|
||||
t.Cleanup(func() {
|
||||
rbac.ReloadBuiltinRoles(nil)
|
||||
rbac.SetChatACLDisabled(previous)
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
values := chatDeploymentValues(t)
|
||||
values.DisableChatSharing = true
|
||||
store, pubsub := dbtestutil.NewDB(t)
|
||||
client := newChatClient(t, func(opts *coderdtest.Options) {
|
||||
opts.DeploymentValues = values
|
||||
opts.Database = store
|
||||
opts.Pubsub = pubsub
|
||||
})
|
||||
firstUser := coderdtest.CreateFirstUser(t, client.Client)
|
||||
modelConfig := createChatModelConfig(t, client)
|
||||
viewerClient, viewer := coderdtest.CreateAnotherUser(t, client.Client, firstUser.OrganizationID, rbac.ScopedRoleAgentsAccess(firstUser.OrganizationID))
|
||||
viewerClientExp := codersdk.NewExperimentalClient(viewerClient)
|
||||
|
||||
chat := dbgen.Chat(t, store, database.Chat{
|
||||
OrganizationID: firstUser.OrganizationID,
|
||||
OwnerID: firstUser.UserID,
|
||||
LastModelConfigID: modelConfig.ID,
|
||||
Title: "disabled sharing",
|
||||
})
|
||||
err := store.UpdateChatACLByID(ctx, database.UpdateChatACLByIDParams{
|
||||
ID: chat.ID,
|
||||
UserACL: database.ChatACL{
|
||||
viewer.ID.String(): database.ChatACLEntry{Permissions: []policy.Action{policy.ActionRead}},
|
||||
},
|
||||
GroupACL: database.ChatACL{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = viewerClientExp.GetChat(ctx, chat.ID)
|
||||
requireSDKError(t, err, http.StatusNotFound)
|
||||
|
||||
_, err = client.GetChatACL(ctx, chat.ID)
|
||||
sdkErr := requireSDKError(t, err, http.StatusForbidden)
|
||||
require.Equal(t, "Chat sharing is disabled for this deployment.", sdkErr.Message)
|
||||
|
||||
err = client.UpdateChatACL(ctx, chat.ID, codersdk.UpdateChatACL{
|
||||
UserRoles: map[string]codersdk.ChatRole{
|
||||
viewer.ID.String(): codersdk.ChatRoleRead,
|
||||
},
|
||||
})
|
||||
requireSDKError(t, err, http.StatusForbidden)
|
||||
|
||||
ownerChats, err := client.ListChats(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[uuid.UUID]struct{}{chat.ID: {}}, chatIDSet(ownerChats))
|
||||
|
||||
viewerChats, err := viewerClientExp.ListChats(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, viewerChats)
|
||||
}
|
||||
|
||||
func createChatForSharing(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
client *codersdk.ExperimentalClient,
|
||||
organizationID uuid.UUID,
|
||||
text string,
|
||||
fileIDs ...uuid.UUID,
|
||||
) codersdk.Chat {
|
||||
t.Helper()
|
||||
|
||||
content := []codersdk.ChatInputPart{{
|
||||
Type: codersdk.ChatInputPartTypeText,
|
||||
Text: text,
|
||||
}}
|
||||
for _, fileID := range fileIDs {
|
||||
content = append(content, codersdk.ChatInputPart{
|
||||
Type: codersdk.ChatInputPartTypeFile,
|
||||
FileID: fileID,
|
||||
})
|
||||
}
|
||||
chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
|
||||
OrganizationID: organizationID,
|
||||
Content: content,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return chat
|
||||
}
|
||||
|
||||
func chatUserRoles(users []codersdk.ChatUser) map[uuid.UUID]codersdk.ChatRole {
|
||||
roles := make(map[uuid.UUID]codersdk.ChatRole, len(users))
|
||||
for _, user := range users {
|
||||
roles[user.ID] = user.Role
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
func chatGroupRoles(groups []codersdk.ChatGroup) map[uuid.UUID]codersdk.ChatRole {
|
||||
roles := make(map[uuid.UUID]codersdk.ChatRole, len(groups))
|
||||
for _, group := range groups {
|
||||
roles[group.ID] = group.Role
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
func chatIDSet(chats []codersdk.Chat) map[uuid.UUID]struct{} {
|
||||
ids := make(map[uuid.UUID]struct{}, len(chats))
|
||||
for _, chat := range chats {
|
||||
ids[chat.ID] = struct{}{}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
type UpdateValidator[Role codersdk.WorkspaceRole | codersdk.TemplateRole] interface {
|
||||
type UpdateValidator[Role codersdk.WorkspaceRole | codersdk.TemplateRole | codersdk.ChatRole] interface {
|
||||
// Users should return a map from user UUIDs (as strings) to the role they
|
||||
// are being assigned. Additionally, it should return a string that will be
|
||||
// used as the field name for the ValidationErrors returned from Validate.
|
||||
@@ -25,7 +25,7 @@ type UpdateValidator[Role codersdk.WorkspaceRole | codersdk.TemplateRole] interf
|
||||
ValidateRole(role Role) error
|
||||
}
|
||||
|
||||
func Validate[Role codersdk.WorkspaceRole | codersdk.TemplateRole](
|
||||
func Validate[Role codersdk.WorkspaceRole | codersdk.TemplateRole | codersdk.ChatRole](
|
||||
ctx context.Context,
|
||||
db database.Store,
|
||||
v UpdateValidator[Role],
|
||||
|
||||
@@ -227,6 +227,16 @@ func TestRegoQueries(t *testing.T) {
|
||||
" OR (chats_expanded.user_acl#>array['d5389ccc-57a4-4b13-8c3f-31747bcdc9f1', 'permissions'] ? '*'))",
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
},
|
||||
{
|
||||
Name: "ChatAllowList",
|
||||
Queries: []string{
|
||||
`input.object.id != ""`,
|
||||
`input.object.id in ["9046b041-58ed-47a3-9c3a-de302577875a"]`,
|
||||
},
|
||||
ExpectedSQL: p(`(chats_expanded.id :: text != '') OR ` +
|
||||
`(chats_expanded.id :: text = ANY(ARRAY ['9046b041-58ed-47a3-9c3a-de302577875a']))`),
|
||||
VariableConverter: regosql.ChatConverter(),
|
||||
},
|
||||
{
|
||||
Name: "NoACLConfig",
|
||||
Queries: []string{
|
||||
|
||||
@@ -6,6 +6,10 @@ func resourceIDMatcher() sqltypes.VariableMatcher {
|
||||
return sqltypes.StringVarMatcher("id :: text", []string{"input", "object", "id"})
|
||||
}
|
||||
|
||||
func chatResourceIDMatcher() sqltypes.VariableMatcher {
|
||||
return sqltypes.StringVarMatcher("chats_expanded.id :: text", []string{"input", "object", "id"})
|
||||
}
|
||||
|
||||
func organizationOwnerMatcher() sqltypes.VariableMatcher {
|
||||
return sqltypes.StringVarMatcher("organization_id :: text", []string{"input", "object", "org_owner"})
|
||||
}
|
||||
@@ -72,7 +76,7 @@ func ChatNoACLConverter() *sqltypes.VariableConverter {
|
||||
|
||||
func chatBaseConverter() *sqltypes.VariableConverter {
|
||||
return sqltypes.NewVariableConverter().RegisterMatcher(
|
||||
resourceIDMatcher(),
|
||||
chatResourceIDMatcher(),
|
||||
sqltypes.StringVarMatcher("chats_expanded.organization_id :: text", []string{"input", "object", "org_owner"}),
|
||||
userOwnerMatcher(),
|
||||
)
|
||||
|
||||
@@ -109,6 +109,8 @@ type Chat struct {
|
||||
ID uuid.UUID `json:"id" format:"uuid"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
|
||||
OwnerID uuid.UUID `json:"owner_id" format:"uuid"`
|
||||
OwnerUsername string `json:"owner_username,omitempty"`
|
||||
OwnerName string `json:"owner_name,omitempty"`
|
||||
WorkspaceID *uuid.UUID `json:"workspace_id,omitempty" format:"uuid"`
|
||||
BuildID *uuid.UUID `json:"build_id,omitempty" format:"uuid"`
|
||||
AgentID *uuid.UUID `json:"agent_id,omitempty" format:"uuid"`
|
||||
@@ -1967,6 +1969,33 @@ type ChatUsageLimitConfigResponse struct {
|
||||
GroupOverrides []ChatUsageLimitGroupOverride `json:"group_overrides"`
|
||||
}
|
||||
|
||||
type ChatRole string
|
||||
|
||||
const (
|
||||
ChatRoleRead ChatRole = "read"
|
||||
ChatRoleDeleted ChatRole = ""
|
||||
)
|
||||
|
||||
type ChatUser struct {
|
||||
MinimalUser
|
||||
Role ChatRole `json:"role" enums:"read"`
|
||||
}
|
||||
|
||||
type ChatGroup struct {
|
||||
Group
|
||||
Role ChatRole `json:"role" enums:"read"`
|
||||
}
|
||||
|
||||
type ChatACL struct {
|
||||
Users []ChatUser `json:"users"`
|
||||
Groups []ChatGroup `json:"groups"`
|
||||
}
|
||||
|
||||
type UpdateChatACL struct {
|
||||
UserRoles map[string]ChatRole `json:"user_roles,omitempty"`
|
||||
GroupRoles map[string]ChatRole `json:"group_roles,omitempty"`
|
||||
}
|
||||
|
||||
// ListChatsOptions are optional parameters for ListChats.
|
||||
type ListChatsOptions struct {
|
||||
Query string
|
||||
@@ -2942,6 +2971,31 @@ func (c *ExperimentalClient) GetChat(ctx context.Context, chatID uuid.UUID) (Cha
|
||||
return chat, json.NewDecoder(res.Body).Decode(&chat)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) GetChatACL(ctx context.Context, chatID uuid.UUID) (ChatACL, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/experimental/chats/%s/acl", chatID), nil)
|
||||
if err != nil {
|
||||
return ChatACL{}, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return ChatACL{}, ReadBodyAsError(res)
|
||||
}
|
||||
var acl ChatACL
|
||||
return acl, json.NewDecoder(res.Body).Decode(&acl)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) UpdateChatACL(ctx context.Context, chatID uuid.UUID, req UpdateChatACL) error {
|
||||
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/experimental/chats/%s/acl", chatID), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return ReadBodyAsError(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetChatMessages returns the messages and queued messages for a chat.
|
||||
// ChatMessagesPaginationOptions are optional pagination params for
|
||||
// GetChatMessages.
|
||||
|
||||
Generated
+22
@@ -148,6 +148,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -270,6 +272,8 @@ Status Code **200**
|
||||
| `» mcp_server_ids` | array | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» owner_id` | string(uuid) | false | | |
|
||||
| `» owner_name` | string | false | | |
|
||||
| `» owner_username` | string | false | | |
|
||||
| `» parent_chat_id` | string(uuid) | false | | |
|
||||
| `» pin_order` | integer | false | | |
|
||||
| `» plan_mode` | [codersdk.ChatPlanMode](schemas.md#codersdkchatplanmode) | false | | |
|
||||
@@ -482,6 +486,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -608,6 +614,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -885,6 +893,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -1065,6 +1075,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -1191,6 +1203,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -1452,6 +1466,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -1578,6 +1594,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -2686,6 +2704,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -2812,6 +2832,8 @@ Experimental: this endpoint is subject to change.
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
|
||||
Generated
+185
@@ -2338,6 +2338,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -2464,6 +2466,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -2501,6 +2505,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `mcp_server_ids` | array of string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `owner_id` | string | false | | |
|
||||
| `owner_name` | string | false | | |
|
||||
| `owner_username` | string | false | | |
|
||||
| `parent_chat_id` | string | false | | |
|
||||
| `pin_order` | integer | false | | |
|
||||
| `plan_mode` | [codersdk.ChatPlanMode](#codersdkchatplanmode) | false | | |
|
||||
@@ -2511,6 +2517,60 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `warnings` | array of string | false | | |
|
||||
| `workspace_id` | string | false | | |
|
||||
|
||||
## codersdk.ChatACL
|
||||
|
||||
```json
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"is_service_account": true,
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
"name": "string",
|
||||
"status": "active",
|
||||
"theme_preference": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"username": "string"
|
||||
}
|
||||
],
|
||||
"name": "string",
|
||||
"organization_display_name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"quota_allowance": 0,
|
||||
"role": "read",
|
||||
"source": "user",
|
||||
"total_member_count": 0
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"role": "read",
|
||||
"username": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------|---------------------------------------------------|----------|--------------|-------------|
|
||||
| `groups` | array of [codersdk.ChatGroup](#codersdkchatgroup) | false | | |
|
||||
| `users` | array of [codersdk.ChatUser](#codersdkchatuser) | false | | |
|
||||
|
||||
## codersdk.ChatBusyBehavior
|
||||
|
||||
```json
|
||||
@@ -2691,6 +2751,63 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `organization_id` | string | false | | |
|
||||
| `owner_id` | string | false | | |
|
||||
|
||||
## codersdk.ChatGroup
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"display_name": "string",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"members": [
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"created_at": "2019-08-24T14:15:22Z",
|
||||
"email": "user@example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"is_service_account": true,
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
"name": "string",
|
||||
"status": "active",
|
||||
"theme_preference": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"username": "string"
|
||||
}
|
||||
],
|
||||
"name": "string",
|
||||
"organization_display_name": "string",
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"organization_name": "string",
|
||||
"quota_allowance": 0,
|
||||
"role": "read",
|
||||
"source": "user",
|
||||
"total_member_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-----------------------------|-------------------------------------------------------|----------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `members` | array of [codersdk.ReducedUser](#codersdkreduceduser) | false | | |
|
||||
| `name` | string | false | | |
|
||||
| `organization_display_name` | string | false | | |
|
||||
| `organization_id` | string | false | | |
|
||||
| `organization_name` | string | false | | |
|
||||
| `quota_allowance` | integer | false | | |
|
||||
| `role` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
|
||||
| `source` | [codersdk.GroupSource](#codersdkgroupsource) | false | | |
|
||||
| `total_member_count` | integer | false | | How many members are in this group. Shows the total count, even if the user is not authorized to read group member details. May be greater than `len(Group.Members)`. |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|----------|----------|
|
||||
| `role` | `read` |
|
||||
|
||||
## codersdk.ChatInputPart
|
||||
|
||||
```json
|
||||
@@ -3377,6 +3494,20 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
|------------------|---------|----------|--------------|-------------|
|
||||
| `retention_days` | integer | false | | |
|
||||
|
||||
## codersdk.ChatRole
|
||||
|
||||
```json
|
||||
"read"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value(s) |
|
||||
|------------|
|
||||
| ``, `read` |
|
||||
|
||||
## codersdk.ChatStatus
|
||||
|
||||
```json
|
||||
@@ -3810,6 +3941,34 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
| `tool_call_id` | string | false | | |
|
||||
| `tool_name` | string | false | | |
|
||||
|
||||
## codersdk.ChatUser
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar_url": "http://example.com",
|
||||
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
|
||||
"name": "string",
|
||||
"role": "read",
|
||||
"username": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------|----------------------------------------|----------|--------------|-------------|
|
||||
| `avatar_url` | string | false | | |
|
||||
| `id` | string | true | | |
|
||||
| `name` | string | false | | |
|
||||
| `role` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
|
||||
| `username` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Property | Value(s) |
|
||||
|----------|----------|
|
||||
| `role` | `read` |
|
||||
|
||||
## codersdk.ChatWatchEvent
|
||||
|
||||
```json
|
||||
@@ -3934,6 +4093,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
||||
],
|
||||
"organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6",
|
||||
"owner_id": "8826ee2e-7933-4665-aef2-2393f84a0d05",
|
||||
"owner_name": "string",
|
||||
"owner_username": "string",
|
||||
"parent_chat_id": "c3609ee6-3b11-4a93-b9ae-e4fabcc99359",
|
||||
"pin_order": 0,
|
||||
"plan_mode": "plan",
|
||||
@@ -12893,6 +13054,30 @@ Restarts will only happen on weekdays in this list on weeks which line up with W
|
||||
| `logo_url` | string | false | | |
|
||||
| `service_banner` | [codersdk.BannerConfig](#codersdkbannerconfig) | false | | Deprecated: ServiceBanner has been replaced by AnnouncementBanners. |
|
||||
|
||||
## codersdk.UpdateChatACL
|
||||
|
||||
```json
|
||||
{
|
||||
"group_roles": {
|
||||
"property1": "read",
|
||||
"property2": "read"
|
||||
},
|
||||
"user_roles": {
|
||||
"property1": "read",
|
||||
"property2": "read"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|----------------------------------------|----------|--------------|-------------|
|
||||
| `group_roles` | object | false | | |
|
||||
| » `[any property]` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
|
||||
| `user_roles` | object | false | | |
|
||||
| » `[any property]` | [codersdk.ChatRole](#codersdkchatrole) | false | | |
|
||||
|
||||
## codersdk.UpdateChatRequest
|
||||
|
||||
```json
|
||||
|
||||
@@ -101,6 +101,7 @@ const makeChat = (
|
||||
id,
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
last_model_config_id: "model-1",
|
||||
mcp_server_ids: [],
|
||||
labels: {},
|
||||
|
||||
Generated
+29
@@ -1480,6 +1480,8 @@ export interface Chat {
|
||||
readonly id: string;
|
||||
readonly organization_id: string;
|
||||
readonly owner_id: string;
|
||||
readonly owner_username?: string;
|
||||
readonly owner_name?: string;
|
||||
readonly workspace_id?: string;
|
||||
readonly build_id?: string;
|
||||
readonly agent_id?: string;
|
||||
@@ -1524,6 +1526,12 @@ export interface Chat {
|
||||
readonly children: readonly Chat[];
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface ChatACL {
|
||||
readonly users: readonly ChatUser[];
|
||||
readonly groups: readonly ChatGroup[];
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export type ChatAttachmentMediaType =
|
||||
| "application/json"
|
||||
@@ -2049,6 +2057,11 @@ export const ChatGitWatchWorkspaceNoAgentsMessage =
|
||||
*/
|
||||
export const ChatGitWatchWorkspaceNotFoundMessage = "Chat workspace not found.";
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface ChatGroup extends Group {
|
||||
readonly role: ChatRole;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* ChatInputPart is a single user input part for creating a chat.
|
||||
@@ -2652,6 +2665,11 @@ export interface ChatRetentionDaysResponse {
|
||||
readonly retention_days: number;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export type ChatRole = "" | "read";
|
||||
|
||||
export const ChatRoles: ChatRole[] = ["", "read"];
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface ChatSkillPart {
|
||||
readonly type: "skill";
|
||||
@@ -2975,6 +2993,11 @@ export interface ChatUsageLimitStatus {
|
||||
readonly period_end?: string;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface ChatUser extends MinimalUser {
|
||||
readonly role: ChatRole;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* ChatWatchEvent represents an event from the global chat watch stream.
|
||||
@@ -8377,6 +8400,12 @@ export interface UpdateAppearanceConfig {
|
||||
readonly announcement_banners: readonly BannerConfig[];
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
export interface UpdateChatACL {
|
||||
readonly user_roles?: Record<string, ChatRole>;
|
||||
readonly group_roles?: Record<string, ChatRole>;
|
||||
}
|
||||
|
||||
// From codersdk/chats.go
|
||||
/**
|
||||
* UpdateChatAutoArchiveDaysRequest is a request to update the chat
|
||||
|
||||
@@ -128,6 +128,7 @@ const mockModelConfigs: TypesGen.ChatModelConfig[] = [
|
||||
const baseChatFields = {
|
||||
organization_id: "test-org-id",
|
||||
owner_id: MockUserOwner.id,
|
||||
owner_username: MockUserOwner.username,
|
||||
workspace_id: mockWorkspace.id,
|
||||
last_model_config_id: MODEL_CONFIG_ID,
|
||||
mcp_server_ids: [],
|
||||
|
||||
@@ -52,6 +52,7 @@ const buildChat = (overrides: Partial<TypesGen.Chat> = {}): TypesGen.Chat => ({
|
||||
id: AGENT_ID,
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
title: "Help me refactor",
|
||||
status: "completed",
|
||||
last_model_config_id: defaultModelConfigID,
|
||||
|
||||
@@ -144,6 +144,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
|
||||
id: "chat-default",
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
title: "Agent",
|
||||
status: "completed",
|
||||
last_model_config_id: defaultModelConfigs[0].id,
|
||||
|
||||
@@ -207,6 +207,7 @@ const makeChat = (chatID: string): TypesGen.Chat => ({
|
||||
id: chatID,
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
last_model_config_id: "model-1",
|
||||
mcp_server_ids: [],
|
||||
labels: {},
|
||||
|
||||
@@ -60,6 +60,7 @@ export const WithParentChat: Story = {
|
||||
id: "parent-chat-1",
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-id",
|
||||
owner_username: "owner",
|
||||
last_model_config_id: "model-config-1",
|
||||
mcp_server_ids: [],
|
||||
labels: {},
|
||||
|
||||
@@ -59,6 +59,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
|
||||
id: "chat-default",
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
title: "Agent",
|
||||
status: "completed",
|
||||
last_model_config_id: defaultModelConfigs[0].id,
|
||||
|
||||
@@ -57,6 +57,7 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
|
||||
id: "chat-default",
|
||||
organization_id: "test-org-id",
|
||||
owner_id: "owner-1",
|
||||
owner_username: "owner",
|
||||
title: "Agent",
|
||||
status: "completed",
|
||||
last_model_config_id: "model-1",
|
||||
|
||||
Reference in New Issue
Block a user