feat(ip): protect persona 'formula' — secret fields only super_admin sees/edits
IP protection so casual copying yields inferior results: - SECRET_PERSONA_FIELDS (pains/objections/negotiation_levers/opener/tolerance + pain rootCause/resolutionConditions): only super_admin can view/edit them. - list_personas/get_persona/update_persona/get_group strip these for role=admin (and hide sales_kit + pain-fit report from admins too). - update_persona rejects admin attempts to set secret fields (403). - PersonaForm hides the 'การขาย' recipe section for non-super-admin (shows locked note); auth.isSuperAdmin getter added. Rebuilt dist. Added test_ip_protection.
This commit is contained in:
@@ -14,6 +14,29 @@ from .helpers import ApiError, current_user, require_auth, require_roles
|
||||
|
||||
groups_bp = Blueprint("groups", __name__)
|
||||
|
||||
# Fields that encode the "formula"/process of a persona. Only super_admin may see/edit
|
||||
# them; admins get the persona but NOT these — so a casual copy yields inferior results.
|
||||
SECRET_PERSONA_FIELDS = {
|
||||
"pains", "objections", "negotiation_levers", "opener",
|
||||
"rootCause", "resolutionConditions", "tolerance",
|
||||
}
|
||||
|
||||
|
||||
def strip_secret_fields(persona: dict) -> dict:
|
||||
"""Return a copy of a persona with secret/process fields removed."""
|
||||
out = dict(persona)
|
||||
for f in SECRET_PERSONA_FIELDS:
|
||||
out.pop(f, None)
|
||||
pains = out.get("pains")
|
||||
if isinstance(pains, list):
|
||||
cleaned = []
|
||||
for p in pains:
|
||||
if isinstance(p, dict):
|
||||
p = {k: v for k, v in p.items() if k not in ("rootCause", "resolutionConditions")}
|
||||
cleaned.append(p)
|
||||
out["pains"] = cleaned
|
||||
return out
|
||||
|
||||
_ANALYZE_LOCKS: dict[str, threading.Lock] = {}
|
||||
_ANALYZE_GUARD = threading.Lock()
|
||||
|
||||
@@ -203,6 +226,12 @@ def get_group(gid: str):
|
||||
view["personas"] = [revealable_view(p) for p in group.get("personas", [])]
|
||||
view["sales_kit"] = None
|
||||
view["report"] = None
|
||||
elif actor.get("role") != "super_admin":
|
||||
# admin: see group, but not secret/process persona fields nor the sales-kit/report
|
||||
# (pain-fit analysis is the IP to protect).
|
||||
view["personas"] = [strip_secret_fields(p) for p in group.get("personas", [])]
|
||||
view["sales_kit"] = None
|
||||
view["report"] = None
|
||||
return jsonify({"group": view})
|
||||
|
||||
|
||||
@@ -216,8 +245,11 @@ def list_personas(gid: str):
|
||||
if group.get("status") != "ready":
|
||||
raise ApiError("group not ready", 403)
|
||||
personas = [revealable_view(p) for p in group.get("personas", [])]
|
||||
else:
|
||||
elif actor.get("role") == "super_admin":
|
||||
personas = group.get("personas", [])
|
||||
else:
|
||||
# admin: see persona but not the secret/process fields (IP protection)
|
||||
personas = [strip_secret_fields(p) for p in group.get("personas", [])]
|
||||
# attach per-user status (won/lost/not-tried) for trainees
|
||||
if actor.get("role") == "user":
|
||||
sess = _stores().get("session_store")
|
||||
@@ -244,7 +276,10 @@ def get_persona(gid: str, pid: str):
|
||||
ensure = ensure_persona_shape(p)
|
||||
if actor.get("role") == "user":
|
||||
return jsonify({"persona": revealable_view(ensure)})
|
||||
return jsonify({"persona": ensure})
|
||||
if actor.get("role") == "super_admin":
|
||||
return jsonify({"persona": ensure})
|
||||
# admin: hidden secret/process fields (IP protection)
|
||||
return jsonify({"persona": strip_secret_fields(ensure)})
|
||||
|
||||
|
||||
@groups_bp.put("/<gid>/personas/<pid>")
|
||||
@@ -254,13 +289,22 @@ def update_persona(gid: str, pid: str):
|
||||
s = _stores()
|
||||
_get_owned_group(s, gid)
|
||||
data = request.get_json(silent=True) or {}
|
||||
actor = current_user()
|
||||
# IP protection: only super_admin may set/alter secret formula fields.
|
||||
if actor.get("role") != "super_admin":
|
||||
for f in SECRET_PERSONA_FIELDS:
|
||||
if f in data:
|
||||
raise ApiError(f"field '{f}' is locked (super_admin only)", 403)
|
||||
try:
|
||||
updated = s["groups"].update_persona(gid, pid, data)
|
||||
except ValueError as exc:
|
||||
raise ApiError(str(exc), 404)
|
||||
return jsonify({"persona": ensure_persona_shape(updated["personas"][
|
||||
full = ensure_persona_shape(updated["personas"][
|
||||
next(i for i, p in enumerate(updated["personas"]) if p["id"] == pid)
|
||||
])})
|
||||
])
|
||||
if actor.get("role") == "super_admin":
|
||||
return jsonify({"persona": full})
|
||||
return jsonify({"persona": strip_secret_fields(full)})
|
||||
|
||||
|
||||
@groups_bp.post("/<gid>/reanalyze")
|
||||
|
||||
Reference in New Issue
Block a user