137 lines
5.3 KiB
Rust
137 lines
5.3 KiB
Rust
//! 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)
|
|
}
|