209 lines
7.3 KiB
Go
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
|
|
}
|