package model // CustomRole represents an enterprise custom role with fine-grained permissions. // Reference: Chatwoot enterprise/app/models/custom_role.rb // P2E §2.1 — 6 permission dimensions: conversation_manage, conversation_delete, // contact_manage, report_manage, knowledge_base_manage, automation_manage // // CustomRoles allow enterprise accounts to define roles between agent and administrator // with specific permission levels (full/read/none) on each dimension. // // NOTE: PermissionMatrix type is intentionally NOT imported from internal/auth to avoid // a circular dependency (auth -> model -> auth). Instead, the model stores permissions // as raw JSONB string, and the auth/service layers handle deserialization into PermissionMatrix. // See RBACService.GetPermissionMatrix(customRole) for the conversion. import ( "encoding/json" "fmt" "time" "gorm.io/gorm" ) // PermissionDimension constants — duplicated here to avoid circular import with auth package. // These must be kept in sync with auth.PermissionDimension. 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" ) // 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"` // JSONB: {"conversation_manage":"read", ...} 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" } // 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 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) } return m, nil } // SetPermissionMap serializes a permission map into the JSONB permissions field. func (cr *CustomRole) SetPermissionMap(m map[PermissionDimension]PermissionLevel) error { data, err := json.Marshal(m) if err != nil { return fmt.Errorf("failed to serialize custom role permissions: %w", err) } cr.Permissions = string(data) return nil } // 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") } pm, err := cr.GetPermissionMap() if err != nil { return err } for dim, level := range pm { if level != PermissionLevelFull && level != PermissionLevelRead && level != PermissionLevelNone { return fmt.Errorf("invalid permission level '%s' for dimension '%s'", level, dim) } } return 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 }