Files
gochat/internal/model/custom_role.go
T

209 lines
7.3 KiB
Go

package model
// CustomRole represents an enterprise custom role with Chatwoot-compatible
// permission keys. Chatwoot stores permissions as a text array; GoChat stores the
// same string array as JSON for portability while keeping legacy map reads.
// Reference: Chatwoot enterprise/app/models/custom_role.rb
import (
"encoding/json"
"fmt"
"time"
"gorm.io/gorm"
)
// PermissionDimension constants are duplicated here to avoid a circular import
// with auth. The first six constants are the current Chatwoot custom-role keys.
type PermissionDimension string
const (
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. These are accepted when reading older JSON map
// values but are not valid Chatwoot custom-role API keys.
DimensionConversationDelete PermissionDimension = "conversation_delete"
DimensionAutomationManage PermissionDimension = "automation_manage"
)
// PermissionLevel constants — duplicated here to avoid circular import with auth package.
type PermissionLevel string
const (
PermissionLevelFull PermissionLevel = "full"
PermissionLevelRead PermissionLevel = "read"
PermissionLevelNone PermissionLevel = "none"
)
// CustomRole represents a custom enterprise role definition.
type CustomRole struct {
ID uint `gorm:"primaryKey" json:"id"`
AccountID uint `gorm:"not null;index" json:"account_id"`
Name string `gorm:"size:255;not null" json:"name"`
Permissions string `gorm:"type:jsonb;not null;default:'[]'" json:"permissions"` // JSON array of Chatwoot permission keys.
Description string `gorm:"size:500" json:"description,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
// Relations
Account Account `gorm:"foreignKey:AccountID" json:"account,omitempty"`
AccountUsers []AccountUser `gorm:"foreignKey:CustomRoleID" json:"account_users,omitempty"`
}
func (CustomRole) TableName() string { return "custom_roles" }
var validCustomRolePermissions = map[PermissionDimension]struct{}{
DimensionConversationManage: {},
DimensionConversationUnassignedManage: {},
DimensionConversationParticipatingManage: {},
DimensionContactManage: {},
DimensionReportManage: {},
DimensionKnowledgeBaseManage: {},
}
// IsValidCustomRolePermission reports whether key is one of Chatwoot's current
// CustomRole::PERMISSIONS values.
func IsValidCustomRolePermission(key PermissionDimension) bool {
_, ok := validCustomRolePermissions[key]
return ok
}
// GetPermissionKeys returns Chatwoot custom-role permission keys. It accepts the
// current JSON array shape and the legacy local JSON map shape for old rows.
func (cr *CustomRole) GetPermissionKeys() ([]PermissionDimension, error) {
if cr.Permissions == "" || cr.Permissions == "{}" || cr.Permissions == "[]" || cr.Permissions == "null" {
return []PermissionDimension{}, nil
}
var keys []PermissionDimension
if err := json.Unmarshal([]byte(cr.Permissions), &keys); err == nil {
return normalizePermissionKeys(keys)
}
legacyMap, err := cr.GetPermissionMap()
if err != nil {
return nil, err
}
keys = make([]PermissionDimension, 0, len(legacyMap))
for dim, level := range legacyMap {
if !IsValidCustomRolePermission(dim) || level == PermissionLevelNone {
continue
}
keys = append(keys, dim)
}
return normalizePermissionKeys(keys)
}
// SetPermissionKeys stores Chatwoot custom-role permission keys as a JSON array.
func (cr *CustomRole) SetPermissionKeys(keys []PermissionDimension) error {
normalized, err := normalizePermissionKeys(keys)
if err != nil {
return err
}
data, err := json.Marshal(normalized)
if err != nil {
return fmt.Errorf("failed to serialize custom role permissions: %w", err)
}
cr.Permissions = string(data)
return nil
}
// GetPermissionMap deserializes the JSONB permissions field into a map of
// PermissionDimension → PermissionLevel. This is the raw deserialization;
// the auth.PolicyContext layer converts this into auth.PermissionMatrix for
// policy evaluation.
func (cr *CustomRole) GetPermissionMap() (map[PermissionDimension]PermissionLevel, error) {
var keys []PermissionDimension
if err := json.Unmarshal([]byte(cr.Permissions), &keys); err == nil {
pm := map[PermissionDimension]PermissionLevel{}
normalized, err := normalizePermissionKeys(keys)
if err != nil {
return nil, err
}
for _, key := range normalized {
pm[key] = PermissionLevelFull
}
return pm, nil
}
var m map[PermissionDimension]PermissionLevel
if cr.Permissions == "" || cr.Permissions == "{}" {
return map[PermissionDimension]PermissionLevel{}, nil
}
if err := json.Unmarshal([]byte(cr.Permissions), &m); err != nil {
return nil, fmt.Errorf("failed to parse custom role permissions JSON: %w", err)
}
for dim, level := range m {
if level != PermissionLevelFull && level != PermissionLevelRead && level != PermissionLevelNone {
return nil, fmt.Errorf("invalid permission level '%s' for dimension '%s'", level, dim)
}
}
return m, nil
}
// SetPermissionMap stores non-none valid Chatwoot permissions from a legacy
// permission matrix as the current JSON array shape.
func (cr *CustomRole) SetPermissionMap(m map[PermissionDimension]PermissionLevel) error {
keys := make([]PermissionDimension, 0, len(m))
for dim, level := range m {
if level == PermissionLevelNone || !IsValidCustomRolePermission(dim) {
continue
}
keys = append(keys, dim)
}
return cr.SetPermissionKeys(keys)
}
// Validate checks that the custom role has a valid name and permission dimensions.
func (cr *CustomRole) Validate() error {
if cr.Name == "" {
return fmt.Errorf("custom role name is required")
}
keys, err := cr.GetPermissionKeys()
if err != nil {
return err
}
for _, key := range keys {
if !IsValidCustomRolePermission(key) {
return fmt.Errorf("invalid custom role permission '%s'", key)
}
}
return nil
}
func normalizePermissionKeys(keys []PermissionDimension) ([]PermissionDimension, error) {
seen := map[PermissionDimension]struct{}{}
normalized := make([]PermissionDimension, 0, len(keys))
for _, key := range keys {
if !IsValidCustomRolePermission(key) {
return nil, fmt.Errorf("invalid custom role permission '%s'", key)
}
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
normalized = append(normalized, key)
}
return normalized, nil
}
// CustomRoleValidationError represents a validation error for a custom role.
type CustomRoleValidationError struct {
Dimension string
Level string
Message string
}
func (e *CustomRoleValidationError) Error() string {
return "custom_role validation error: dimension '" + e.Dimension + "' has invalid level '" + e.Level + "' — " + e.Message
}