feat(settings): settings app: apps list + per-capability allow/revoke, about (who/role), sign out; system-only caps permissions/session (install refused); pm MANAGE_BADGE copy (list/apps/allow/revoke only), APPS op; init WHO + session pass-on; factory decisions seeded for new system apps; k-ui list_item/toggle, screens/back/scroll
This commit is contained in:
@@ -29,6 +29,8 @@ const DISPLAY: CapSlot = CapSlot::const_new(2);
|
||||
const USER: CapSlot = CapSlot::const_new(3);
|
||||
const SYSTEM: CapSlot = CapSlot::const_new(4);
|
||||
const INIT: CapSlot = CapSlot::const_new(5);
|
||||
/// Init's session, to pass on to apps granted `session`.
|
||||
const SESSION: CapSlot = CapSlot::const_new(6);
|
||||
|
||||
const USER_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
|
||||
@@ -45,7 +47,8 @@ pub fn run() -> ! {
|
||||
};
|
||||
let store = store::DiskStore::new(user, system, SOURCE_BUF_VA);
|
||||
let consent = consent::TerminalConsent { terminal: TERMINAL };
|
||||
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
|
||||
let ports = server::Ports { init: INIT, display: Some(DISPLAY), session: Some(SESSION), endpoint: EP };
|
||||
let mut srv = server::PermissionServer::new(store, consent, ports, deaths);
|
||||
libkos::println!("permission: serving");
|
||||
server_framework::serve(&mut srv, EP)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ pub enum Refusal {
|
||||
Missing(Capabilities),
|
||||
/// It never asked for that capability.
|
||||
NotRequested,
|
||||
/// It asks for these, which only system apps may have.
|
||||
SystemOnly(Capabilities),
|
||||
/// Reading or writing the store failed.
|
||||
Store,
|
||||
}
|
||||
@@ -68,6 +70,10 @@ pub fn install<S: Store>(
|
||||
if store.is_installed(&app.id) {
|
||||
return Err(Refusal::AlreadyInstalled);
|
||||
}
|
||||
let system = app.requested().intersect(Capabilities::SYSTEM_ONLY);
|
||||
if !system.is_empty() {
|
||||
return Err(Refusal::SystemOnly(system));
|
||||
}
|
||||
let lacking = app.required.minus(available);
|
||||
if !lacking.is_empty() {
|
||||
return Err(Refusal::Unavailable(lacking));
|
||||
|
||||
@@ -46,6 +46,9 @@ struct Running {
|
||||
data: Option<CapSlot>,
|
||||
/// Its window, closed when it dies.
|
||||
window: Option<CapSlot>,
|
||||
/// The management copy of this server's endpoint (`permissions`),
|
||||
/// revoked when it dies.
|
||||
manage: Option<CapSlot>,
|
||||
}
|
||||
|
||||
pub struct PermissionServer {
|
||||
@@ -54,6 +57,11 @@ pub struct PermissionServer {
|
||||
/// Init's spawn endpoint.
|
||||
init: CapSlot,
|
||||
display: Option<CapSlot>,
|
||||
/// Init's session (who is signed in, signing out), for apps granted
|
||||
/// `session`.
|
||||
session: Option<CapSlot>,
|
||||
/// This server's own endpoint, which management copies are minted from.
|
||||
endpoint: CapSlot,
|
||||
/// Bound to the serving thread: deaths of running apps.
|
||||
deaths: CapSlot,
|
||||
staged: Staged,
|
||||
@@ -70,6 +78,7 @@ fn refused(r: Refusal) -> [usize; 4] {
|
||||
Refusal::Declined => (reply::DECLINED, 0),
|
||||
Refusal::Missing(c) => (reply::MISSING, c.bits() as usize),
|
||||
Refusal::NotRequested => (reply::NOT_REQUESTED, 0),
|
||||
Refusal::SystemOnly(c) => (reply::SYSTEM_ONLY, c.bits() as usize),
|
||||
Refusal::Store => (reply::FAILED, 0),
|
||||
};
|
||||
[code, detail, 0, 0]
|
||||
@@ -116,14 +125,21 @@ fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
|
||||
}
|
||||
|
||||
impl PermissionServer {
|
||||
pub fn new(store: DiskStore, consent: TerminalConsent, init: CapSlot, display: Option<CapSlot>, deaths: CapSlot) -> Self {
|
||||
Self { store, consent, init, display, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] }
|
||||
pub fn new(store: DiskStore, consent: TerminalConsent, ports: Ports, deaths: CapSlot) -> Self {
|
||||
let Ports { init, display, session, endpoint } = ports;
|
||||
Self { store, consent, init, display, session, endpoint, deaths, staged: Staged::default(), running: [const { None }; MAX_RUNNING] }
|
||||
}
|
||||
|
||||
/// What this device has a server for.
|
||||
fn available(&self) -> Capabilities {
|
||||
let base = Capabilities::NONE.with(Capability::Storage);
|
||||
if self.display.is_some() { base.with(Capability::Display) } else { base }
|
||||
let mut c = Capabilities::NONE.with(Capability::Storage).with(Capability::Permissions);
|
||||
if self.display.is_some() {
|
||||
c = c.with(Capability::Display);
|
||||
}
|
||||
if self.session.is_some() {
|
||||
c = c.with(Capability::Session);
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn stage(&mut self, badge: usize, op: usize, cap: Option<CapSlot>) -> [usize; 4] {
|
||||
@@ -185,6 +201,16 @@ impl PermissionServer {
|
||||
true => self.display.and_then(|d| libkos::display::open(d, plan.app.name.as_str().as_bytes())).map(|o| o.window),
|
||||
false => None,
|
||||
};
|
||||
// Managing others through a copy of this endpoint that can do no
|
||||
// more (see `MANAGE_BADGE`).
|
||||
let manage = match plan.granted.contains(Capability::Permissions) {
|
||||
true => {
|
||||
let perms = PermissionSet::empty().with(Permission::Execute).with(Permission::Delegate);
|
||||
syscall::mint(self.endpoint, types::badge::Badge::new(wire::MANAGE_BADGE | index), perms).ok()
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
let session = self.session.filter(|_| plan.granted.contains(Capability::Session));
|
||||
let staged = (badge == self.staged.badge).then_some(&self.staged);
|
||||
let arg = StartArg {
|
||||
log: staged.and_then(|s| s.output).and_then(|o| child.give(o, call)).or(child.log()),
|
||||
@@ -195,6 +221,9 @@ impl PermissionServer {
|
||||
display: window.and_then(|w| child.give_sending(w, call)),
|
||||
args: child.set_args(args),
|
||||
console: None,
|
||||
// Delegate, to lend its request page with each call.
|
||||
permissions: manage.and_then(|m| child.give_sending(m, call)),
|
||||
session: session.and_then(|s| child.give(s, call)),
|
||||
};
|
||||
let watched = self.watch(child.process, index);
|
||||
if !watched || !child.start_with(arg) {
|
||||
@@ -203,10 +232,11 @@ impl PermissionServer {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
close_window(window);
|
||||
revoke(manage);
|
||||
syscall::drop_cap(child.process);
|
||||
return Err(code(reply::FAILED));
|
||||
}
|
||||
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window });
|
||||
self.running[index] = Some(Running { id: plan.app.id, process: child.process, granted: plan.granted, data, window, manage });
|
||||
Ok((child.process, plan.granted))
|
||||
}
|
||||
|
||||
@@ -248,6 +278,20 @@ impl PermissionServer {
|
||||
if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) }
|
||||
}
|
||||
|
||||
/// Every app, for a program to read (`types::apps::APPS`).
|
||||
fn apps(&mut self, page: CapSlot) -> [usize; 4] {
|
||||
use core::fmt::Write;
|
||||
let mut ids = Vec::new();
|
||||
self.store.ids(|id| ids.push(id));
|
||||
let mut text = Text(Vec::new());
|
||||
for id in ids {
|
||||
let (Some(app), Some(d)) = (self.store.manifest(&id), self.store.decision(&id)) else { continue };
|
||||
let _ = writeln!(text, "{}\t{}\t{}\t{}\t{}", app.id, app.name.as_str(), app.required.bits(), app.optional.bits(), d.granted.bits());
|
||||
}
|
||||
let len = text.0.len().min(PAGE_SIZE);
|
||||
if write_page(page, &text.0[..len]) { [reply::OK, len, 0, 0] } else { code(reply::FAILED) }
|
||||
}
|
||||
|
||||
/// The owner changes app `id`'s (in the page) capability `index`.
|
||||
fn change(&mut self, page: CapSlot, index: usize, allow: bool) -> [usize; 4] {
|
||||
let page = read_page(page);
|
||||
@@ -282,6 +326,7 @@ impl PermissionServer {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
close_window(r.window);
|
||||
revoke(r.manage);
|
||||
syscall::drop_cap(r.process);
|
||||
}
|
||||
}
|
||||
@@ -322,11 +367,18 @@ impl Server for PermissionServer {
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
|
||||
if !allowed(badge, words[0]) {
|
||||
if let Some(c) = cap {
|
||||
syscall::drop_cap(c);
|
||||
}
|
||||
return ([MALFORMED, 0, 0, 0], None);
|
||||
}
|
||||
match (words[0], cap) {
|
||||
(wire::PASS_OUTPUT | wire::PASS_FOLDER, cap) => (self.stage(badge, words[0], cap), None),
|
||||
(wire::LAUNCH, Some(page)) => self.launch(badge, page),
|
||||
(wire::INSTALL, source) => (self.install(source), None),
|
||||
(wire::LIST, Some(page)) => (self.list(page), None),
|
||||
(wire::APPS, Some(page)) => (self.apps(page), None),
|
||||
(wire::REVOKE, Some(page)) => (self.change(page, words[1], false), None),
|
||||
(wire::ALLOW, Some(page)) => (self.change(page, words[1], true), None),
|
||||
(_, cap) => {
|
||||
@@ -349,3 +401,26 @@ fn close_window(window: Option<CapSlot>) {
|
||||
syscall::drop_cap(w);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `op` may come through the copy of this endpoint badged `badge`:
|
||||
/// a management copy (`permissions`) decides, never installs or launches.
|
||||
pub fn allowed(badge: usize, op: usize) -> bool {
|
||||
badge & wire::MANAGE_BADGE == 0 || matches!(op, wire::LIST | wire::APPS | wire::REVOKE | wire::ALLOW)
|
||||
}
|
||||
|
||||
fn revoke(cap: Option<CapSlot>) {
|
||||
if let Some(c) = cap {
|
||||
let _ = syscall::revoke(c);
|
||||
syscall::drop_cap(c);
|
||||
}
|
||||
}
|
||||
|
||||
/// What the permission manager was given to reach others.
|
||||
pub struct Ports {
|
||||
/// Init's spawn endpoint.
|
||||
pub init: CapSlot,
|
||||
pub display: Option<CapSlot>,
|
||||
pub session: Option<CapSlot>,
|
||||
/// Its own endpoint.
|
||||
pub endpoint: CapSlot,
|
||||
}
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
mod policy;
|
||||
|
||||
#[test]
|
||||
fn a_management_copy_only_decides() {
|
||||
use crate::server::allowed;
|
||||
use types::apps::*;
|
||||
let manager = MANAGE_BADGE | 3;
|
||||
for op in [LIST, APPS, REVOKE, ALLOW] {
|
||||
assert!(allowed(manager, op));
|
||||
}
|
||||
for op in [LAUNCH, INSTALL, PASS_OUTPUT, PASS_FOLDER, 99] {
|
||||
assert!(!allowed(manager, op), "op {op}");
|
||||
}
|
||||
assert!(allowed(5, LAUNCH) && allowed(5, INSTALL), "a launcher's copy does everything");
|
||||
}
|
||||
|
||||
@@ -202,3 +202,16 @@ fn a_server_gone_since_install_stops_an_app_that_requires_it() {
|
||||
let headless = Capabilities::NONE.with(STORAGE);
|
||||
assert_eq!(plan_launch(&mut store, "snake", headless), Err(Refusal::Missing(set(&[DISPLAY]))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_only_capabilities_are_refused_to_installed_apps() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
for (required, optional) in [("\"permissions\"", ""), ("", "\"session\"")] {
|
||||
let src = app("kos.sneaky", "sneaky", required, optional);
|
||||
let refused = install(&mut store, &mut owner, &src, device());
|
||||
assert!(matches!(refused, Err(Refusal::SystemOnly(c)) if !c.is_empty()), "{refused:?}");
|
||||
}
|
||||
assert!(owner.asked.is_empty(), "the user is never asked to hand them over");
|
||||
assert!(!store.is_installed(&id("kos.sneaky")));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user