security: Hide contact data from non-members in NordaGPT

- Add role-based access control to AI chat context
- Phone/email only visible to users with MEMBER role or higher
- Load User object in send_message() to check can_view_contacts()
- Pass permission through _build_conversation_context() to _company_to_compact_dict()
- Update AI system prompt to inform about contact data availability
- Non-members are directed to company profiles for contact details

This fixes a security gap where contact data was exposed to all users
regardless of their membership status in the organization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-01 21:17:51 +01:00
parent 4181a2e760
commit 579b4636bc

View File

@ -53,7 +53,9 @@ from database import (
CompanyPerson,
CompanySocialMedia,
GBPAudit,
CompanyWebsiteAnalysis
CompanyWebsiteAnalysis,
# User for permission checks
User
)
# Import feedback learning service for few-shot learning
@ -219,6 +221,10 @@ class NordaBizChatEngine:
)
raise PermissionError("Access denied: You don't own this conversation")
# SECURITY: Load User to check permission for viewing contact data
user = db.query(User).filter_by(id=user_id).first()
can_view_contacts = user.can_view_contacts() if user else False
# RODO/GDPR: Sanitize user message - remove sensitive data before storage
# Note: NIP and email are NOT considered sensitive (public business data)
sanitized_message = user_message
@ -246,7 +252,10 @@ class NordaBizChatEngine:
# Build context from conversation history and relevant companies
# Use ORIGINAL message for AI (so it can understand the question)
# but the sanitized version is what gets stored in DB
context = self._build_conversation_context(db, conversation, user_message)
# SECURITY: Pass can_view_contacts to filter contact data based on role
context = self._build_conversation_context(
db, conversation, user_message, can_view_contacts=can_view_contacts
)
# Get AI response with cost tracking
response = self._query_ai(
@ -361,7 +370,8 @@ class NordaBizChatEngine:
self,
db,
conversation: AIChatConversation,
current_message: str
current_message: str,
can_view_contacts: bool = False
) -> Dict[str, Any]:
"""
Build context for AI with ALL companies (not pre-filtered)
@ -373,6 +383,7 @@ class NordaBizChatEngine:
db: Database session
conversation: Current conversation
current_message: User's current message (for reference only)
can_view_contacts: Whether user can view contact data (requires MEMBER role)
Returns:
Context dict with ALL companies and categories
@ -382,7 +393,9 @@ class NordaBizChatEngine:
context = {
'conversation_type': conversation.conversation_type,
'total_companies': len(all_companies)
'total_companies': len(all_companies),
# SECURITY: Store permission for contact data visibility
'can_view_contacts': can_view_contacts
}
# Get all categories with company counts
@ -398,8 +411,9 @@ class NordaBizChatEngine:
# Include ALL companies in compact format to minimize tokens
# AI will intelligently select the most relevant ones
# SECURITY: Pass can_view_contacts to filter phone/email based on user role
context['all_companies'] = [
self._company_to_compact_dict(c)
self._company_to_compact_dict(c, can_view_contacts=can_view_contacts)
for c in all_companies
]
@ -631,13 +645,19 @@ class NordaBizChatEngine:
return context
def _company_to_compact_dict(self, c: Company) -> Dict[str, Any]:
def _company_to_compact_dict(
self, c: Company, can_view_contacts: bool = False
) -> Dict[str, Any]:
"""
Convert company to compact dictionary for AI context.
Optimized to minimize tokens while keeping all important data.
SECURITY: Phone and email are only included if user has MEMBER role or higher.
This prevents non-members from accessing contact information through AI chat.
Args:
c: Company object
can_view_contacts: Whether to include phone/email (requires MEMBER role)
Returns:
Compact dict with essential company info
@ -663,10 +683,14 @@ class NordaBizChatEngine:
compact['comp'] = competencies
if c.website:
compact['web'] = c.website
if c.phone:
compact['tel'] = c.phone
if c.email:
compact['mail'] = c.email
# SECURITY: Contact data (phone, email) only for MEMBER role or higher
if can_view_contacts:
if c.phone:
compact['tel'] = c.phone
if c.email:
compact['mail'] = c.email
if c.address_city:
compact['city'] = c.address_city
if c.year_established:
@ -918,7 +942,7 @@ class NordaBizChatEngine:
🎯 TWOJA ROLA:
- Analizujesz CAŁĄ bazę firm i wybierasz najlepsze dopasowania do pytania użytkownika
- Odpowiadasz zwięźle (2-3 zdania), chyba że użytkownik prosi o szczegóły
- Podajesz konkretne nazwy firm z kontaktem
- Podajesz konkretne nazwy firm z kontaktem (jeśli dostępny)
- Możesz wyszukiwać po: nazwie firmy, usługach, kompetencjach, właścicielach (w history), mieście
- Możesz cytować rekomendacje innych członków
- Możesz informować o aktualnych newsach, wydarzeniach, ogłoszeniach i dyskusjach na forum
@ -931,7 +955,8 @@ class NordaBizChatEngine:
- history: historia firmy, właściciele, założyciele
- svc: usługi
- comp: kompetencje
- web/tel/mail: kontakt
- web: strona www
- tel/mail: telefon i email (dostępne tylko dla członków Izby)
- city: miasto
- cert: certyfikaty
@ -1110,6 +1135,19 @@ W dyskusji [Artur Wiertel](link) pytał o moderację. Pełna treść: [moje uwag
system_prompt += """
TRYB SZYBKI - odpowiadaj zwięźle ale z PEŁNYMI linkami do firm i tematów.
"""
# SECURITY: Inform AI about contact data availability based on user's role
if context.get('can_view_contacts', False):
system_prompt += """
🔓 DANE KONTAKTOWE: Użytkownik jest członkiem Izby - możesz podawać numery telefonów i adresy email firm.
"""
else:
system_prompt += """
🔒 DANE KONTAKTOWE: Użytkownik NIE jest członkiem Izby - NIE podawaj telefonów ani emaili.
Zamiast tego kieruj na profil firmy: "Szczegóły kontaktowe znajdziesz na profilu: [Nazwa firmy](link)"
"""
# Add feedback-based learning context (few-shot examples)