mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
316 lines
9.3 KiB
Go
316 lines
9.3 KiB
Go
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
|
|
}
|