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 }