feat(custom-roles): align chatwoot permissions
This commit is contained in:
+72
-31
@@ -13,27 +13,32 @@ import (
|
||||
)
|
||||
|
||||
// --- Permission Dimensions (P2E §2.1) ---
|
||||
// Six enterprise permission dimensions, each with levels: full, read, none.
|
||||
// Chatwoot custom-role permissions are stored as string keys. The local policy
|
||||
// matrix maps present keys to full access while retaining legacy level support.
|
||||
|
||||
type PermissionDimension string
|
||||
|
||||
const (
|
||||
DimensionConversationManage PermissionDimension = "conversation_manage"
|
||||
DimensionConversationDelete PermissionDimension = "conversation_delete"
|
||||
DimensionContactManage PermissionDimension = "contact_manage"
|
||||
DimensionReportManage PermissionDimension = "report_manage"
|
||||
DimensionKnowledgeBaseManage PermissionDimension = "knowledge_base_manage"
|
||||
DimensionAutomationManage PermissionDimension = "automation_manage"
|
||||
DimensionConversationManage PermissionDimension = "conversation_manage"
|
||||
DimensionConversationUnassignedManage PermissionDimension = "conversation_unassigned_manage"
|
||||
DimensionConversationParticipatingManage PermissionDimension = "conversation_participating_manage"
|
||||
DimensionContactManage PermissionDimension = "contact_manage"
|
||||
DimensionReportManage PermissionDimension = "report_manage"
|
||||
DimensionKnowledgeBaseManage PermissionDimension = "knowledge_base_manage"
|
||||
|
||||
// Legacy local dimensions retained for old rows/tests.
|
||||
DimensionConversationDelete PermissionDimension = "conversation_delete"
|
||||
DimensionAutomationManage PermissionDimension = "automation_manage"
|
||||
)
|
||||
|
||||
// AllDimensions lists all 6 permission dimensions for iteration.
|
||||
var AllDimensions = []PermissionDimension{
|
||||
DimensionConversationManage,
|
||||
DimensionConversationDelete,
|
||||
DimensionConversationUnassignedManage,
|
||||
DimensionConversationParticipatingManage,
|
||||
DimensionContactManage,
|
||||
DimensionReportManage,
|
||||
DimensionKnowledgeBaseManage,
|
||||
DimensionAutomationManage,
|
||||
}
|
||||
|
||||
// PermissionLevel represents the access level for a permission dimension.
|
||||
@@ -69,23 +74,23 @@ type PermissionMatrixMap map[PermissionDimension]PermissionLevel
|
||||
// AgentDefaultPermissions defines the default permission matrix for agent role.
|
||||
// Reference: P2E §2.2 — agent defaults
|
||||
var AgentDefaultPermissions = PermissionMatrixMap{
|
||||
DimensionConversationManage: PermissionRead,
|
||||
DimensionConversationDelete: PermissionNone,
|
||||
DimensionContactManage: PermissionRead,
|
||||
DimensionReportManage: PermissionNone,
|
||||
DimensionKnowledgeBaseManage: PermissionNone,
|
||||
DimensionAutomationManage: PermissionNone,
|
||||
DimensionConversationManage: PermissionRead,
|
||||
DimensionConversationDelete: PermissionNone,
|
||||
DimensionContactManage: PermissionRead,
|
||||
DimensionReportManage: PermissionNone,
|
||||
DimensionKnowledgeBaseManage: PermissionNone,
|
||||
DimensionAutomationManage: PermissionNone,
|
||||
}
|
||||
|
||||
// AdministratorPermissions defines the permission matrix for administrator role.
|
||||
// All dimensions are set to "full".
|
||||
var AdministratorPermissions = PermissionMatrixMap{
|
||||
DimensionConversationManage: PermissionFull,
|
||||
DimensionConversationDelete: PermissionFull,
|
||||
DimensionContactManage: PermissionFull,
|
||||
DimensionReportManage: PermissionFull,
|
||||
DimensionKnowledgeBaseManage: PermissionFull,
|
||||
DimensionAutomationManage: PermissionFull,
|
||||
DimensionConversationManage: PermissionFull,
|
||||
DimensionConversationDelete: PermissionFull,
|
||||
DimensionContactManage: PermissionFull,
|
||||
DimensionReportManage: PermissionFull,
|
||||
DimensionKnowledgeBaseManage: PermissionFull,
|
||||
DimensionAutomationManage: PermissionFull,
|
||||
}
|
||||
|
||||
// ToJSON serializes the permission matrix to JSON bytes (for JSONB storage).
|
||||
@@ -137,8 +142,12 @@ func NewPolicyContext(userID, accountID uint, role string, customRoleID uint, pe
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
if role == "agent" && customRoleID > 0 {
|
||||
pc.Role = "custom_role"
|
||||
}
|
||||
|
||||
// Apply role-based defaults
|
||||
switch role {
|
||||
switch pc.Role {
|
||||
case "administrator":
|
||||
pc.Permissions = AdministratorPermissions
|
||||
case "agent":
|
||||
@@ -160,12 +169,12 @@ func (pc *PolicyContext) IsAdministrator() bool {
|
||||
|
||||
// IsAgent returns true if the role is agent.
|
||||
func (pc *PolicyContext) IsAgent() bool {
|
||||
return pc.Role == "agent"
|
||||
return pc.Role == "agent" && pc.CustomRoleID == 0
|
||||
}
|
||||
|
||||
// IsCustomRole returns true if the role is a custom (enterprise) role.
|
||||
func (pc *PolicyContext) IsCustomRole() bool {
|
||||
return pc.Role == "custom_role"
|
||||
return pc.Role == "custom_role" || (pc.Role == "agent" && pc.CustomRoleID > 0)
|
||||
}
|
||||
|
||||
// Can checks whether the current user is authorized for an action on a resource.
|
||||
@@ -203,11 +212,16 @@ func (pc *PolicyContext) Can(action, resource string) bool {
|
||||
// Special rule: agents can create messages (reply) with read-level access
|
||||
// This matches Chatwoot's behavior where agents can send replies to conversations
|
||||
if resource == "message" && action == "create" {
|
||||
level, ok := pc.Permissions[DimensionConversationManage]
|
||||
if !ok {
|
||||
return pc.canAccessConversationAction("read")
|
||||
}
|
||||
|
||||
if resource == "conversation" || resource == "message" {
|
||||
if pc.canAccessConversationAction(action) {
|
||||
return true
|
||||
}
|
||||
if action != "delete" {
|
||||
return false
|
||||
}
|
||||
return level.CanRead()
|
||||
}
|
||||
|
||||
dimension := mapActionToDimension(action, resource)
|
||||
@@ -224,6 +238,31 @@ func (pc *PolicyContext) Can(action, resource string) bool {
|
||||
return matchesAction(level, action)
|
||||
}
|
||||
|
||||
func (pc *PolicyContext) canAccessConversationAction(action string) bool {
|
||||
if action == "delete" {
|
||||
level, ok := pc.Permissions[DimensionConversationDelete]
|
||||
return ok && level.CanWrite()
|
||||
}
|
||||
|
||||
for _, dim := range []PermissionDimension{
|
||||
DimensionConversationManage,
|
||||
DimensionConversationUnassignedManage,
|
||||
DimensionConversationParticipatingManage,
|
||||
} {
|
||||
level, ok := pc.Permissions[dim]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if action == "read" && level.CanRead() {
|
||||
return true
|
||||
}
|
||||
if matchesAction(level, action) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mapActionToDimension maps a (action, resource) pair to a PermissionDimension.
|
||||
// This bridges the flat permission constants in permission.go with the
|
||||
// 6 enterprise dimensions.
|
||||
@@ -296,14 +335,16 @@ func (pc *PolicyContext) Scope(db *gorm.DB, resource string) *gorm.DB {
|
||||
|
||||
switch resource {
|
||||
case "conversation":
|
||||
if pc.Can("manage", "conversation") {
|
||||
if pc.GetPermissionLevel(DimensionConversationManage).CanRead() {
|
||||
// Full manage: see all conversations in account
|
||||
return db.Where("account_id = ?", pc.AccountID)
|
||||
}
|
||||
if pc.Can("read", "conversation") {
|
||||
// Read-only: see assigned + unassigned conversations
|
||||
if pc.GetPermissionLevel(DimensionConversationUnassignedManage).CanRead() {
|
||||
return db.Where("account_id = ? AND (assignee_id = ? OR assignee_id IS NULL OR assignee_id = 0)", pc.AccountID, pc.UserID)
|
||||
}
|
||||
if pc.GetPermissionLevel(DimensionConversationParticipatingManage).CanRead() {
|
||||
return db.Where("account_id = ? AND (assignee_id = ? OR id IN (SELECT conversation_id FROM conversation_participants WHERE user_id = ?))", pc.AccountID, pc.UserID, pc.UserID)
|
||||
}
|
||||
// No access: no conversations visible
|
||||
return db.Where("account_id = ? AND 1=0", pc.AccountID) // empty result set
|
||||
|
||||
@@ -364,4 +405,4 @@ func (pc *PolicyContext) HasFeatureAccess(dim PermissionDimension, required Perm
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user