add validation to s9pks

This commit is contained in:
Aiden McClelland
2022-02-07 15:27:07 -07:00
committed by Aiden McClelland
parent 2c17038b19
commit 08770567d2
11 changed files with 321 additions and 15 deletions

View File

@@ -3,9 +3,9 @@
set -e
shopt -s expand_aliases
if [ "$0" != "./build-sdk.sh" ]; then
if [ "$0" != "./install-sdk.sh" ]; then
>&2 echo "Must be run from backend directory"
exit 1
fi
cargo install --bin=embassy-sdk --path=. --no-default-features --verbose
cargo install --bin=embassy-sdk --path=. --no-default-features

View File

@@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::net::Ipv4Addr;
use std::path::PathBuf;
@@ -23,6 +23,17 @@ use crate::{Error, ResultExt, HOST_IP};
pub const NET_TLD: &str = "embassy";
lazy_static::lazy_static! {
pub static ref SYSTEM_IMAGES: BTreeSet<ImageId> = {
let mut set = BTreeSet::new();
set.insert("compat".parse().unwrap());
set.insert("utils".parse().unwrap());
set
};
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DockerAction {
@@ -44,6 +55,32 @@ pub struct DockerAction {
pub sigterm_timeout: Option<SerdeDuration>,
}
impl DockerAction {
pub fn validate(
&self,
volumes: &Volumes,
image_ids: &BTreeSet<ImageId>,
expected_io: bool,
) -> Result<(), color_eyre::eyre::Report> {
for (volume, _) in &self.mounts {
if !volumes.contains_key(volume) {
color_eyre::eyre::bail!("unknown volume: {}", volume);
}
}
if self.system {
if !SYSTEM_IMAGES.contains(&self.image) {
color_eyre::eyre::bail!("unknown system image: {}", self.image);
}
} else {
if !image_ids.contains(&self.image) {
color_eyre::eyre::bail!("image for {} not contained in package", self.image);
}
}
if expected_io && self.io_format.is_none() {
color_eyre::eyre::bail!("expected io-format");
}
Ok(())
}
#[instrument(skip(ctx, input))]
pub async fn execute<I: Serialize, O: for<'de> Deserialize<'de>>(
&self,

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
@@ -14,7 +14,7 @@ use tracing::instrument;
use self::docker::DockerAction;
use crate::config::{Config, ConfigSpec};
use crate::context::RpcContext;
use crate::id::{Id, InvalidId};
use crate::id::{Id, ImageId, InvalidId};
use crate::s9pk::manifest::PackageId;
use crate::util::serde::{display_serializable, parse_stdin_deserializable, IoFormat};
use crate::util::Version;
@@ -109,6 +109,18 @@ pub struct Action {
pub input_spec: ConfigSpec,
}
impl Action {
#[instrument]
pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet<ImageId>) -> Result<(), Error> {
self.implementation
.validate(volumes, image_ids, true)
.with_ctx(|_| {
(
crate::ErrorKind::ValidateS9pk,
format!("Action {}", self.name),
)
})
}
#[instrument(skip(ctx))]
pub async fn execute(
&self,
@@ -147,6 +159,20 @@ pub enum ActionImplementation {
Docker(DockerAction),
}
impl ActionImplementation {
#[instrument]
pub fn validate(
&self,
volumes: &Volumes,
image_ids: &BTreeSet<ImageId>,
expected_io: bool,
) -> Result<(), color_eyre::eyre::Report> {
match self {
ActionImplementation::Docker(action) => {
action.validate(volumes, image_ids, expected_io)
}
}
}
#[instrument(skip(ctx, input))]
pub async fn execute<I: Serialize, O: for<'de> Deserialize<'de>>(
&self,

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use chrono::{DateTime, Utc};
@@ -15,6 +15,7 @@ use self::target::PackageBackupInfo;
use crate::action::{ActionImplementation, NoOutput};
use crate::context::RpcContext;
use crate::dependencies::reconfigure_dependents_with_live_pointers;
use crate::id::ImageId;
use crate::install::PKG_ARCHIVE_DIR;
use crate::net::interface::{InterfaceId, Interfaces};
use crate::s9pk::manifest::PackageId;
@@ -67,6 +68,16 @@ pub struct BackupActions {
pub restore: ActionImplementation,
}
impl BackupActions {
pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet<ImageId>) -> Result<(), Error> {
self.create
.validate(volumes, image_ids, false)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Create"))?;
self.restore
.validate(volumes, image_ids, false)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Backup Restore"))?;
Ok(())
}
#[instrument(skip(ctx))]
pub async fn create(
&self,

View File

@@ -10,11 +10,12 @@ use super::{Config, ConfigSpec};
use crate::action::ActionImplementation;
use crate::context::RpcContext;
use crate::dependencies::Dependencies;
use crate::id::ImageId;
use crate::s9pk::manifest::PackageId;
use crate::status::health_check::HealthCheckId;
use crate::util::Version;
use crate::volume::Volumes;
use crate::Error;
use crate::{Error, ResultExt};
#[derive(Debug, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
@@ -29,6 +30,16 @@ pub struct ConfigActions {
pub set: ActionImplementation,
}
impl ConfigActions {
#[instrument]
pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet<ImageId>) -> Result<(), Error> {
self.get
.validate(volumes, image_ids, true)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Get"))?;
self.set
.validate(volumes, image_ids, true)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Config Set"))?;
Ok(())
}
#[instrument(skip(ctx))]
pub async fn get(
&self,

View File

@@ -1,5 +1,6 @@
use std::borrow::{Borrow, Cow};
use std::fmt::Debug;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -158,6 +159,12 @@ impl<S: AsRef<str>> ImageId<S> {
)
}
}
impl FromStr for ImageId {
type Err = InvalidId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(ImageId(Id::try_from(s.to_owned())?))
}
}
impl<'de, S> Deserialize<'de> for ImageId<S>
where
S: AsRef<str>,

View File

@@ -1,3 +1,5 @@
use std::collections::BTreeSet;
use color_eyre::eyre::eyre;
use emver::VersionRange;
use futures::{Future, FutureExt};
@@ -8,10 +10,11 @@ use tracing::instrument;
use crate::action::ActionImplementation;
use crate::context::RpcContext;
use crate::id::ImageId;
use crate::s9pk::manifest::PackageId;
use crate::util::Version;
use crate::volume::Volumes;
use crate::Error;
use crate::{Error, ResultExt};
#[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel)]
#[serde(rename_all = "kebab-case")]
@@ -20,6 +23,27 @@ pub struct Migrations {
pub to: IndexMap<VersionRange, ActionImplementation>,
}
impl Migrations {
#[instrument]
pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet<ImageId>) -> Result<(), Error> {
for (version, migration) in &self.from {
migration.validate(volumes, image_ids, true).with_ctx(|_| {
(
crate::ErrorKind::ValidateS9pk,
format!("Migration from {}", version),
)
})?;
}
for (version, migration) in &self.to {
migration.validate(volumes, image_ids, true).with_ctx(|_| {
(
crate::ErrorKind::ValidateS9pk,
format!("Migration to {}", version),
)
})?;
}
Ok(())
}
#[instrument(skip(ctx))]
pub fn from<'a>(
&'a self,

View File

@@ -14,12 +14,24 @@ use crate::db::model::{InterfaceAddressMap, InterfaceAddresses};
use crate::id::Id;
use crate::s9pk::manifest::PackageId;
use crate::util::serde::Port;
use crate::Error;
use crate::{Error, ResultExt};
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Interfaces(pub BTreeMap<InterfaceId, Interface>); // TODO
impl Interfaces {
#[instrument]
pub fn validate(&self) -> Result<(), Error> {
for (_, interface) in &self.0 {
interface.validate().with_ctx(|_| {
(
crate::ErrorKind::ValidateS9pk,
format!("Interface {}", interface.name),
)
})?;
}
Ok(())
}
#[instrument(skip(secrets))]
pub async fn install<Ex>(
&self,
@@ -152,6 +164,21 @@ pub struct Interface {
pub ui: bool,
pub protocols: IndexSet<String>,
}
impl Interface {
#[instrument]
pub fn validate(&self) -> Result<(), color_eyre::eyre::Report> {
if self.tor_config.is_some() && !self.protocols.contains("tcp") {
color_eyre::eyre::bail!("must support tcp to set up a tor hidden service");
}
if self.lan_config.is_some() && !self.protocols.contains("http") {
color_eyre::eyre::bail!("must support http to set up a lan service");
}
if self.ui && !(self.protocols.contains("http") || self.protocols.contains("https")) {
color_eyre::eyre::bail!("must support http or https to serve a ui");
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]

View File

@@ -1,19 +1,25 @@
use std::collections::BTreeSet;
use std::io::SeekFrom;
use std::path::Path;
use std::pin::Pin;
use std::str::FromStr;
use std::task::{Context, Poll};
use color_eyre::eyre::eyre;
use digest::Output;
use ed25519_dalek::PublicKey;
use futures::TryStreamExt;
use sha2::{Digest, Sha512};
use tokio::fs::File;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, ReadBuf, Take};
use tracing::instrument;
use super::header::{FileSection, Header, TableOfContents};
use super::manifest::Manifest;
use super::manifest::{Manifest, PackageId};
use super::SIG_CONTEXT;
use crate::id::ImageId;
use crate::install::progress::InstallProgressTracker;
use crate::util::Version;
use crate::{Error, ResultExt};
#[pin_project::pin_project]
@@ -45,6 +51,66 @@ impl<'a, R: AsyncRead + AsyncSeek + Unpin> AsyncRead for ReadHandle<'a, R> {
}
}
#[derive(Debug)]
pub struct ImageTag {
pub package_id: PackageId,
pub image_id: ImageId,
pub version: Version,
}
impl ImageTag {
#[instrument]
pub fn validate(&self, id: &PackageId, version: &Version) -> Result<(), Error> {
if id != &self.package_id {
return Err(Error::new(
eyre!(
"Contains image for incorrect package: id {}",
self.package_id,
),
crate::ErrorKind::ValidateS9pk,
));
}
if id != &self.package_id {
return Err(Error::new(
eyre!(
"Contains image with incorrect version: expected {} received {}",
version,
self.version,
),
crate::ErrorKind::ValidateS9pk,
));
}
Ok(())
}
}
impl FromStr for ImageTag {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rest = s.strip_prefix("start9/").ok_or_else(|| {
Error::new(
eyre!("Invalid image tag prefix: expected start9/"),
crate::ErrorKind::ValidateS9pk,
)
})?;
let (package, rest) = rest.split_once("/").ok_or_else(|| {
Error::new(
eyre!("Image tag missing image id"),
crate::ErrorKind::ValidateS9pk,
)
})?;
let (image, version) = rest.split_once(":").ok_or_else(|| {
Error::new(
eyre!("Image tag missing version"),
crate::ErrorKind::ValidateS9pk,
)
})?;
Ok(ImageTag {
package_id: package.parse()?,
image_id: image.parse()?,
version: version.parse()?,
})
}
}
pub struct S9pkReader<R: AsyncRead + AsyncSeek + Unpin = File> {
hash: Option<Output<Sha512>>,
hash_string: Option<String>,
@@ -71,8 +137,66 @@ impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<InstallProgressTracker<R>> {
impl<R: AsyncRead + AsyncSeek + Unpin> S9pkReader<R> {
#[instrument(skip(self))]
pub async fn validate(&mut self) -> Result<(), Error> {
let image_tags = self.image_tags().await?;
let man = self.manifest().await?;
let validated_image_ids = image_tags
.into_iter()
.map(|i| i.validate(&man.id, &man.version).map(|_| i.image_id))
.collect::<Result<BTreeSet<ImageId>, _>>()?;
man.actions
.0
.iter()
.map(|(_, action)| action.validate(&man.volumes, &validated_image_ids))
.collect::<Result<(), Error>>()?;
man.backup.validate(&man.volumes, &validated_image_ids)?;
if let Some(cfg) = &man.config {
cfg.validate(&man.volumes, &validated_image_ids)?;
}
man.health_checks
.validate(&man.volumes, &validated_image_ids)?;
man.interfaces.validate()?;
man.main
.validate(&man.volumes, &validated_image_ids, false)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Main"))?;
man.migrations
.validate(&man.volumes, &validated_image_ids)?;
if let Some(props) = &man.properties {
props
.validate(&man.volumes, &validated_image_ids, true)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, "Properties"))?;
}
man.volumes.validate(&man.interfaces)?;
Ok(())
}
#[instrument(skip(self))]
pub async fn image_tags(&mut self) -> Result<Vec<ImageTag>, Error> {
let mut tar = tokio_tar::Archive::new(self.docker_images().await?);
let mut entries = tar.entries()?;
while let Some(mut entry) = entries.try_next().await? {
if &*entry.path()? != Path::new("manifest.json") {
continue;
}
let mut buf = Vec::with_capacity(entry.header().size()? as usize);
entry.read_to_end(&mut buf).await?;
#[derive(serde::Deserialize)]
struct ManEntry {
#[serde(rename = "RepoTags")]
tags: Vec<String>,
}
let man_entries = serde_json::from_slice::<Vec<ManEntry>>(&buf)
.with_ctx(|_| (crate::ErrorKind::Deserialization, "manifest.json"))?;
return man_entries
.iter()
.flat_map(|e| &e.tags)
.map(|t| t.parse())
.collect();
}
Err(Error::new(
eyre!("image.tar missing manifest.json"),
crate::ErrorKind::ParseS9pk,
))
}
#[instrument(skip(rdr))]
pub async fn from_reader(mut rdr: R, check_sig: bool) -> Result<Self, Error> {
let header = Header::deserialize(&mut rdr).await?;

View File

@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use chrono::{DateTime, Utc};
@@ -7,12 +7,12 @@ use tracing::instrument;
use crate::action::{ActionImplementation, NoOutput};
use crate::context::RpcContext;
use crate::id::Id;
use crate::id::{Id, ImageId};
use crate::s9pk::manifest::PackageId;
use crate::util::serde::Duration;
use crate::util::Version;
use crate::volume::Volumes;
use crate::Error;
use crate::{Error, ResultExt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct HealthCheckId<S: AsRef<str> = String>(Id<S>);
@@ -47,6 +47,21 @@ impl<S: AsRef<str>> AsRef<Path> for HealthCheckId<S> {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct HealthChecks(pub BTreeMap<HealthCheckId, HealthCheck>);
impl HealthChecks {
#[instrument]
pub fn validate(&self, volumes: &Volumes, image_ids: &BTreeSet<ImageId>) -> Result<(), Error> {
for (_, check) in &self.0 {
check
.implementation
.validate(&volumes, image_ids, false)
.with_ctx(|_| {
(
crate::ErrorKind::ValidateS9pk,
format!("Health Check {}", check.name),
)
})?;
}
Ok(())
}
pub async fn check_all(
&self,
ctx: &RpcContext,

View File

@@ -3,15 +3,17 @@ use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use color_eyre::eyre::eyre;
use patch_db::{HasModel, Map, MapModel};
use serde::{Deserialize, Deserializer, Serialize};
use tracing::instrument;
use crate::context::RpcContext;
use crate::id::{Id, IdUnchecked};
use crate::net::interface::InterfaceId;
use crate::net::interface::{InterfaceId, Interfaces};
use crate::s9pk::manifest::PackageId;
use crate::util::Version;
use crate::Error;
use crate::{Error, ResultExt};
pub const PKG_VOLUME_DIR: &'static str = "package-data/volumes";
pub const BACKUP_DIR: &'static str = "/media/embassy-os/backups";
@@ -75,6 +77,16 @@ impl<S: AsRef<str>> Serialize for VolumeId<S> {
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Volumes(BTreeMap<VolumeId, Volume>);
impl Volumes {
#[instrument]
pub fn validate(&self, interfaces: &Interfaces) -> Result<(), Error> {
for (id, volume) in &self.0 {
volume
.validate(interfaces)
.with_ctx(|_| (crate::ErrorKind::ValidateS9pk, format!("Volume {}", id)))?;
}
Ok(())
}
#[instrument(skip(ctx))]
pub async fn install(
&self,
ctx: &RpcContext,
@@ -180,6 +192,18 @@ pub enum Volume {
Backup { readonly: bool },
}
impl Volume {
#[instrument]
pub fn validate(&self, interfaces: &Interfaces) -> Result<(), color_eyre::eyre::Report> {
match self {
Volume::Certificate { interface_id } => {
if !interfaces.0.contains_key(interface_id) {
color_eyre::eyre::bail!("unknown interface: {}", interface_id);
}
}
_ => (),
}
Ok(())
}
pub async fn install(
&self,
ctx: &RpcContext,