85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
"""Machine PAM indépendante de GTK pour préserver les invites multiples."""
|
|
from greetd_ipc import Client, ProtocolError, session_command
|
|
|
|
|
|
class AuthFlow:
|
|
def __init__(self, socket_path=None):
|
|
self.socket_path = socket_path
|
|
self.client = None
|
|
self.username = None
|
|
self.pending = None
|
|
self.authenticated = False
|
|
self.selection = None
|
|
|
|
def begin(self, username, selection="stable"):
|
|
self.reset()
|
|
session_command(selection)
|
|
if type(username) is not str or not username or len(username) > 256 or any(ord(c) < 32 for c in username):
|
|
raise ValueError("Nom utilisateur invalide")
|
|
# CONTRACT: compte et choix sont figés pour toute cette conversation PAM.
|
|
# Le thread GTK ne les modifie pas ; un nouveau begin annule l'ancien état.
|
|
self.username = username
|
|
self.selection = selection
|
|
try:
|
|
self.client = Client(self.socket_path)
|
|
return self._accept(self.client.create(username))
|
|
except (OSError, ProtocolError):
|
|
self.reset()
|
|
raise
|
|
|
|
def answer(self, value=None):
|
|
if self.pending is None or self.client is None:
|
|
raise ProtocolError("Aucune invite PAM active")
|
|
kind = self.pending["auth_message_type"]
|
|
if kind in ("secret", "visible") and value is None:
|
|
raise ProtocolError("Réponse PAM requise")
|
|
if kind in ("info", "error"):
|
|
value = None
|
|
self.pending = None
|
|
try:
|
|
return self._accept(self.client.answer(value))
|
|
except (OSError, ProtocolError):
|
|
self.reset()
|
|
raise
|
|
|
|
def _accept(self, reply):
|
|
self.pending = None
|
|
if reply["type"] == "auth_message":
|
|
self.pending = reply
|
|
elif reply["type"] == "success":
|
|
self.authenticated = True
|
|
elif reply["type"] == "error":
|
|
self.reset()
|
|
return reply
|
|
|
|
def start(self):
|
|
if not self.authenticated or self.client is None:
|
|
raise ProtocolError("Session non authentifiée")
|
|
# INVARIANT: un seul start par authentification, succès ou refus ; aucun repli.
|
|
self.authenticated = False
|
|
try:
|
|
reply = self.client.start(self.selection)
|
|
except (OSError, ProtocolError):
|
|
self.reset()
|
|
raise
|
|
if reply["type"] == "success":
|
|
self.client.close()
|
|
self.client = None
|
|
self.username = None
|
|
self.selection = None
|
|
else:
|
|
self.reset()
|
|
return reply
|
|
|
|
def reset(self):
|
|
if self.client is not None:
|
|
try:
|
|
self.client.cancel()
|
|
except (OSError, ProtocolError):
|
|
pass
|
|
self.client.close()
|
|
self.client = None
|
|
self.username = None
|
|
self.selection = None
|
|
self.pending = None
|
|
self.authenticated = False
|