feat(apps): permission manager equips apps (manifest+consent+decisions in /system), launches via init w/ file grant; shell unprivileged (/home); install/apps/allow/revoke; revoke stops running app; app-image tool; counter app
This commit is contained in:
83
crates/servers/permission-manager/src/consent.rs
Normal file
83
crates/servers/permission-manager/src/consent.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
//! The `Consent` port on the terminal: a yes/no question, answered by the
|
||||
//! owner typing. (Not yet a trusted path: an app could draw a look-alike
|
||||
//! prompt, though it cannot answer one.)
|
||||
|
||||
use libkos::io::print_to;
|
||||
use manifest::{Capabilities, Capability, Manifest};
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
use crate::policy::Consent;
|
||||
|
||||
pub struct TerminalConsent {
|
||||
pub terminal: CapSlot,
|
||||
}
|
||||
|
||||
impl TerminalConsent {
|
||||
/// Only `y` or `yes` is yes.
|
||||
fn answer(&self) -> bool {
|
||||
let mut line = [0u8; 16];
|
||||
let n = libkos::terminal::read_line(self.terminal, &mut line);
|
||||
matches!(core::str::from_utf8(&line[..n]).map(str::trim), Ok("y" | "Y" | "yes"))
|
||||
}
|
||||
}
|
||||
|
||||
/// `display, storage`.
|
||||
pub struct CapabilityNames(pub Capabilities);
|
||||
|
||||
impl core::fmt::Display for CapabilityNames {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
for (i, c) in self.0.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
f.write_str(c.name())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Consent for TerminalConsent {
|
||||
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool {
|
||||
print_to(
|
||||
self.terminal,
|
||||
format_args!("Install {} ({} {})? It needs: {}. [y/N] ", app.name, app.id, version(app), CapabilityNames(required)),
|
||||
);
|
||||
self.answer()
|
||||
}
|
||||
|
||||
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool {
|
||||
print_to(self.terminal, format_args!("Let {} use {} (it can do without)? [y/N] ", app.name, cap.name()));
|
||||
self.answer()
|
||||
}
|
||||
}
|
||||
|
||||
/// `1.0.0`.
|
||||
pub fn version(app: &Manifest) -> impl core::fmt::Display {
|
||||
struct V(manifest::Version);
|
||||
impl core::fmt::Display for V {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let mut v = [0u8; 18];
|
||||
let mut w = Buf { buf: &mut v, len: 0 };
|
||||
let _ = core::fmt::write(&mut w, format_args!("{}.{}.{}", self.0.major, self.0.minor, self.0.patch));
|
||||
let len = w.len;
|
||||
f.pad(core::str::from_utf8(&v[..len]).unwrap_or(""))
|
||||
}
|
||||
}
|
||||
V(app.version)
|
||||
}
|
||||
|
||||
|
||||
/// Formats into a small buffer, so the whole can then be padded.
|
||||
struct Buf<'a> {
|
||||
buf: &'a mut [u8],
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl core::fmt::Write for Buf<'_> {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
let end = self.len + s.len();
|
||||
self.buf.get_mut(self.len..end).ok_or(core::fmt::Error)?.copy_from_slice(s.as_bytes());
|
||||
self.len = end;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,49 @@
|
||||
//! The permission manager (docs/apps.md): the only one that grants an app
|
||||
//! what its manifest asks for, as far as the owner allows. Its logic
|
||||
//! (`policy`) is pure; `store`, `consent` and `server` are its adapters.
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod protocol;
|
||||
extern crate alloc;
|
||||
|
||||
pub mod consent;
|
||||
pub mod policy;
|
||||
pub mod server;
|
||||
pub mod store;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/mod.rs"]
|
||||
mod tests;
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use libkos::syscall;
|
||||
use types::address::VirtualAddress;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
/// Where init's manifest put them: its own endpoint, then `uses`, then
|
||||
/// storage, then the spawn endpoint.
|
||||
const EP: CapSlot = CapSlot::const_new(0);
|
||||
const TERMINAL: CapSlot = CapSlot::const_new(1);
|
||||
const DISPLAY: CapSlot = CapSlot::const_new(2);
|
||||
const STORAGE: CapSlot = CapSlot::const_new(3);
|
||||
const INIT: CapSlot = CapSlot::const_new(4);
|
||||
|
||||
const STORAGE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0180_0000);
|
||||
const SOURCE_BUF_VA: VirtualAddress = VirtualAddress::from_raw(0x0190_0000);
|
||||
|
||||
pub fn run() -> ! {
|
||||
let mut srv = server::PermissionManager::new();
|
||||
let ep = types::bounded::CapSlot::new(0).unwrap();
|
||||
server_framework::serve(&mut srv, ep)
|
||||
let Ok(fs) = Storage::attach(STORAGE, STORAGE_BUF_VA) else {
|
||||
libkos::println!("permission: no storage, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
let Some(deaths) = syscall::create_notification().ok().filter(|n| syscall::bind_notification(*n).is_ok()) else {
|
||||
libkos::println!("permission: no notification, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
let store = store::DiskStore::new(fs, SOURCE_BUF_VA);
|
||||
let consent = consent::TerminalConsent { terminal: TERMINAL };
|
||||
let mut srv = server::PermissionServer::new(store, consent, INIT, Some(DISPLAY), deaths);
|
||||
libkos::println!("permission: serving");
|
||||
server_framework::serve(&mut srv, EP)
|
||||
}
|
||||
|
||||
136
crates/servers/permission-manager/src/policy.rs
Normal file
136
crates/servers/permission-manager/src/policy.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
//! What the permission manager decides, with nothing but its two ports:
|
||||
//! the owner (`Consent`) and where apps and decisions live (`Store`).
|
||||
//! No kernel, no IPC, no hardware: tested on the host.
|
||||
|
||||
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
|
||||
|
||||
/// Asks the owner. Answers are final: there is no "ask again later".
|
||||
pub trait Consent {
|
||||
/// May `app` have everything it cannot work without? No means it is
|
||||
/// not installed.
|
||||
fn approve_required(&mut self, app: &Manifest, required: Capabilities) -> bool;
|
||||
/// May `app` have `cap`, which it can do without?
|
||||
fn approve_optional(&mut self, app: &Manifest, cap: Capability) -> bool;
|
||||
}
|
||||
|
||||
/// Installed apps, their files and the owner's decisions.
|
||||
pub trait Store {
|
||||
/// An app folder offered for installation.
|
||||
type Source;
|
||||
|
||||
/// The manifest in `source`, as text.
|
||||
fn source_manifest<'a>(&mut self, source: &Self::Source, buf: &'a mut [u8]) -> Option<&'a str>;
|
||||
fn is_installed(&mut self, id: &AppId) -> bool;
|
||||
/// Copies `source`'s binary and manifest into the app's own folder,
|
||||
/// and makes its data folder if it is to have one.
|
||||
fn install(&mut self, source: &Self::Source, app: &Manifest, data_folder: bool) -> bool;
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest>;
|
||||
/// The installed app run as `command`.
|
||||
fn find(&mut self, command: &str) -> Option<AppId>;
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision>;
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Refusal {
|
||||
/// The offered folder holds no manifest it can read.
|
||||
NoManifest,
|
||||
BadManifest(manifest::Error),
|
||||
AlreadyInstalled,
|
||||
NotInstalled,
|
||||
/// This device has no server for these.
|
||||
Unavailable(Capabilities),
|
||||
/// The owner said no to what it requires.
|
||||
Declined,
|
||||
/// It does not hold these, and cannot run without them.
|
||||
Missing(Capabilities),
|
||||
/// It never asked for that capability.
|
||||
NotRequested,
|
||||
/// Reading or writing the store failed.
|
||||
Store,
|
||||
}
|
||||
|
||||
/// Longest manifest read.
|
||||
pub const MAX_MANIFEST: usize = 2048;
|
||||
|
||||
/// Checks the app in `source` and asks the owner: required capabilities
|
||||
/// all together, then each optional one this device has. Installs it and
|
||||
/// records the decision; returns its manifest.
|
||||
pub fn install<S: Store>(
|
||||
store: &mut S,
|
||||
consent: &mut impl Consent,
|
||||
source: &S::Source,
|
||||
available: Capabilities,
|
||||
) -> Result<Manifest, Refusal> {
|
||||
let mut buf = [0u8; MAX_MANIFEST];
|
||||
let text = store.source_manifest(source, &mut buf).ok_or(Refusal::NoManifest)?;
|
||||
let app = Manifest::parse(text).map_err(Refusal::BadManifest)?;
|
||||
if store.is_installed(&app.id) {
|
||||
return Err(Refusal::AlreadyInstalled);
|
||||
}
|
||||
let lacking = app.required.minus(available);
|
||||
if !lacking.is_empty() {
|
||||
return Err(Refusal::Unavailable(lacking));
|
||||
}
|
||||
if !app.required.is_empty() && !consent.approve_required(&app, app.required) {
|
||||
return Err(Refusal::Declined);
|
||||
}
|
||||
let mut granted = app.required;
|
||||
for cap in app.optional.intersect(available).iter() {
|
||||
if consent.approve_optional(&app, cap) {
|
||||
granted = granted.with(cap);
|
||||
}
|
||||
}
|
||||
let decision = Decision { granted };
|
||||
if !store.install(source, &app, granted.contains(Capability::Storage)) || !store.record(&app.id, &decision) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
/// An app about to run and what it gets.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Launch {
|
||||
pub app: Manifest,
|
||||
pub granted: Capabilities,
|
||||
}
|
||||
|
||||
/// What the app run as `command` gets: what it asked for, the owner
|
||||
/// granted and this device has. Refused if that leaves out anything it
|
||||
/// requires (a decision changed since install, or a server is gone).
|
||||
pub fn plan_launch(store: &mut impl Store, command: &str, available: Capabilities) -> Result<Launch, Refusal> {
|
||||
let id = store.find(command).ok_or(Refusal::NotInstalled)?;
|
||||
let app = store.manifest(&id).ok_or(Refusal::NotInstalled)?;
|
||||
let decision = store.decision(&id).ok_or(Refusal::Store)?;
|
||||
let granted = decision.granted.intersect(app.requested()).intersect(available);
|
||||
let missing = app.required.minus(granted);
|
||||
if !missing.is_empty() {
|
||||
return Err(Refusal::Missing(missing));
|
||||
}
|
||||
Ok(Launch { app, granted })
|
||||
}
|
||||
|
||||
/// The owner takes `cap` away from `id`; the new decision.
|
||||
pub fn revoke(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
|
||||
let decision = store.decision(id).ok_or(Refusal::NotInstalled)?;
|
||||
let changed = Decision { granted: decision.granted.without(cap) };
|
||||
if !store.record(id, &changed) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
/// The owner gives `id` a capability it asked for (one withdrawn, or an
|
||||
/// optional one declined at install); the new decision.
|
||||
pub fn allow(store: &mut impl Store, id: &AppId, cap: Capability) -> Result<Decision, Refusal> {
|
||||
let app = store.manifest(id).ok_or(Refusal::NotInstalled)?;
|
||||
if !app.requested().contains(cap) {
|
||||
return Err(Refusal::NotRequested);
|
||||
}
|
||||
let decision = store.decision(id).ok_or(Refusal::Store)?;
|
||||
let changed = Decision { granted: decision.granted.with(cap) };
|
||||
if !store.record(id, &changed) {
|
||||
return Err(Refusal::Store);
|
||||
}
|
||||
Ok(changed)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
use server_framework::{Decode, Encode};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CapabilityKind {
|
||||
Display,
|
||||
Input,
|
||||
AudioPlayback,
|
||||
AudioCapture,
|
||||
Camera,
|
||||
Storage,
|
||||
Network,
|
||||
Gps,
|
||||
Bluetooth,
|
||||
}
|
||||
|
||||
impl CapabilityKind {
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Display => 0,
|
||||
Self::Input => 1,
|
||||
Self::AudioPlayback => 2,
|
||||
Self::AudioCapture => 3,
|
||||
Self::Camera => 4,
|
||||
Self::Storage => 5,
|
||||
Self::Network => 6,
|
||||
Self::Gps => 7,
|
||||
Self::Bluetooth => 8,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_index(v: usize) -> Option<Self> {
|
||||
match v {
|
||||
0 => Some(Self::Display),
|
||||
1 => Some(Self::Input),
|
||||
2 => Some(Self::AudioPlayback),
|
||||
3 => Some(Self::AudioCapture),
|
||||
4 => Some(Self::Camera),
|
||||
5 => Some(Self::Storage),
|
||||
6 => Some(Self::Network),
|
||||
7 => Some(Self::Gps),
|
||||
8 => Some(Self::Bluetooth),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PermissionRequest {
|
||||
RequestCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
RevokeCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
CheckCapability { process: types::id::ProcessId, kind: CapabilityKind },
|
||||
ListGranted { process: types::id::ProcessId },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum PermissionResponse {
|
||||
Granted,
|
||||
Denied,
|
||||
Revoked,
|
||||
HasCapability(bool),
|
||||
GrantedList(u16),
|
||||
}
|
||||
|
||||
|
||||
impl Encode for PermissionRequest {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
match self {
|
||||
PermissionRequest::RequestCapability { process, kind } => {
|
||||
[0, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::RevokeCapability { process, kind } => {
|
||||
[1, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::CheckCapability { process, kind } => {
|
||||
[2, process.raw() as usize, kind.index(), 0]
|
||||
}
|
||||
PermissionRequest::ListGranted { process } => {
|
||||
[3, process.raw() as usize, 0, 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for PermissionRequest {
|
||||
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||
match words[0] {
|
||||
0 => Some(PermissionRequest::RequestCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
1 => Some(PermissionRequest::RevokeCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
2 => Some(PermissionRequest::CheckCapability {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
kind: CapabilityKind::from_index(words[2])?,
|
||||
}),
|
||||
3 => Some(PermissionRequest::ListGranted {
|
||||
process: types::id::ProcessId::from_raw(words[1] as u16),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for PermissionResponse {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
match self {
|
||||
PermissionResponse::Granted => [0, 0, 0, 0],
|
||||
PermissionResponse::Denied => [1, 0, 0, 0],
|
||||
PermissionResponse::Revoked => [2, 0, 0, 0],
|
||||
PermissionResponse::HasCapability(has) => [3, *has as usize, 0, 0],
|
||||
PermissionResponse::GrantedList(bits) => [4, *bits as usize, 0, 0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode for PermissionResponse {
|
||||
fn decode(words: [usize; 4]) -> Option<Self> {
|
||||
match words[0] {
|
||||
0 => Some(PermissionResponse::Granted),
|
||||
1 => Some(PermissionResponse::Denied),
|
||||
2 => Some(PermissionResponse::Revoked),
|
||||
3 => Some(PermissionResponse::HasCapability(words[1] != 0)),
|
||||
4 => Some(PermissionResponse::GrantedList(words[1] as u16)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +1,335 @@
|
||||
use server_framework::Server;
|
||||
use types::id::ProcessId;
|
||||
//! The permission manager over IPC (`types::apps`): the policy, with the
|
||||
//! storage server as its store, the terminal as the owner, and init to
|
||||
//! load what it runs.
|
||||
|
||||
use crate::protocol::{PermissionRequest, PermissionResponse};
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use libkos::syscall::{self, Give};
|
||||
use libkos::window;
|
||||
use manifest::{AppId, Capabilities, Capability};
|
||||
use server_framework::{MALFORMED, Server};
|
||||
use types::address::{ByteOffset, VirtualAddress};
|
||||
use types::apps::{self as wire, PAGE_SIZE, reply};
|
||||
use types::badge::Badge;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
use types::mapping::PagePerms;
|
||||
use types::permission::{Permission, PermissionSet};
|
||||
use types::start_arg::StartArg;
|
||||
|
||||
const MAX_PROCESSES: usize = 64;
|
||||
const NUM_KINDS: usize = 9;
|
||||
use crate::consent::{CapabilityNames, TerminalConsent, version};
|
||||
use crate::policy::{self, Refusal, Store};
|
||||
use crate::store::{self, DiskStore, PATH_BUF};
|
||||
|
||||
pub struct PermissionManager {
|
||||
grants: [[bool; NUM_KINDS]; MAX_PROCESSES],
|
||||
/// What each app gets to run in, out of the permission manager's allowance.
|
||||
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||
/// Apps running at once; a death is a bit each on the notification.
|
||||
const MAX_RUNNING: usize = 8;
|
||||
const MAX_ARGS: usize = 16;
|
||||
/// Where a client's request page is mapped during its call.
|
||||
const PAGE_VA: VirtualAddress = VirtualAddress::from_raw(0x01A0_0000);
|
||||
|
||||
/// What a launcher passed for its next launch.
|
||||
#[derive(Default)]
|
||||
struct Staged {
|
||||
badge: usize,
|
||||
output: Option<CapSlot>,
|
||||
folder: Option<CapSlot>,
|
||||
}
|
||||
|
||||
impl PermissionManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
grants: [[false; NUM_KINDS]; MAX_PROCESSES],
|
||||
}
|
||||
}
|
||||
struct Running {
|
||||
id: AppId,
|
||||
process: CapSlot,
|
||||
granted: Capabilities,
|
||||
/// Its data folder grant, taken back when it dies.
|
||||
data: Option<CapSlot>,
|
||||
}
|
||||
|
||||
fn granted_bits(&self, process: ProcessId) -> u16 {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return 0;
|
||||
}
|
||||
let mut bits = 0u16;
|
||||
for i in 0..NUM_KINDS {
|
||||
if self.grants[idx][i] {
|
||||
bits |= 1 << i;
|
||||
}
|
||||
}
|
||||
bits
|
||||
pub struct PermissionServer {
|
||||
store: DiskStore,
|
||||
consent: TerminalConsent,
|
||||
/// Init's spawn endpoint.
|
||||
init: CapSlot,
|
||||
display: Option<CapSlot>,
|
||||
/// Bound to the serving thread: deaths of running apps.
|
||||
deaths: CapSlot,
|
||||
staged: Staged,
|
||||
running: [Option<Running>; MAX_RUNNING],
|
||||
}
|
||||
|
||||
fn refused(r: Refusal) -> [usize; 4] {
|
||||
let (code, detail) = match r {
|
||||
Refusal::NoManifest => (reply::NO_MANIFEST, 0),
|
||||
Refusal::BadManifest(e) => (reply::BAD_MANIFEST, line_of(e)),
|
||||
Refusal::AlreadyInstalled => (reply::ALREADY_INSTALLED, 0),
|
||||
Refusal::NotInstalled => (reply::NOT_INSTALLED, 0),
|
||||
Refusal::Unavailable(c) => (reply::UNAVAILABLE, c.bits() as usize),
|
||||
Refusal::Declined => (reply::DECLINED, 0),
|
||||
Refusal::Missing(c) => (reply::MISSING, c.bits() as usize),
|
||||
Refusal::NotRequested => (reply::NOT_REQUESTED, 0),
|
||||
Refusal::Store => (reply::FAILED, 0),
|
||||
};
|
||||
[code, detail, 0, 0]
|
||||
}
|
||||
|
||||
fn line_of(e: manifest::Error) -> usize {
|
||||
use manifest::Error::*;
|
||||
match e {
|
||||
Syntax { line } | Duplicate { line } | Unknown { line } | Invalid { line } => line,
|
||||
Missing(_) | Conflict => 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for PermissionManager {
|
||||
type Request = PermissionRequest;
|
||||
type Response = PermissionResponse;
|
||||
const fn code(c: usize) -> [usize; 4] {
|
||||
[c, 0, 0, 0]
|
||||
}
|
||||
|
||||
fn handle(&mut self, request: PermissionRequest) -> PermissionResponse {
|
||||
match request {
|
||||
PermissionRequest::RequestCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
}
|
||||
self.grants[idx][kind.index()] = true;
|
||||
PermissionResponse::Granted
|
||||
/// The client's page, copied out; the mapping goes again at once.
|
||||
fn read_page(page: CapSlot) -> Option<Vec<u8>> {
|
||||
let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok();
|
||||
let out = w.map(|w| {
|
||||
let mut buf = vec![0u8; PAGE_SIZE.min(w.len())];
|
||||
w.copy_to(ByteOffset::ZERO, &mut buf);
|
||||
buf
|
||||
});
|
||||
syscall::drop_cap(page);
|
||||
out
|
||||
}
|
||||
|
||||
/// `text` into the client's page.
|
||||
fn write_page(page: CapSlot, text: &[u8]) -> bool {
|
||||
let w = window::map(page, PAGE_VA, PagePerms::ReadWrite).ok();
|
||||
let ok = w.is_some_and(|w| text.len() <= w.len() && {
|
||||
w.copy_from(ByteOffset::ZERO, text);
|
||||
true
|
||||
});
|
||||
syscall::drop_cap(page);
|
||||
ok
|
||||
}
|
||||
|
||||
/// The NUL-separated parts of a page, up to the first empty one.
|
||||
fn parts(page: &[u8]) -> impl Iterator<Item = &str> {
|
||||
page.split(|b| *b == 0).take_while(|p| !p.is_empty()).map_while(|p| core::str::from_utf8(p).ok())
|
||||
}
|
||||
|
||||
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] }
|
||||
}
|
||||
|
||||
/// 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 }
|
||||
}
|
||||
|
||||
fn stage(&mut self, badge: usize, op: usize, cap: Option<CapSlot>) -> [usize; 4] {
|
||||
if self.staged.badge != badge {
|
||||
self.unstage();
|
||||
self.staged.badge = badge;
|
||||
}
|
||||
let slot = if op == wire::PASS_OUTPUT { &mut self.staged.output } else { &mut self.staged.folder };
|
||||
if let Some(old) = core::mem::replace(slot, cap) {
|
||||
syscall::drop_cap(old);
|
||||
}
|
||||
code(reply::OK)
|
||||
}
|
||||
|
||||
fn unstage(&mut self) {
|
||||
for cap in [self.staged.output.take(), self.staged.folder.take()].into_iter().flatten() {
|
||||
syscall::drop_cap(cap);
|
||||
}
|
||||
}
|
||||
|
||||
fn launch(&mut self, badge: usize, page: CapSlot) -> ([usize; 4], Option<Give>) {
|
||||
let result = self.try_launch(badge, page);
|
||||
self.unstage();
|
||||
match result {
|
||||
Ok((process, granted)) => ([reply::OK, granted.bits() as usize, 0, 0], Some(process.into())),
|
||||
Err(words) => (words, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_launch(&mut self, badge: usize, page: CapSlot) -> Result<(CapSlot, Capabilities), [usize; 4]> {
|
||||
let page = read_page(page).ok_or(code(reply::FAILED))?;
|
||||
let mut args = [""; MAX_ARGS];
|
||||
let mut n = 0;
|
||||
for (slot, p) in args.iter_mut().zip(parts(&page)) {
|
||||
*slot = p;
|
||||
n += 1;
|
||||
}
|
||||
let args = &args[..n];
|
||||
let command = *args.first().ok_or(code(reply::NOT_INSTALLED))?;
|
||||
let available = self.available();
|
||||
let plan = policy::plan_launch(&mut self.store, command, available).map_err(refused)?;
|
||||
let index = self.running.iter().position(Option::is_none).ok_or(code(reply::BUSY))?;
|
||||
|
||||
let mut p = [0u8; PATH_BUF];
|
||||
let binary = store::binary_path(&mut p, &plan.app).ok_or(code(reply::FAILED))?;
|
||||
let file = self.store.storage().grant(binary, false).map_err(|_| code(reply::FAILED))?;
|
||||
let spawned = libkos::spawn::spawn(self.init, file, PROGRAM);
|
||||
let _ = libkos::fs::release(file);
|
||||
let child = spawned.map_err(|e| match e {
|
||||
libkos::spawn::SpawnError::OverAllowance => code(reply::BUSY),
|
||||
_ => code(reply::FAILED),
|
||||
})?;
|
||||
|
||||
let call = PermissionSet::empty().with(Permission::Execute);
|
||||
let data = match plan.granted.contains(Capability::Storage) {
|
||||
true => store::data_path(&mut p, &plan.app.id).and_then(|d| self.store.storage().grant(d, true).ok()),
|
||||
false => None,
|
||||
};
|
||||
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()),
|
||||
storage: staged.and_then(|s| s.folder).and_then(|f| child.give(f, call)),
|
||||
data: data.and_then(|d| child.give(d, call)),
|
||||
// It subscribes to input by sending the display a notification.
|
||||
display: self.display.filter(|_| plan.granted.contains(Capability::Display)).and_then(|d| child.give_sending(d, call)),
|
||||
args: child.set_args(args),
|
||||
console: None,
|
||||
};
|
||||
let watched = self.watch(child.process, index);
|
||||
if !watched || !child.start_with(arg) {
|
||||
let _ = syscall::kill(child.process);
|
||||
if let Some(d) = data {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
PermissionRequest::RevokeCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::Denied;
|
||||
}
|
||||
self.grants[idx][kind.index()] = false;
|
||||
PermissionResponse::Revoked
|
||||
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 });
|
||||
Ok((child.process, plan.granted))
|
||||
}
|
||||
|
||||
/// Has `process`'s death signal bit `index`.
|
||||
fn watch(&self, process: CapSlot, index: usize) -> bool {
|
||||
let signal = PermissionSet::empty().with(Permission::Write);
|
||||
let Ok(copy) = syscall::mint(self.deaths, Badge::new(1 << index), signal) else {
|
||||
return false;
|
||||
};
|
||||
let watched = syscall::watch_death(process, copy).is_ok();
|
||||
syscall::drop_cap(copy);
|
||||
watched
|
||||
}
|
||||
|
||||
fn install(&mut self, source: Option<CapSlot>) -> [usize; 4] {
|
||||
let Some(source) = source else { return code(reply::FAILED) };
|
||||
let available = self.available();
|
||||
let result = policy::install(&mut self.store, &mut self.consent, &source, available);
|
||||
syscall::drop_cap(source);
|
||||
match result {
|
||||
Ok(app) => {
|
||||
let granted = self.store.decision(&app.id).map_or(0, |d| d.granted.bits() as usize);
|
||||
[reply::OK, granted, 0, 0]
|
||||
}
|
||||
PermissionRequest::CheckCapability { process, kind } => {
|
||||
let idx = process.raw() as usize;
|
||||
if idx >= MAX_PROCESSES {
|
||||
return PermissionResponse::HasCapability(false);
|
||||
}
|
||||
PermissionResponse::HasCapability(self.grants[idx][kind.index()])
|
||||
Err(r) => refused(r),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&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, "{:<20} {:<10} {:<8} {}", app.id, app.command.as_str(), version(&app), CapabilityNames(d.granted));
|
||||
}
|
||||
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);
|
||||
let id = page.as_deref().and_then(|p| parts(p).next()).and_then(AppId::new);
|
||||
let (Some(id), Some(cap)) = (id, Capability::from_index(index)) else {
|
||||
return code(reply::NOT_INSTALLED);
|
||||
};
|
||||
let result = match allow {
|
||||
true => policy::allow(&mut self.store, &id, cap),
|
||||
false => policy::revoke(&mut self.store, &id, cap),
|
||||
};
|
||||
if let Err(r) = result {
|
||||
return refused(r);
|
||||
}
|
||||
if !allow {
|
||||
// Its access dies at once: a running app that had it stops.
|
||||
for r in self.running.iter().flatten().filter(|r| r.id == id && r.granted.contains(cap)) {
|
||||
let _ = syscall::kill(r.process);
|
||||
}
|
||||
PermissionRequest::ListGranted { process } => {
|
||||
PermissionResponse::GrantedList(self.granted_bits(process))
|
||||
}
|
||||
code(reply::OK)
|
||||
}
|
||||
|
||||
/// Apps behind `bits` died: their data grants go back.
|
||||
fn reap(&mut self, bits: usize) {
|
||||
for (i, slot) in self.running.iter_mut().enumerate() {
|
||||
if bits & (1 << i) == 0 {
|
||||
continue;
|
||||
}
|
||||
if let Some(r) = slot.take() {
|
||||
if let Some(d) = r.data {
|
||||
let _ = libkos::fs::release(d);
|
||||
}
|
||||
syscall::drop_cap(r.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text built for the client's page.
|
||||
struct Text(Vec<u8>);
|
||||
|
||||
impl core::fmt::Write for Text {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
self.0.extend_from_slice(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests carry their own words; there is nothing to decode up front.
|
||||
pub struct NoRequest;
|
||||
|
||||
impl server_framework::Decode for NoRequest {
|
||||
fn decode(_: [usize; 4]) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl server_framework::Encode for NoRequest {
|
||||
fn encode(&self) -> [usize; 4] {
|
||||
[MALFORMED, 0, 0, 0]
|
||||
}
|
||||
}
|
||||
|
||||
impl Server for PermissionServer {
|
||||
type Request = NoRequest;
|
||||
type Response = NoRequest;
|
||||
|
||||
fn handle(&mut self, _: NoRequest) -> NoRequest {
|
||||
NoRequest
|
||||
}
|
||||
|
||||
fn handle_call(&mut self, badge: usize, words: [usize; 4], cap: Option<CapSlot>) -> ([usize; 4], Option<Give>) {
|
||||
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::REVOKE, Some(page)) => (self.change(page, words[1], false), None),
|
||||
(wire::ALLOW, Some(page)) => (self.change(page, words[1], true), None),
|
||||
(_, cap) => {
|
||||
if let Some(c) = cap {
|
||||
syscall::drop_cap(c);
|
||||
}
|
||||
([MALFORMED, 0, 0, 0], None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn signal(&mut self, bits: usize) {
|
||||
self.reap(bits);
|
||||
}
|
||||
}
|
||||
|
||||
196
crates/servers/permission-manager/src/store.rs
Normal file
196
crates/servers/permission-manager/src/store.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
//! The `Store` port on the storage server: apps in `/apps/<id>/`, their
|
||||
//! data in `/data/<id>/`, the owner's decisions in `/system/permissions/`.
|
||||
|
||||
use libkos::fs::Storage;
|
||||
use manifest::{AppId, Decision, MAX_DECISION_TEXT, Manifest};
|
||||
use types::address::VirtualAddress;
|
||||
use types::bounded::CapSlot;
|
||||
|
||||
use crate::policy::{MAX_MANIFEST, Store};
|
||||
|
||||
const APPS: &str = "apps";
|
||||
const DATA: &str = "data";
|
||||
const DECISIONS: &str = "system/permissions";
|
||||
const MANIFEST: &str = "manifest.toml";
|
||||
/// Apps `find` looks through.
|
||||
pub const MAX_APPS: usize = 32;
|
||||
const LONGEST_PATH: usize = 160;
|
||||
const COPY_CHUNK: usize = 4096;
|
||||
|
||||
/// `parts` joined with `/`, in `buf`.
|
||||
fn path<'a>(buf: &'a mut [u8; LONGEST_PATH], parts: &[&str]) -> Option<&'a [u8]> {
|
||||
let mut len = 0;
|
||||
for (i, p) in parts.iter().enumerate() {
|
||||
let sep = usize::from(i > 0);
|
||||
let end = len + sep + p.len();
|
||||
let room = buf.get_mut(len..end)?;
|
||||
room[..sep].copy_from_slice(&b"/"[..sep]);
|
||||
room[sep..].copy_from_slice(p.as_bytes());
|
||||
len = end;
|
||||
}
|
||||
Some(&buf[..len])
|
||||
}
|
||||
|
||||
/// All of the file at `path` into `buf`, as text.
|
||||
fn read_text<'a>(fs: &mut Storage, path: &[u8], buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
let h = fs.open(path).ok()?;
|
||||
let size = fs.file_size(h).ok().filter(|s| *s <= buf.len());
|
||||
let read = size.and_then(|s| fs.read(h, &mut buf[..s]).ok());
|
||||
let _ = fs.close(h);
|
||||
let n = read.filter(|r| Some(*r) == size)?;
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
|
||||
/// Copies the file at `from` (in `src`) to `to` (in `dst`), replacing
|
||||
/// what an earlier, failed attempt left there.
|
||||
fn copy(src: &mut Storage, from: &[u8], dst: &mut Storage, to: &[u8]) -> bool {
|
||||
let Ok(input) = src.open(from) else { return false };
|
||||
let _ = dst.delete(to);
|
||||
let Ok(output) = dst.create(to) else {
|
||||
let _ = src.close(input);
|
||||
return false;
|
||||
};
|
||||
let mut chunk = [0u8; COPY_CHUNK];
|
||||
let ok = loop {
|
||||
match src.read(input, &mut chunk) {
|
||||
Ok(0) => break true,
|
||||
Ok(n) if dst.write(output, &chunk[..n]) == Ok(n) => {}
|
||||
_ => break false,
|
||||
}
|
||||
};
|
||||
let _ = src.close(input);
|
||||
let _ = dst.close(output);
|
||||
ok
|
||||
}
|
||||
|
||||
pub struct DiskStore {
|
||||
fs: Storage,
|
||||
/// Where an offered app folder's grant is mapped while it is read.
|
||||
source_va: VirtualAddress,
|
||||
}
|
||||
|
||||
impl DiskStore {
|
||||
pub fn new(fs: Storage, source_va: VirtualAddress) -> Self {
|
||||
Self { fs, source_va }
|
||||
}
|
||||
|
||||
/// Storage itself, for grants on the app's behalf.
|
||||
pub fn storage(&mut self) -> &mut Storage {
|
||||
&mut self.fs
|
||||
}
|
||||
|
||||
/// The ids of the installed apps (folder names in `/apps`).
|
||||
pub fn ids(&mut self, mut each: impl FnMut(AppId)) {
|
||||
let _ = self.fs.list_dir(APPS.as_bytes(), |e| {
|
||||
if let Some(id) = core::str::from_utf8(e.name()).ok().and_then(AppId::new) {
|
||||
each(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn with_source<T>(&self, source: CapSlot, f: impl FnOnce(&mut Storage) -> T) -> Option<T> {
|
||||
let mut src = Storage::attach(source, self.source_va).ok()?;
|
||||
let out = f(&mut src);
|
||||
src.detach();
|
||||
Some(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for DiskStore {
|
||||
/// A read-only storage grant to the offered app's folder.
|
||||
type Source = CapSlot;
|
||||
|
||||
fn source_manifest<'a>(&mut self, source: &CapSlot, buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let n = self.with_source(*source, |src| read_text(src, MANIFEST.as_bytes(), &mut text).map(str::len))??;
|
||||
buf.get_mut(..n)?.copy_from_slice(&text[..n]);
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
|
||||
fn is_installed(&mut self, id: &AppId) -> bool {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let Some(p) = path(&mut p, &[APPS, id.as_str(), MANIFEST]) else { return false };
|
||||
self.fs.open(p).and_then(|h| self.fs.close(h)).is_ok()
|
||||
}
|
||||
|
||||
fn install(&mut self, source: &CapSlot, app: &Manifest, data_folder: bool) -> bool {
|
||||
let id = app.id.as_str();
|
||||
let (mut folder, mut to_manifest, mut to_binary) = ([0u8; LONGEST_PATH], [0u8; LONGEST_PATH], [0u8; LONGEST_PATH]);
|
||||
let (Some(folder), Some(to_manifest), Some(to_binary)) = (
|
||||
path(&mut folder, &[APPS, id]),
|
||||
path(&mut to_manifest, &[APPS, id, MANIFEST]),
|
||||
path(&mut to_binary, &[APPS, id, app.binary.as_str()]),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
// There already if an earlier attempt failed part way; an app
|
||||
// counts as installed only once its manifest is in.
|
||||
let _ = self.fs.mkdir(folder);
|
||||
let fs = &mut self.fs;
|
||||
let copied = Storage::attach(*source, self.source_va).ok().map(|mut src| {
|
||||
// The binary first: an app folder with a manifest is installed.
|
||||
let ok = copy(&mut src, app.binary.as_str().as_bytes(), fs, to_binary)
|
||||
&& copy(&mut src, MANIFEST.as_bytes(), fs, to_manifest);
|
||||
src.detach();
|
||||
ok
|
||||
});
|
||||
if copied != Some(true) {
|
||||
return false;
|
||||
}
|
||||
let mut data = [0u8; LONGEST_PATH];
|
||||
// Kept from an earlier install if there is one: the app's data.
|
||||
!data_folder || path(&mut data, &[DATA, id]).is_some_and(|d| self.fs.mkdir(d).is_ok() || self.fs.list_dir(d, |_| {}).is_ok())
|
||||
}
|
||||
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[APPS, id.as_str(), MANIFEST])?;
|
||||
let mut text = [0u8; MAX_MANIFEST];
|
||||
let m = Manifest::parse(read_text(&mut self.fs, p, &mut text)?).ok()?;
|
||||
// A manifest in the wrong folder is no app.
|
||||
(m.id == *id).then_some(m)
|
||||
}
|
||||
|
||||
fn find(&mut self, command: &str) -> Option<AppId> {
|
||||
let mut ids = [None; MAX_APPS];
|
||||
let mut n = 0;
|
||||
self.ids(|id| {
|
||||
if let Some(slot) = ids.get_mut(n) {
|
||||
*slot = Some(id);
|
||||
n += 1;
|
||||
}
|
||||
});
|
||||
ids.into_iter().flatten().find(|id| self.manifest(id).is_some_and(|m| m.command.as_str() == command))
|
||||
}
|
||||
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision> {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let p = path(&mut p, &[DECISIONS, id.as_str()])?;
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
Decision::parse(read_text(&mut self.fs, p, &mut text)?).ok()
|
||||
}
|
||||
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
|
||||
let mut p = [0u8; LONGEST_PATH];
|
||||
let Some(p) = path(&mut p, &[DECISIONS, id.as_str()]) else { return false };
|
||||
let mut text = [0u8; MAX_DECISION_TEXT];
|
||||
let n = decision.write(&mut text);
|
||||
// Replaced whole: there is no truncate.
|
||||
let _ = self.fs.delete(p);
|
||||
let Ok(h) = self.fs.create(p) else { return false };
|
||||
let written = self.fs.write(h, &text[..n]) == Ok(n);
|
||||
let _ = self.fs.close(h);
|
||||
written
|
||||
}
|
||||
}
|
||||
|
||||
/// The path of app `id`'s binary and its data folder, for grants.
|
||||
pub fn binary_path<'a>(buf: &'a mut [u8; LONGEST_PATH], app: &Manifest) -> Option<&'a [u8]> {
|
||||
path(buf, &[APPS, app.id.as_str(), app.binary.as_str()])
|
||||
}
|
||||
|
||||
pub fn data_path<'a>(buf: &'a mut [u8; LONGEST_PATH], id: &AppId) -> Option<&'a [u8]> {
|
||||
path(buf, &[DATA, id.as_str()])
|
||||
}
|
||||
|
||||
pub const PATH_BUF: usize = LONGEST_PATH;
|
||||
@@ -1,2 +1 @@
|
||||
mod protocol;
|
||||
mod server;
|
||||
mod policy;
|
||||
|
||||
204
crates/servers/permission-manager/src/tests/policy.rs
Normal file
204
crates/servers/permission-manager/src/tests/policy.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
extern crate std;
|
||||
|
||||
use crate::policy::*;
|
||||
use manifest::{AppId, Capabilities, Capability, Decision, Manifest};
|
||||
use std::collections::BTreeMap;
|
||||
use std::string::{String, ToString};
|
||||
use std::vec::Vec;
|
||||
|
||||
/// Installed apps in memory; a source is the manifest text itself.
|
||||
#[derive(Default)]
|
||||
struct MemoryStore {
|
||||
manifests: BTreeMap<String, Manifest>,
|
||||
decisions: BTreeMap<String, Decision>,
|
||||
data_folders: Vec<String>,
|
||||
fail_writes: bool,
|
||||
}
|
||||
|
||||
impl Store for MemoryStore {
|
||||
type Source = String;
|
||||
|
||||
fn source_manifest<'a>(&mut self, source: &String, buf: &'a mut [u8]) -> Option<&'a str> {
|
||||
if source.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let n = source.len().min(buf.len());
|
||||
buf[..n].copy_from_slice(&source.as_bytes()[..n]);
|
||||
core::str::from_utf8(&buf[..n]).ok()
|
||||
}
|
||||
fn is_installed(&mut self, id: &AppId) -> bool {
|
||||
self.manifests.contains_key(id.as_str())
|
||||
}
|
||||
fn install(&mut self, _source: &String, app: &Manifest, data_folder: bool) -> bool {
|
||||
if self.fail_writes {
|
||||
return false;
|
||||
}
|
||||
self.manifests.insert(app.id.as_str().to_string(), *app);
|
||||
if data_folder {
|
||||
self.data_folders.push(app.id.as_str().to_string());
|
||||
}
|
||||
true
|
||||
}
|
||||
fn manifest(&mut self, id: &AppId) -> Option<Manifest> {
|
||||
self.manifests.get(id.as_str()).copied()
|
||||
}
|
||||
fn find(&mut self, command: &str) -> Option<AppId> {
|
||||
self.manifests.values().find(|m| m.command.as_str() == command).map(|m| m.id)
|
||||
}
|
||||
fn decision(&mut self, id: &AppId) -> Option<Decision> {
|
||||
self.decisions.get(id.as_str()).copied()
|
||||
}
|
||||
fn record(&mut self, id: &AppId, decision: &Decision) -> bool {
|
||||
if self.fail_writes {
|
||||
return false;
|
||||
}
|
||||
self.decisions.insert(id.as_str().to_string(), *decision);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// An owner who answers from a script, and remembers what was asked.
|
||||
struct Owner {
|
||||
required: bool,
|
||||
optional: Vec<Capability>,
|
||||
asked: Vec<String>,
|
||||
}
|
||||
|
||||
impl Owner {
|
||||
fn says(required: bool, optional: &[Capability]) -> Self {
|
||||
Self { required, optional: optional.to_vec(), asked: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Consent for Owner {
|
||||
fn approve_required(&mut self, _app: &Manifest, required: Capabilities) -> bool {
|
||||
self.asked.push(std::format!("required {:?}", required.iter().collect::<Vec<_>>()));
|
||||
self.required
|
||||
}
|
||||
fn approve_optional(&mut self, _app: &Manifest, cap: Capability) -> bool {
|
||||
self.asked.push(std::format!("optional {}", cap.name()));
|
||||
self.optional.contains(&cap)
|
||||
}
|
||||
}
|
||||
|
||||
fn app(id: &str, command: &str, required: &str, optional: &str) -> String {
|
||||
std::format!(
|
||||
"[app]\nid = \"{id}\"\nname = \"App\"\nversion = \"1.0.0\"\nbinary = \"{command}\"\n[capabilities]\nrequired = [{required}]\noptional = [{optional}]\n"
|
||||
)
|
||||
}
|
||||
|
||||
const DISPLAY: Capability = Capability::Display;
|
||||
const STORAGE: Capability = Capability::Storage;
|
||||
const NETWORK: Capability = Capability::Network;
|
||||
|
||||
fn device() -> Capabilities {
|
||||
Capabilities::NONE.with(DISPLAY).with(STORAGE)
|
||||
}
|
||||
|
||||
fn set(caps: &[Capability]) -> Capabilities {
|
||||
caps.iter().copied().collect()
|
||||
}
|
||||
|
||||
fn id(s: &str) -> AppId {
|
||||
AppId::new(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installing_asks_once_for_the_required_then_each_optional() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[STORAGE]);
|
||||
let src = app("kos.snake", "snake", "\"display\"", "\"storage\"");
|
||||
let m = install(&mut store, &mut owner, &src, device()).unwrap();
|
||||
assert_eq!(owner.asked, ["required [Display]", "optional storage"]);
|
||||
assert_eq!(store.decision(&m.id), Some(Decision { granted: set(&[DISPLAY, STORAGE]) }));
|
||||
assert_eq!(store.data_folders, ["kos.snake"], "storage granted: it gets a data folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_app_needing_nothing_is_installed_without_asking() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(false, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()).unwrap();
|
||||
assert!(owner.asked.is_empty());
|
||||
assert!(store.data_folders.is_empty(), "no storage, no data folder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declining_the_required_installs_nothing() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(false, &[]);
|
||||
let src = app("kos.snake", "snake", "\"display\"", "");
|
||||
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::Declined));
|
||||
assert!(store.manifests.is_empty() && store.decisions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_the_device_lacks_is_refused_or_never_asked() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[NETWORK]);
|
||||
let needs_network = app("kos.chat", "chat", "\"network\"", "");
|
||||
assert_eq!(install(&mut store, &mut owner, &needs_network, device()), Err(Refusal::Unavailable(set(&[NETWORK]))));
|
||||
assert!(owner.asked.is_empty(), "nothing asked for an app that cannot run here");
|
||||
|
||||
let wants_network = app("kos.notes", "notes", "", "\"network\"");
|
||||
install(&mut store, &mut owner, &wants_network, device()).unwrap();
|
||||
assert!(owner.asked.is_empty(), "an optional capability the device lacks is not offered");
|
||||
assert_eq!(store.decision(&id("kos.notes")), Some(Decision::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_or_duplicate_apps_are_refused() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
assert!(matches!(install(&mut store, &mut owner, &"[app]\n".to_string(), device()), Err(Refusal::BadManifest(_))));
|
||||
assert_eq!(install(&mut store, &mut owner, &String::new(), device()), Err(Refusal::NoManifest));
|
||||
let src = app("kos.cat", "cat", "", "");
|
||||
install(&mut store, &mut owner, &src, device()).unwrap();
|
||||
assert_eq!(install(&mut store, &mut owner, &src, device()), Err(Refusal::AlreadyInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_store_is_reported() {
|
||||
let mut store = MemoryStore { fail_writes: true, ..MemoryStore::default() };
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
assert_eq!(install(&mut store, &mut owner, &app("kos.cat", "cat", "", ""), device()), Err(Refusal::Store));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_launch_gets_what_was_asked_granted_and_present() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[STORAGE]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
|
||||
let plan = plan_launch(&mut store, "snake", device()).unwrap();
|
||||
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
|
||||
// A decision listing more than the app asked for grants no more.
|
||||
store.decisions.insert("kos.snake".to_string(), Decision { granted: set(&[DISPLAY, STORAGE, NETWORK]) });
|
||||
let plan = plan_launch(&mut store, "snake", device().with(NETWORK)).unwrap();
|
||||
assert_eq!(plan.granted, set(&[DISPLAY, STORAGE]));
|
||||
assert_eq!(plan_launch(&mut store, "nope", device()), Err(Refusal::NotInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoking_and_allowing_change_the_next_launch() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", "\"storage\""), device()).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY]));
|
||||
|
||||
allow(&mut store, &id("kos.snake"), STORAGE).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()).unwrap().granted, set(&[DISPLAY, STORAGE]));
|
||||
assert_eq!(allow(&mut store, &id("kos.snake"), NETWORK), Err(Refusal::NotRequested), "only what it asked for");
|
||||
|
||||
revoke(&mut store, &id("kos.snake"), DISPLAY).unwrap();
|
||||
assert_eq!(plan_launch(&mut store, "snake", device()), Err(Refusal::Missing(set(&[DISPLAY]))), "a required one gone: it cannot run");
|
||||
assert_eq!(revoke(&mut store, &id("kos.nope"), DISPLAY), Err(Refusal::NotInstalled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_gone_since_install_stops_an_app_that_requires_it() {
|
||||
let mut store = MemoryStore::default();
|
||||
let mut owner = Owner::says(true, &[]);
|
||||
install(&mut store, &mut owner, &app("kos.snake", "snake", "\"display\"", ""), device()).unwrap();
|
||||
let headless = Capabilities::NONE.with(STORAGE);
|
||||
assert_eq!(plan_launch(&mut store, "snake", headless), Err(Refusal::Missing(set(&[DISPLAY]))));
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use crate::protocol::*;
|
||||
use server_framework::{Decode, Encode};
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn pid(v: u16) -> ProcessId { ProcessId::from_raw(v) }
|
||||
|
||||
#[test]
|
||||
fn request_capability_roundtrip() {
|
||||
let req = PermissionRequest::RequestCapability { process: pid(5), kind: CapabilityKind::Camera };
|
||||
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_capability_roundtrip() {
|
||||
let req = PermissionRequest::RevokeCapability { process: pid(3), kind: CapabilityKind::Network };
|
||||
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_capability_roundtrip() {
|
||||
let req = PermissionRequest::CheckCapability { process: pid(1), kind: CapabilityKind::Storage };
|
||||
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_granted_roundtrip() {
|
||||
let req = PermissionRequest::ListGranted { process: pid(7) };
|
||||
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_kinds_roundtrip() {
|
||||
let kinds = [
|
||||
CapabilityKind::Display, CapabilityKind::Input, CapabilityKind::AudioPlayback,
|
||||
CapabilityKind::AudioCapture, CapabilityKind::Camera, CapabilityKind::Storage,
|
||||
CapabilityKind::Network, CapabilityKind::Gps, CapabilityKind::Bluetooth,
|
||||
];
|
||||
for kind in kinds {
|
||||
let req = PermissionRequest::RequestCapability { process: pid(0), kind };
|
||||
assert_eq!(PermissionRequest::decode(req.encode()), Some(req));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_roundtrips() {
|
||||
let responses = [
|
||||
PermissionResponse::Granted,
|
||||
PermissionResponse::Denied,
|
||||
PermissionResponse::Revoked,
|
||||
PermissionResponse::HasCapability(true),
|
||||
PermissionResponse::HasCapability(false),
|
||||
PermissionResponse::GrantedList(0b101),
|
||||
];
|
||||
for resp in responses {
|
||||
assert_eq!(PermissionResponse::decode(resp.encode()), Some(resp));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_request_returns_none() {
|
||||
assert!(PermissionRequest::decode([99, 0, 0, 0]).is_none());
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
use crate::protocol::*;
|
||||
use crate::server::PermissionManager;
|
||||
use server_framework::Server;
|
||||
use types::id::ProcessId;
|
||||
|
||||
fn pid(v: u16) -> ProcessId { ProcessId::from_raw(v) }
|
||||
|
||||
#[test]
|
||||
fn new_manager_has_no_grants() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
let resp = mgr.handle(PermissionRequest::CheckCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Camera,
|
||||
});
|
||||
assert_eq!(resp, PermissionResponse::HasCapability(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_then_check() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(1),
|
||||
kind: CapabilityKind::Network,
|
||||
});
|
||||
let resp = mgr.handle(PermissionRequest::CheckCapability {
|
||||
process: pid(1),
|
||||
kind: CapabilityKind::Network,
|
||||
});
|
||||
assert_eq!(resp, PermissionResponse::HasCapability(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grant_does_not_affect_other_process() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(1),
|
||||
kind: CapabilityKind::Camera,
|
||||
});
|
||||
let resp = mgr.handle(PermissionRequest::CheckCapability {
|
||||
process: pid(2),
|
||||
kind: CapabilityKind::Camera,
|
||||
});
|
||||
assert_eq!(resp, PermissionResponse::HasCapability(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_removes_grant() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Storage,
|
||||
});
|
||||
mgr.handle(PermissionRequest::RevokeCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Storage,
|
||||
});
|
||||
let resp = mgr.handle(PermissionRequest::CheckCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Storage,
|
||||
});
|
||||
assert_eq!(resp, PermissionResponse::HasCapability(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_granted_returns_bitfield() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Display,
|
||||
});
|
||||
mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(0),
|
||||
kind: CapabilityKind::Network,
|
||||
});
|
||||
let resp = mgr.handle(PermissionRequest::ListGranted { process: pid(0) });
|
||||
assert_eq!(resp, PermissionResponse::GrantedList(0b1000001));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_process_denied() {
|
||||
let mut mgr = PermissionManager::new();
|
||||
let resp = mgr.handle(PermissionRequest::RequestCapability {
|
||||
process: pid(999),
|
||||
kind: CapabilityKind::Camera,
|
||||
});
|
||||
assert_eq!(resp, PermissionResponse::Denied);
|
||||
}
|
||||
Reference in New Issue
Block a user