1mod helper;
13#[cfg(test)]
14mod tests;
15
16use std::cell::{Cell, RefCell};
17use std::collections::{HashMap, VecDeque};
18use std::future::poll_fn;
19use std::pin::Pin;
20use std::rc::Rc;
21use std::sync::Once;
22
23use calloop::Interest;
24use crabshell_reactive::{ReadSignal, Signal};
25use crabshell_ui::Cx;
26use zbus::export::futures_core::Stream;
27use zbus::message::{Flags, Type as Kind};
28use zbus::{Connection, Message, MessageStream, OwnedVariant, Variant};
29
30use self::helper::{Helper, Line, Secret};
31use crate::Shared;
32use crate::dbus::{CALL_TIMEOUT, bus_call, owner_changed, owner_rule, set_changed};
33
34const AUTHORITY: &str = "org.freedesktop.PolicyKit1";
35const AUTHORITY_PATH: &str = "/org/freedesktop/PolicyKit1/Authority";
36const AUTHORITY_INTERFACE: &str = "org.freedesktop.PolicyKit1.Authority";
37const AGENT_INTERFACE: &str = "org.freedesktop.PolicyKit1.AuthenticationAgent";
38const AGENT_PATH: &str = "/org/crabshell/PolkitAgent";
40
41pub(crate) const HELPER: &str = "/usr/lib/polkit-1/polkit-agent-helper-1";
45
46const FAILED: &str = "org.freedesktop.PolicyKit1.Error.Failed";
47const CANCELLED: &str = "org.freedesktop.PolicyKit1.Error.Cancelled";
48const ACCESS_DENIED: &str = "org.freedesktop.DBus.Error.AccessDenied";
49const INVALID_ARGS: &str = "org.freedesktop.DBus.Error.InvalidArgs";
50const UNKNOWN_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
51
52const MAX_TEXT: usize = 4 * 1024;
54const MAX_TRIES: u32 = 3;
56const MAX_IDENTITIES: usize = 64;
58const MAX_QUEUED: usize = 8;
60const MAX_LINES: usize = 16;
62
63#[derive(Clone, Debug, PartialEq)]
66pub struct AuthRequest {
67 id: u64,
70 pub action_id: String,
72 pub message: String,
74 pub icon_name: String,
76 pub identities: Vec<String>,
79 pub state: AuthState,
81 pub info: Vec<String>,
84 pub errors: Vec<String>,
86}
87
88#[derive(Clone, Debug, PartialEq)]
90pub enum AuthState {
91 Waiting,
93 Asking {
97 message: String,
99 visible: bool,
101 },
102 Busy,
104 Failed {
107 message: String,
109 },
110}
111
112#[derive(Clone)]
115pub struct PolkitAgent {
116 service: Option<Rc<Service>>,
118 request: ReadSignal<Option<AuthRequest>>,
119}
120
121impl PolkitAgent {
122 pub fn request(&self) -> ReadSignal<Option<AuthRequest>> {
125 self.request
126 }
127
128 pub fn respond(&self, text: impl Into<String>) {
133 let secret = Secret(text.into().into_bytes());
134 if let Some(service) = &self.service {
135 service.respond(secret);
136 }
137 }
138
139 pub fn cancel(&self) {
142 if let Some(service) = &self.service {
143 service.finish(Some((CANCELLED, "the user cancelled")));
144 }
145 }
146
147 pub fn pick_identity(&self, name: &str) {
151 if let Some(service) = &self.service {
152 service.pick(name);
153 }
154 }
155}
156
157pub(crate) fn polkit_agent(cx: &Cx) -> PolkitAgent {
159 let Some(service) = cx.service::<Service>() else {
160 static LOGGED: Once = Once::new();
161 LOGGED.call_once(|| {
162 tracing::warn!("cx.polkit_agent() needs the services the shell provides; no requests arrive");
163 });
164 return PolkitAgent {
165 service: None,
166 request: cx.signal(None).read_only(),
167 };
168 };
169 let request = service.start();
170 PolkitAgent {
171 service: Some(service),
172 request,
173 }
174}
175
176pub(crate) struct Service {
178 shared: Rc<Shared>,
179 state: RefCell<Option<State>>,
180 next: Cell<u64>,
182}
183
184struct State {
185 request: Signal<Option<AuthRequest>>,
186 connection: Option<Connection>,
188 authority: Option<String>,
191 current: Option<Current>,
192 queue: VecDeque<Begin>,
194}
195
196struct StopOnDrop(Rc<Service>);
199
200impl Drop for StopOnDrop {
201 fn drop(&mut self) {
202 self.0.forget();
203 }
204}
205
206struct Begin {
208 call: Message,
209 cookie: String,
210 shown: AuthRequest,
211}
212
213struct Current {
215 begin: Begin,
216 failures: u32,
217 helper: Option<Helper>,
218 answer: Option<Secret>,
220}
221
222impl Current {
223 fn new(begin: Begin) -> Current {
224 Current {
225 begin,
226 failures: 0,
227 helper: None,
228 answer: None,
229 }
230 }
231}
232
233impl Service {
234 pub(crate) fn new(shared: Rc<Shared>) -> Service {
235 Service {
236 shared,
237 state: RefCell::new(None),
238 next: Cell::new(1),
239 }
240 }
241
242 fn next(&self) -> u64 {
243 let next = self.next.get();
244 self.next.set(next + 1);
245 next
246 }
247
248 fn start(self: &Rc<Self>) -> ReadSignal<Option<AuthRequest>> {
250 if let Some(state) = &*self.state.borrow() {
251 return state.request.read_only();
252 }
253 let request = self.shared.root.signal(None);
254 *self.state.borrow_mut() = Some(State {
255 request,
256 connection: None,
257 authority: None,
258 current: None,
259 queue: VecDeque::new(),
260 });
261 self.shared.root.signal(StopOnDrop(Rc::clone(self)));
262 match self.shared.config.system_bus.clone() {
263 Some(address) => self.shared.spawn(run(Rc::clone(self), address)),
264 None => tracing::info!("no system bus, so no polkit requests arrive"),
265 }
266 request.read_only()
267 }
268
269 fn forget(&self) {
272 if let Some(state) = self.state.borrow_mut().as_mut() {
273 state.connection = None;
274 }
275 self.drop_requests();
276 }
277
278 fn drop_requests(&self) {
281 let current = self.state.borrow_mut().as_mut().and_then(|state| {
282 state.queue.clear();
283 state.authority = None;
284 state.current.take()
285 });
286 if let Some(helper) = current.and_then(|current| current.helper) {
287 helper.stop(&self.shared, true);
288 }
289 }
290
291 fn alive(&self) -> bool {
294 let state = self.state.borrow();
295 let alive = state
296 .as_ref()
297 .and_then(|state| state.request.try_with(|_| ()));
298 alive.is_some()
299 }
300
301 fn with_current<R>(&self, f: impl FnOnce(&mut Current) -> R) -> Option<R> {
303 let mut state = self.state.borrow_mut();
304 state.as_mut()?.current.as_mut().map(f)
305 }
306
307 fn publish(&self) {
309 let shown = self.state.borrow().as_ref().map(|state| {
310 let current = state.current.as_ref();
311 (
312 state.request,
313 current.map(|current| current.begin.shown.clone()),
314 )
315 });
316 if let Some((signal, shown)) = shown {
317 set_changed(signal, shown);
318 }
319 }
320
321 fn on_call(self: &Rc<Self>, call: Message) {
323 let header = call.header();
324 let sender = header.sender().map(|sender| sender.as_str());
325 let authority = self
326 .state
327 .borrow()
328 .as_ref()
329 .and_then(|state| state.authority.clone());
330 if sender.is_none() || sender != authority.as_deref() {
331 tracing::warn!(
332 ?sender,
333 "a program other than polkit called the agent; refused"
334 );
335 return self.reply(
336 &call,
337 Some((ACCESS_DENIED, "only polkit may call the agent")),
338 );
339 }
340 let path = header.path().map(|path| path.as_str());
341 let interface = header.interface().map(|interface| interface.as_str());
342 let member = header.member().map(|member| member.as_str());
343 match (path, interface, member) {
344 (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("BeginAuthentication")) => self.begin(call),
345 (Some(AGENT_PATH), Some(AGENT_INTERFACE), Some("CancelAuthentication")) => {
346 self.cancelled(call);
347 }
348 _ => self.reply(&call, Some((UNKNOWN_METHOD, "no such method"))),
349 }
350 }
351
352 fn begin(self: &Rc<Self>, call: Message) {
354 let begin = match begin_args(&call, self.next()) {
355 Ok(begin) => begin,
356 Err((error, why)) => {
357 tracing::warn!(why, "a polkit request the agent can't answer");
358 return self.reply(&call, Some((error, why)));
359 }
360 };
361 let refused = {
362 let mut state = self.state.borrow_mut();
363 let Some(state) = state.as_mut() else {
364 return;
365 };
366 let action = &begin.shown.action_id;
367 tracing::info!(action, "polkit asks to authenticate");
368 if state.current.is_none() {
369 state.current = Some(Current::new(begin));
370 None
371 } else if state.queue.len() < MAX_QUEUED {
372 state.queue.push_back(begin);
373 return;
374 } else {
375 Some(begin)
376 }
377 };
378 match refused {
379 Some(begin) => {
380 tracing::warn!("{MAX_QUEUED} polkit requests wait already; refused another");
381 self.reply(&begin.call, Some((FAILED, "too many requests wait")));
382 }
383 None => self.run_helper(),
384 }
385 }
386
387 fn cancelled(self: &Rc<Self>, call: Message) {
389 let Ok(cookie) = call.body().deserialize::<String>() else {
390 return self.reply(&call, Some((INVALID_ARGS, "not a cookie")));
391 };
392 self.reply(&call, None);
393 let why = "polkit cancelled it";
394 let current = self.with_current(|current| current.begin.cookie == cookie);
395 if current == Some(true) {
396 tracing::info!("polkit cancelled the request");
397 return self.finish(Some((CANCELLED, why)));
398 }
399 let queued = self.state.borrow_mut().as_mut().and_then(|state| {
400 let at = state
401 .queue
402 .iter()
403 .position(|begin| begin.cookie == cookie)?;
404 state.queue.remove(at)
405 });
406 if let Some(begin) = queued {
407 self.reply(&begin.call, Some((CANCELLED, why)));
408 }
409 }
410
411 fn pick(self: &Rc<Self>, name: &str) {
413 let picked = self.with_current(|current| {
414 let identities = &mut current.begin.shown.identities;
415 let at = identities.iter().position(|known| known == name)?;
416 identities[..=at].rotate_right(1);
417 current.answer = None;
418 Some(current.helper.take())
419 });
420 let Some(helper) = picked.flatten() else {
421 return tracing::warn!(name, "no such identity to pick for polkit");
422 };
423 if let Some(helper) = helper {
424 helper.stop(&self.shared, true);
425 }
426 self.run_helper();
427 }
428
429 fn run_helper(self: &Rc<Self>) {
431 let Some(event_loop) = self.shared.event_loop() else {
432 return;
433 };
434 let run = self.next();
435 let started = self.with_current(|current| {
436 let shown = &mut current.begin.shown;
437 shown.state = AuthState::Waiting;
438 shown.info.clear();
439 shown.errors.clear();
440 let program = &self.shared.config.polkit_helper;
441 let mut helper = Helper::start(program, &shown.identities[0], run)?;
442 if let Err(error) = helper.send(current.begin.cookie.as_bytes()) {
443 helper.stop(&self.shared, true);
444 return Err(error);
445 }
446 let service = Rc::clone(self);
447 let ready = Box::new(move || service.on_output(run));
448 helper.token = helper
449 .stdout_fd()
450 .and_then(|fd| event_loop.insert_fd(fd, Interest::READ, ready));
451 if helper.token.is_none() {
452 helper.stop(&self.shared, true);
453 return Err(std::io::Error::other("its output couldn't be watched"));
454 }
455 current.helper = Some(helper);
456 Ok(())
457 });
458 match started {
459 Some(Ok(())) => self.publish(),
460 Some(Err(error)) => {
461 let program = self.shared.config.polkit_helper.display();
462 tracing::warn!(%error, %program, "couldn't start polkit's helper");
463 self.finish(Some((FAILED, "polkit's helper couldn't start")));
464 }
465 None => {}
466 }
467 }
468
469 fn on_output(self: &Rc<Self>, run: u64) {
471 let running = |service: &Service| {
472 let running =
473 service.with_current(|current| current.helper.as_ref().map(|helper| helper.run));
474 running.flatten() == Some(run)
475 };
476 if !running(self) {
477 return;
478 }
479 let read = self.with_current(|current| current.helper.as_mut()?.read());
480 let Some((lines, ended)) = read.flatten() else {
481 return;
482 };
483 for line in lines {
484 if !running(self) {
485 return;
486 }
487 self.on_line(helper::parse(&line));
488 }
489 if ended && running(self) {
490 tracing::warn!("polkit's helper ended without an answer");
491 self.failed();
492 }
493 }
494
495 fn on_line(self: &Rc<Self>, line: Line) {
496 let keep = |lines: &mut Vec<String>, text| {
497 if lines.len() == MAX_LINES {
498 lines.remove(0);
499 }
500 lines.push(text);
501 };
502 let verdict = line_verdict(&line);
503 let answer = self.with_current(|current| {
504 let shown = &mut current.begin.shown;
505 match line {
506 Line::Prompt { message, visible } => {
507 shown.state = AuthState::Asking { message, visible };
508 return current.answer.take();
509 }
510 Line::Info(text) => keep(&mut shown.info, text),
511 Line::Error(text) => keep(&mut shown.errors, text),
512 Line::Unknown => tracing::debug!("polkit's helper printed a line it doesn't know; skipped"),
513 Line::Success | Line::Failure => {}
514 }
515 None
516 });
517 match verdict {
518 Some(true) => {
519 tracing::info!("authenticated for polkit");
520 self.finish(None);
521 }
522 Some(false) => self.failed(),
523 None => match answer.flatten() {
524 Some(secret) => self.send(secret),
526 None => self.publish(),
527 },
528 }
529 }
530
531 fn failed(self: &Rc<Self>) {
534 let gave_up = self.with_current(|current| {
535 if let Some(helper) = current.helper.take() {
536 helper.stop(&self.shared, true);
537 }
538 current.answer = None;
539 current.failures += 1;
540 let shown = &mut current.begin.shown;
541 let message = shown.errors.last().cloned();
542 let message = message.unwrap_or_else(|| String::from("Authentication failed"));
543 shown.state = AuthState::Failed { message };
544 current.failures >= MAX_TRIES
545 });
546 match gave_up {
547 Some(true) => {
548 tracing::info!("polkit authentication failed {MAX_TRIES} times");
549 self.finish(Some((FAILED, "authentication failed")));
550 }
551 Some(false) => self.publish(),
552 None => {}
553 }
554 }
555
556 fn respond(self: &Rc<Self>, secret: Secret) {
558 if secret.0.contains(&b'\n') || secret.0.contains(&0) {
561 return tracing::warn!("a polkit answer with a line break or NUL was dropped");
562 }
563 let state = self.with_current(|current| current.begin.shown.state.clone());
564 match state {
565 Some(AuthState::Asking { .. }) => self.send(secret),
566 Some(AuthState::Failed { .. }) => {
567 self.with_current(|current| current.answer = Some(secret));
568 self.run_helper();
569 }
570 _ => tracing::debug!("no polkit question to answer; an answer was dropped"),
571 }
572 }
573
574 fn send(self: &Rc<Self>, secret: Secret) {
577 let sent = self.with_current(|current| {
578 let sent = current.helper.as_mut().map(|helper| helper.send(&secret.0));
579 if let Some(Ok(())) = sent {
580 current.begin.shown.state = AuthState::Busy;
581 }
582 sent
583 });
584 match sent.flatten() {
585 Some(Ok(())) => self.publish(),
586 Some(Err(error)) => {
587 tracing::warn!(%error, "couldn't answer polkit's helper");
588 self.failed();
589 }
590 None => {}
591 }
592 }
593
594 fn finish(self: &Rc<Self>, error: Option<(&'static str, &str)>) {
598 let current = self.state.borrow_mut().as_mut().and_then(|state| {
599 let current = state.current.take()?;
600 state.current = state.queue.pop_front().map(Current::new);
601 Some(current)
602 });
603 let Some(current) = current else {
604 return;
605 };
606 if let Some(helper) = current.helper {
607 helper.stop(&self.shared, error.is_some());
609 }
610 self.reply(¤t.begin.call, error);
611 match self.with_current(|_| ()) {
612 Some(()) => self.run_helper(),
613 None => self.publish(),
614 }
615 }
616
617 fn reply(&self, call: &Message, error: Option<(&str, &str)>) {
620 let header = call.header();
621 if header.primary().flags().contains(Flags::NO_REPLY_EXPECTED) {
622 return;
623 }
624 let reply = match error {
625 None => Message::method_return(&header).build(&()),
626 Some((name, why)) => Message::error(&header, name).build(&why),
627 };
628 let connection = self
629 .state
630 .borrow()
631 .as_ref()
632 .and_then(|state| state.connection.clone());
633 let Some(connection) = connection else {
634 return;
635 };
636 self.shared.spawn(async move {
637 let sent = match reply {
638 Ok(reply) => connection.send(&reply).await,
639 Err(error) => Err(error),
640 };
641 if let Err(error) = sent {
642 tracing::debug!(%error, "the polkit agent's answer wasn't sent");
643 }
644 });
645 }
646}
647
648fn line_verdict(line: &Line) -> Option<bool> {
651 match line {
652 Line::Success => Some(true),
653 Line::Failure => Some(false),
654 _ => None,
655 }
656}
657
658type BeginArgs<'a> = (
661 &'a str,
662 &'a str,
663 &'a str,
664 HashMap<&'a str, &'a str>,
665 &'a str,
666 Vec<(&'a str, HashMap<&'a str, OwnedVariant>)>,
667);
668
669fn begin_args(call: &Message, id: u64) -> Result<Begin, (&'static str, &'static str)> {
671 let body = call.body();
672 let Ok((action_id, message, icon_name, _, cookie, identities)) =
673 body.deserialize::<BeginArgs<'_>>()
674 else {
675 return Err((INVALID_ARGS, "not a polkit request"));
676 };
677 if cookie.is_empty() || cookie.len() > MAX_TEXT || cookie.contains('\n') {
679 return Err((FAILED, "a bad cookie"));
680 }
681 let passwd = std::fs::read_to_string("/etc/passwd").unwrap_or_default();
682 let mut users: Vec<(u32, String)> = identities
683 .iter()
684 .take(MAX_IDENTITIES)
685 .filter(|(kind, _)| *kind == "unix-user")
686 .filter_map(|(_, details)| u32::try_from(details.get("uid")?).ok())
687 .filter_map(|uid| Some((uid, user_name(&passwd, uid)?)))
688 .collect();
689 let own = rustix::process::getuid().as_raw();
692 if let Some(at) = users.iter().position(|(uid, _)| *uid == own) {
693 users[..=at].rotate_right(1);
694 }
695 let names: Vec<String> = users.into_iter().map(|(_, name)| name).collect();
696 if names.is_empty() {
697 return Err((FAILED, "no user the agent knows may answer"));
698 }
699 Ok(Begin {
700 call: call.clone(),
701 cookie: cookie.to_owned(),
702 shown: AuthRequest {
703 id,
704 action_id: cut(action_id, MAX_TEXT),
705 message: cut(message, MAX_TEXT),
706 icon_name: cut(icon_name, MAX_TEXT),
707 identities: names,
708 state: AuthState::Waiting,
709 info: Vec::new(),
710 errors: Vec::new(),
711 },
712 })
713}
714
715fn cut(text: &str, max: usize) -> String {
717 text[..text.floor_char_boundary(max)].to_owned()
718}
719
720fn user_name(passwd: &str, uid: u32) -> Option<String> {
725 let listed = passwd.lines().find_map(|line| {
726 let mut fields = line.split(':');
727 let name = fields.next()?;
728 let uid_field = fields.nth(1)?;
729 (uid_field.parse() == Ok(uid)).then(|| name.to_owned())
730 });
731 let own = || {
732 let ours = rustix::process::getuid().as_raw() == uid;
733 ours.then(|| std::env::var("USER").ok()).flatten()
734 };
735 listed.or_else(own).filter(|name| !name.is_empty())
736}
737
738fn subject(session: Option<String>) -> (&'static str, HashMap<&'static str, Variant<'static>>) {
741 match session {
742 Some(id) => (
743 "unix-session",
744 HashMap::from([("session-id", Variant::from(id))]),
745 ),
746 None => (
747 "unix-process",
748 HashMap::from([
749 ("pid", Variant::from(std::process::id())),
750 ("start-time", Variant::from(start_time().unwrap_or(0))),
751 ]),
752 ),
753 }
754}
755
756fn start_time() -> Option<u64> {
759 let stat = std::fs::read_to_string("/proc/self/stat").ok()?;
760 stat
762 .rsplit_once(')')?
763 .1
764 .split_whitespace()
765 .nth(19)?
766 .parse()
767 .ok()
768}
769
770async fn run(service: Rc<Service>, address: zbus::Address) {
774 let connection = match zbus::connection::Builder::address(address).build().await {
775 Ok(connection) => connection,
776 Err(error) => {
777 return tracing::info!(%error, "can't reach the system bus, so no polkit requests arrive");
778 }
779 };
780 let mut messages = MessageStream::from(&connection);
782 if let Err(error) = bus_call(&connection, "AddMatch", &owner_rule(AUTHORITY)).await {
783 return tracing::warn!(%error, "can't follow polkit on the system bus, so no requests arrive");
784 }
785 if let Some(state) = service.state.borrow_mut().as_mut() {
786 state.connection = Some(connection.clone());
787 }
788 register(&service, &connection).await;
789 loop {
790 let next = poll_fn(|cx| Pin::new(&mut messages).poll_next(cx)).await;
791 if !service.alive() {
792 return;
793 }
794 match next {
795 Some(Ok(message)) if message.header().message_type() == Kind::MethodCall => {
796 service.on_call(message);
797 }
798 Some(Ok(message)) => {
799 if let Some((name, owner)) = owner_changed(&message)
801 && name == AUTHORITY
802 {
803 service.drop_requests();
805 service.publish();
806 if !owner.is_empty() {
807 tracing::info!("polkit started again; registering the agent with it");
808 register(&service, &connection).await;
809 }
810 }
811 }
812 Some(Err(error)) => tracing::warn!(%error, "a bad message on the system bus"),
813 None => break,
814 }
815 }
816 tracing::warn!("the system bus closed, so no more polkit requests arrive");
817 service.forget();
818 service.publish();
819}
820
821async fn register(service: &Rc<Service>, connection: &Connection) {
824 let session = std::env::var("XDG_SESSION_ID")
825 .ok()
826 .filter(|id| !id.is_empty());
827 let locale = service.shared.config.desktop_entries.locale.as_str();
829 let body = (subject(session), locale, AGENT_PATH);
830 let call = connection.call_method(
831 Some(AUTHORITY),
832 AUTHORITY_PATH,
833 Some(AUTHORITY_INTERFACE),
834 "RegisterAuthenticationAgent",
835 &body,
836 );
837 let authority = match service.shared.timeout(CALL_TIMEOUT, call).await {
838 Ok(reply) => reply.header().sender().map(|sender| sender.to_string()),
839 Err(error) => return tracing::warn!(%error, "polkit refused the agent, so no requests arrive"),
840 };
841 tracing::info!("registered as the polkit agent");
842 if let Some(state) = service.state.borrow_mut().as_mut() {
843 state.authority = authority;
844 }
845}