95 lines
3.9 KiB
Python
95 lines
3.9 KiB
Python
"""
|
|||
|
|
Copyright 2024-2026 ReplyPier
|
||
|
|
|
||
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
you may not use this file except in compliance with the License.
|
||
|
|
You may obtain a copy of the License at
|
||
|
|
|
||
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
|
||
|
|
Unless required by applicable law or agreed to in writing, software
|
||
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
See the License for the specific language governing permissions and
|
||
|
|
limitations under the License.
|
||
|
|
|
||
|
|
What a widget conversation token may do to the customer it names.
|
||
|
|
|
||
|
|
A widget visitor can be bound to a customer by an email they only typed, so the
|
||
|
|
token naming a customer is not proof of being them. Conversations are already
|
||
|
|
fenced by the token's scope (see conversation_token.conversation_scope); this
|
||
|
|
module fences the customer row itself - its contact details, merges, and whether
|
||
|
|
an unverified token may stand for a person the embedding app has signed in.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Optional
|
||
|
|
from uuid import UUID
|
||
|
|
|
||
|
|
from sqlalchemy.orm import Session
|
||
|
|
|
||
|
|
from app.models.customer import Customer
|
||
|
|
from app.models.session_to_agent import SessionToAgent
|
||
|
|
from app.repositories.customer import CustomerRepository
|
||
|
|
from app.repositories.session_to_agent import SessionToAgentRepository
|
||
|
|
from app.services.conversation_token import (
|
||
|
|
LEGACY_SCOPE_PREFIX,
|
||
|
|
is_verified_scope,
|
||
|
|
scope_for_session_key,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def can_act_as(db: Session, customer: Optional[Customer], scope: Optional[str]) -> bool:
|
||
|
|
"""True when a token with `scope` may change `customer` as if it were them.
|
||
|
|
|
||
|
|
A verified token always may. An unverified one only for a customer its own
|
||
|
|
scope created - "no conversations elsewhere" is not ownership: a customer
|
||
|
|
made by a ticket or an import has none - and only while every conversation
|
||
|
|
of that customer is still its own.
|
||
|
|
"""
|
||
|
|
if customer is None or not scope:
|
||
|
|
return False
|
||
|
|
if is_verified_scope(scope):
|
||
|
|
return True
|
||
|
|
if customer.is_authenticated or not _created_by(customer, scope):
|
||
|
|
return False
|
||
|
|
return not SessionToAgentRepository(db).customer_has_sessions_outside_scope(customer.id, scope)
|
||
|
|
|
||
|
|
|
||
|
|
def _created_by(customer: Customer, scope: str) -> bool:
|
||
|
|
if customer.conversation_key:
|
||
|
|
return customer.conversation_key == scope
|
||
|
|
# Customers from before conversation keys: the anonymous row a legacy
|
||
|
|
# widget token created for itself is the only kind such a token can own.
|
||
|
|
return scope.startswith(LEGACY_SCOPE_PREFIX) and CustomerRepository.is_placeholder_email(customer.email)
|
||
|
|
|
||
|
|
|
||
|
|
def may_stand_for(customer: Optional[Customer], scope: Optional[str]) -> bool:
|
||
|
|
"""True when an unverified token may keep naming `customer` at all.
|
||
|
|
|
||
|
|
Once the embedding app has signed a person in, only a token our side vouched
|
||
|
|
for may name them - otherwise whoever claimed the address first would keep a
|
||
|
|
token for the signed-in user's row.
|
||
|
|
"""
|
||
|
|
if customer is None or not scope:
|
||
|
|
return False
|
||
|
|
return is_verified_scope(scope) or not customer.is_authenticated
|
||
|
|
|
||
|
|
|
||
|
|
def trusts_identity_in_conversation(
|
||
|
|
db: Session, customer: Optional[Customer], conversation: Optional[SessionToAgent]
|
||
|
|
) -> bool:
|
||
|
|
"""True when the visitor in `conversation` may be told, or change, what we
|
||
|
|
know of `customer`.
|
||
|
|
|
||
|
|
Reciting a stored name or email back is how a visitor who only typed someone
|
||
|
|
else's address would learn who they are. Unknown conversation: nothing.
|
||
|
|
"""
|
||
|
|
if conversation is None:
|
||
|
|
return False
|
||
|
|
return can_act_as(db, customer, scope_for_session_key(conversation.conversation_key))
|
||
|
|
|
||
|
|
|
||
|
|
def trusts_identity_in_session(db: Session, customer: Optional[Customer], session_id: UUID | str | None) -> bool:
|
||
|
|
"""trusts_identity_in_conversation, for a caller holding only the session id."""
|
||
|
|
conversation = SessionToAgentRepository(db).get_session(session_id) if session_id else None
|
||
|
|
return trusts_identity_in_conversation(db, customer, conversation)
|