tawasuyu

tawasuyu/mirada

Sovereign tiling Wayland compositor + window manager in Rust, on Llimphi.

Rust 50Markdown 30TOML 211282 Definiciones

⎇ 3dc85ebdcdd5fda0a1be0db5f8fbce5859266f2e shared/auth/auth-core/src/ticket.rs Rust · 175 líneas · 5.9 KB Historia Quién tocó qué crudo

feat: mirada standalone — compositor Wayland + WM sobre Llimphi (build magro) 3dc85ebdc · Sergio ·

1//! El tiquet de sesión: lo que el greeter le entrega al compositor tras
2//! una autenticación exitosa.
3//!
4//! El greeter de carmen corre como proceso hijo del compositor. Cuando
5//! el login tiene éxito, imprime **una línea** de tiquet a su stdout; el
6//! compositor escanea las líneas del hijo buscando el prefijo
7//! [`TICKET_TAG`] y, al encontrarlo, hace el traspaso a modo sesión.
8
9use std::path::PathBuf;
10
11use crate::UserInfo;
12
13/// Etiqueta + versión de la línea de tiquet. El compositor sólo trata
14/// como tiquet las líneas que empiezan con esto — el resto del stdout
15/// del greeter (logs, ruido) se ignora.
16pub const TICKET_TAG: &str = "MIRADA-SESSION-TICKET-v1";
17
18/// Resultado de un login: la identidad autenticada más, opcionalmente,
19/// el comando de sesión elegido. El greeter lo produce; el compositor lo
20/// consume para arrancar la sesión (setuid al usuario + spawn).
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct SessionTicket {
23    /// Identidad del usuario autenticado.
24    pub user: UserInfo,
25    /// Comando de sesión a ejecutar como el usuario. Vacío = que el
26    /// compositor decida (su autostart por defecto).
27    pub session: String,
28    /// `true` si la sesión es un compositor **ajeno** (sway, Plasma…): el
29    /// servidor actual debe soltar el DRM y hacer `exec`, no correrla como
30    /// cliente. `false` para sesiones nativas de mirada (pata, autostart),
31    /// que sí corren como clientes del mismo compositor.
32    pub foreign: bool,
33}
34
35impl SessionTicket {
36    /// Crea un tiquet sin comando de sesión explícito.
37    pub fn new(user: UserInfo) -> Self {
38        Self {
39            user,
40            session: String::new(),
41            foreign: false,
42        }
43    }
44
45    /// Fija el comando de sesión. Encadenable.
46    pub fn with_session(mut self, session: impl Into<String>) -> Self {
47        self.session = session.into();
48        self
49    }
50
51    /// Marca la sesión como compositor ajeno (handoff por `exec`).
52    /// Encadenable.
53    pub fn foreign(mut self, foreign: bool) -> Self {
54        self.foreign = foreign;
55        self
56    }
57
58    /// Serializa el tiquet a una línea única, apta para stdout. Campos
59    /// separados por tabulador: ni los nombres de usuario, ni los paths,
60    /// ni los comandos de sesión suelen contener tabuladores.
61    pub fn to_line(&self) -> String {
62        format!(
63            "{TICKET_TAG}\t{}\t{}\t{}\t{}\t{}\t{}\t{}",
64            self.user.name,
65            self.user.uid,
66            self.user.gid,
67            self.user.home.display(),
68            self.user.shell.display(),
69            self.session,
70            if self.foreign { "1" } else { "0" },
71        )
72    }
73
74    /// Parsea una línea producida por [`to_line`]. `None` si la línea no
75    /// es un tiquet (otra salida del greeter) o está malformada.
76    pub fn from_line(line: &str) -> Option<SessionTicket> {
77        let mut f = line.trim_end_matches(['\r', '\n']).split('\t');
78        if f.next()? != TICKET_TAG {
79            return None;
80        }
81        let name = f.next()?.to_string();
82        let uid = f.next()?.parse().ok()?;
83        let gid = f.next()?.parse().ok()?;
84        let home = PathBuf::from(f.next()?);
85        let shell = PathBuf::from(f.next()?);
86        // El comando de sesión puede venir vacío.
87        let session = f.next().unwrap_or("").to_string();
88        // El flag `foreign` es opcional (tiquets viejos no lo traen).
89        let foreign = matches!(f.next(), Some("1"));
90        Some(SessionTicket {
91            user: UserInfo {
92                name,
93                uid,
94                gid,
95                home,
96                shell,
97            },
98            session,
99            foreign,
100        })
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    fn sample() -> UserInfo {
109        UserInfo {
110            name: "sergio".into(),
111            uid: 1000,
112            gid: 1000,
113            home: PathBuf::from("/home/sergio"),
114            shell: PathBuf::from("/usr/bin/bash"),
115        }
116    }
117
118    #[test]
119    fn round_trip_without_session() {
120        let t = SessionTicket::new(sample());
121        let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
122        assert_eq!(back, t);
123        assert!(back.session.is_empty());
124    }
125
126    #[test]
127    fn round_trip_with_session() {
128        let t = SessionTicket::new(sample()).with_session("shuma-shell --launcher");
129        let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
130        assert_eq!(back, t);
131        assert_eq!(back.session, "shuma-shell --launcher");
132    }
133
134    #[test]
135    fn round_trip_with_foreign() {
136        let t = SessionTicket::new(sample())
137            .with_session("sway")
138            .foreign(true);
139        let back = SessionTicket::from_line(&t.to_line()).expect("parsea");
140        assert_eq!(back, t);
141        assert!(back.foreign);
142    }
143
144    #[test]
145    fn foreign_defaults_false_sin_campo() {
146        // Una línea estilo v1 (sin el campo foreign) parsea con foreign=false.
147        let line = format!("{TICKET_TAG}\tsergio\t1000\t1000\t/home/sergio\t/usr/bin/bash\tsway");
148        let back = SessionTicket::from_line(&line).expect("parsea");
149        assert!(!back.foreign);
150        assert_eq!(back.session, "sway");
151    }
152
153    #[test]
154    fn from_line_ignores_non_ticket() {
155        assert!(SessionTicket::from_line("[INFO] arrancando greeter").is_none());
156        assert!(SessionTicket::from_line("").is_none());
157    }
158
159    #[test]
160    fn from_line_rejects_malformed() {
161        // Prefijo correcto pero faltan campos.
162        assert!(SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio")).is_none());
163        // uid no numérico.
164        assert!(
165            SessionTicket::from_line(&format!("{TICKET_TAG}\tsergio\tXX\t1000\t/h\t/sh\t"))
166                .is_none()
167        );
168    }
169
170    #[test]
171    fn tolerates_trailing_newline() {
172        let line = format!("{}\n", SessionTicket::new(sample()).to_line());
173        assert!(SessionTicket::from_line(&line).is_some());
174    }
175}