mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
Refactor/project structure (#3085)
* refactor project structure * environment-based default registry * fix tests * update build container * use docker platform for iso build emulation * simplify compat * Fix docker platform spec in run-compat.sh * handle riscv compat * fix bug with dep error exists attr * undo removal of sorting * use qemu for iso stage --------- Co-authored-by: Mariusz Kogen <k0gen@pm.me> Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
410
core/src/registry/admin.rs
Normal file
410
core/src/registry/admin.rs
Normal file
@@ -0,0 +1,410 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::VersionRange;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::RegistryDatabase;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::signer::{ContactInfo, SignerInfo};
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
|
||||
pub fn admin_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"signer",
|
||||
signers_api::<C>().with_about("Commands to add or list signers"),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_admin)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(cli_add_admin)
|
||||
.no_display()
|
||||
.with_about("Add admin signer"),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_admin)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove an admin signer")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_admins)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
|
||||
.with_about("List admin signers")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn signers_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_signers)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
|
||||
.with_about("List signers")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(cli_add_signer).with_about("Add signer"),
|
||||
)
|
||||
.subcommand(
|
||||
"edit",
|
||||
from_fn_async(edit_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
impl Model<BTreeMap<Guid, SignerInfo>> {
|
||||
pub fn get_signer(&self, key: &AnyVerifyingKey) -> Result<Guid, Error> {
|
||||
self.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(guid, s)| Ok::<_, Error>((guid, s.as_keys().de()?)))
|
||||
.filter_ok(|(_, s)| s.contains(key))
|
||||
.next()
|
||||
.transpose()?
|
||||
.map(|(a, _)| a)
|
||||
.ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization))
|
||||
}
|
||||
|
||||
pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> {
|
||||
self.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(guid, s)| Ok::<_, Error>((guid, s.de()?)))
|
||||
.filter_ok(|(_, s)| s.keys.contains(key))
|
||||
.next()
|
||||
.transpose()?
|
||||
.ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization))
|
||||
}
|
||||
|
||||
pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> {
|
||||
if let Some((guid, s)) = self
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(guid, s)| Ok::<_, Error>((guid, s.de()?)))
|
||||
.filter_ok(|(_, s)| !s.keys.is_disjoint(&signer.keys))
|
||||
.next()
|
||||
.transpose()?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"A signer {} ({}) already exists with a matching key",
|
||||
guid,
|
||||
s.name
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
let id = Guid::new();
|
||||
self.insert(&id, signer)?;
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_signers(ctx: RegistryContext) -> Result<BTreeMap<Guid, SignerInfo>, Error> {
|
||||
ctx.db.peek().await.into_index().into_signers().de()
|
||||
}
|
||||
|
||||
pub fn display_signers<T>(
|
||||
params: WithIoFormat<T>,
|
||||
signers: BTreeMap<Guid, SignerInfo>,
|
||||
) -> Result<(), Error> {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, signers);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"ID",
|
||||
"NAME",
|
||||
"CONTACT",
|
||||
"KEYS",
|
||||
]);
|
||||
for (id, info) in signers {
|
||||
table.add_row(row![
|
||||
id.as_ref(),
|
||||
&info.name,
|
||||
&info.contact.into_iter().join("\n"),
|
||||
&info.keys.into_iter().join("\n"),
|
||||
]);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn display_package_signers<T>(
|
||||
params: WithIoFormat<T>,
|
||||
signers: BTreeMap<Guid, (SignerInfo, VersionRange)>,
|
||||
) -> Result<(), Error> {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, signers);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"ID",
|
||||
"NAME",
|
||||
"CONTACT",
|
||||
"KEYS",
|
||||
"AUTHORIZED VERSIONS"
|
||||
]);
|
||||
for (id, (info, versions)) in signers {
|
||||
table.add_row(row![
|
||||
id.as_ref(),
|
||||
&info.name,
|
||||
&info.contact.into_iter().join("\n"),
|
||||
&info.keys.into_iter().join("\n"),
|
||||
&versions.to_string()
|
||||
]);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<Guid, Error> {
|
||||
ctx.db
|
||||
.mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer))
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[ts(export)]
|
||||
pub struct EditSignerParams {
|
||||
pub id: Guid,
|
||||
#[arg(short = 'n', long)]
|
||||
pub set_name: Option<String>,
|
||||
#[arg(short = 'c', long)]
|
||||
pub add_contact: Vec<ContactInfo>,
|
||||
#[arg(short = 'k', long)]
|
||||
pub add_key: Vec<AnyVerifyingKey>,
|
||||
#[arg(short = 'C', long)]
|
||||
pub remove_contact: Vec<ContactInfo>,
|
||||
#[arg(short = 'K', long)]
|
||||
pub remove_key: Vec<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
pub async fn edit_signer(
|
||||
ctx: RegistryContext,
|
||||
EditSignerParams {
|
||||
id,
|
||||
set_name,
|
||||
add_contact,
|
||||
add_key,
|
||||
remove_contact,
|
||||
remove_key,
|
||||
}: EditSignerParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_signers_mut()
|
||||
.as_idx_mut(&id)
|
||||
.or_not_found(&id)?
|
||||
.mutate(|s| {
|
||||
if let Some(name) = set_name {
|
||||
s.name = name;
|
||||
}
|
||||
s.contact.extend(add_contact);
|
||||
for rm in remove_contact {
|
||||
let Some((idx, _)) = s.contact.iter().enumerate().find(|(_, c)| *c == &rm)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
s.contact.remove(idx);
|
||||
}
|
||||
|
||||
s.keys.extend(add_key);
|
||||
for rm in remove_key {
|
||||
s.keys.remove(&rm);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliAddSignerParams {
|
||||
#[arg(long = "name", short = 'n')]
|
||||
pub name: String,
|
||||
#[arg(long = "contact", short = 'c')]
|
||||
pub contact: Vec<ContactInfo>,
|
||||
#[arg(long = "key")]
|
||||
pub keys: Vec<AnyVerifyingKey>,
|
||||
pub database: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub async fn cli_add_signer(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliAddSignerParams {
|
||||
name,
|
||||
contact,
|
||||
keys,
|
||||
database,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddSignerParams>,
|
||||
) -> Result<Guid, Error> {
|
||||
let signer = SignerInfo {
|
||||
name,
|
||||
contact,
|
||||
keys: keys.into_iter().collect(),
|
||||
};
|
||||
if let Some(database) = database {
|
||||
TypedPatchDb::<RegistryDatabase>::load(PatchDb::open(database).await?)
|
||||
.await?
|
||||
.mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer))
|
||||
.await
|
||||
.result
|
||||
} else {
|
||||
from_value(
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
to_value(&signer)?,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddAdminParams {
|
||||
pub signer: Guid,
|
||||
}
|
||||
|
||||
pub async fn add_admin(
|
||||
ctx: RegistryContext,
|
||||
AddAdminParams { signer }: AddAdminParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
ensure_code!(
|
||||
db.as_index().as_signers().contains_key(&signer)?,
|
||||
ErrorKind::InvalidRequest,
|
||||
"unknown signer {signer}"
|
||||
);
|
||||
db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemoveAdminParams {
|
||||
pub signer: Guid,
|
||||
}
|
||||
|
||||
// TODO: don't allow removing self?
|
||||
pub async fn remove_admin(
|
||||
ctx: RegistryContext,
|
||||
RemoveAdminParams { signer }: RemoveAdminParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_admins_mut().mutate(|a| Ok(a.remove(&signer)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliAddAdminParams {
|
||||
pub signer: Guid,
|
||||
pub database: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub async fn cli_add_admin(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliAddAdminParams { signer, database },
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddAdminParams>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(database) = database {
|
||||
TypedPatchDb::<RegistryDatabase>::load(PatchDb::open(database).await?)
|
||||
.await?
|
||||
.mutate(|db| {
|
||||
ensure_code!(
|
||||
db.as_index().as_signers().contains_key(&signer)?,
|
||||
ErrorKind::InvalidRequest,
|
||||
"unknown signer {signer}"
|
||||
);
|
||||
db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
} else {
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
to_value(&AddAdminParams { signer })?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_admins(ctx: RegistryContext) -> Result<BTreeMap<Guid, SignerInfo>, Error> {
|
||||
let db = ctx.db.peek().await;
|
||||
let admins = db.as_admins().de()?;
|
||||
Ok(db
|
||||
.into_index()
|
||||
.into_signers()
|
||||
.de()?
|
||||
.into_iter()
|
||||
.filter(|(id, _)| admins.contains(id))
|
||||
.collect())
|
||||
}
|
||||
156
core/src/registry/asset.rs
Normal file
156
core/src/registry/asset.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWrite;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::progress::PhaseProgressTrackerHandle;
|
||||
use crate::registry::signer::AcceptSigners;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
||||
use crate::s9pk::merkle_archive::source::{ArchiveSource, Section};
|
||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||
use crate::sign::commitment::{Commitment, Digestable};
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey};
|
||||
use crate::upload::UploadingFile;
|
||||
use crate::util::future::NonDetachingJoinHandle;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RegistryAsset<Commitment> {
|
||||
#[ts(type = "string")]
|
||||
pub published_at: DateTime<Utc>,
|
||||
#[ts(type = "string")]
|
||||
pub url: Url,
|
||||
pub commitment: Commitment,
|
||||
pub signatures: HashMap<AnyVerifyingKey, AnySignature>,
|
||||
}
|
||||
impl<Commitment> RegistryAsset<Commitment> {
|
||||
pub fn all_signers(&self) -> AcceptSigners {
|
||||
AcceptSigners::All(
|
||||
self.signatures
|
||||
.keys()
|
||||
.cloned()
|
||||
.map(AcceptSigners::Signer)
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<Commitment: Digestable> RegistryAsset<Commitment> {
|
||||
pub fn validate(&self, context: &str, mut accept: AcceptSigners) -> Result<&Commitment, Error> {
|
||||
for (signer, signature) in &self.signatures {
|
||||
accept.process_signature(signer, &self.commitment, context, signature)?;
|
||||
}
|
||||
accept.try_accept()?;
|
||||
Ok(&self.commitment)
|
||||
}
|
||||
}
|
||||
impl<C: for<'a> Commitment<&'a HttpSource>> RegistryAsset<C> {
|
||||
pub async fn download(
|
||||
&self,
|
||||
client: Client,
|
||||
dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized),
|
||||
) -> Result<(), Error> {
|
||||
self.commitment
|
||||
.copy_to(&HttpSource::new(client, self.url.clone()).await?, dst)
|
||||
.await
|
||||
}
|
||||
}
|
||||
impl RegistryAsset<MerkleArchiveCommitment> {
|
||||
pub async fn deserialize_s9pk(
|
||||
&self,
|
||||
client: Client,
|
||||
) -> Result<S9pk<Section<Arc<HttpSource>>>, Error> {
|
||||
S9pk::deserialize(
|
||||
&Arc::new(HttpSource::new(client, self.url.clone()).await?),
|
||||
Some(&self.commitment),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn deserialize_s9pk_buffered(
|
||||
&self,
|
||||
client: Client,
|
||||
progress: PhaseProgressTrackerHandle,
|
||||
) -> Result<S9pk<Section<Arc<BufferedHttpSource>>>, Error> {
|
||||
S9pk::deserialize(
|
||||
&Arc::new(BufferedHttpSource::new(client, self.url.clone(), progress).await?),
|
||||
Some(&self.commitment),
|
||||
)
|
||||
.await
|
||||
}
|
||||
pub async fn download_to(
|
||||
&self,
|
||||
path: impl AsRef<Path>,
|
||||
client: Client,
|
||||
progress: PhaseProgressTrackerHandle,
|
||||
) -> Result<
|
||||
(
|
||||
S9pk<Section<Arc<BufferedHttpSource>>>,
|
||||
Arc<BufferedHttpSource>,
|
||||
),
|
||||
Error,
|
||||
> {
|
||||
let source = Arc::new(
|
||||
BufferedHttpSource::with_path(path, client, self.url.clone(), progress).await?,
|
||||
);
|
||||
Ok((
|
||||
S9pk::deserialize(&source, Some(&self.commitment)).await?,
|
||||
source,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BufferedHttpSource {
|
||||
_download: NonDetachingJoinHandle<()>,
|
||||
file: UploadingFile,
|
||||
}
|
||||
impl BufferedHttpSource {
|
||||
pub async fn with_path(
|
||||
path: impl AsRef<Path>,
|
||||
client: Client,
|
||||
url: Url,
|
||||
progress: PhaseProgressTrackerHandle,
|
||||
) -> Result<Self, Error> {
|
||||
let (mut handle, file) = UploadingFile::with_path(path, progress).await?;
|
||||
let response = client.get(url).send().await?;
|
||||
Ok(Self {
|
||||
_download: tokio::spawn(async move { handle.download(response).await }).into(),
|
||||
file,
|
||||
})
|
||||
}
|
||||
pub async fn new(
|
||||
client: Client,
|
||||
url: Url,
|
||||
progress: PhaseProgressTrackerHandle,
|
||||
) -> Result<Self, Error> {
|
||||
let (mut handle, file) = UploadingFile::new(progress).await?;
|
||||
let response = client.get(url).send().await?;
|
||||
Ok(Self {
|
||||
_download: tokio::spawn(async move { handle.download(response).await }).into(),
|
||||
file,
|
||||
})
|
||||
}
|
||||
pub async fn wait_for_buffered(&self) -> Result<(), Error> {
|
||||
self.file.wait_for_complete().await
|
||||
}
|
||||
}
|
||||
impl ArchiveSource for BufferedHttpSource {
|
||||
type FetchReader = <UploadingFile as ArchiveSource>::FetchReader;
|
||||
type FetchAllReader = <UploadingFile as ArchiveSource>::FetchAllReader;
|
||||
async fn size(&self) -> Option<u64> {
|
||||
self.file.size().await
|
||||
}
|
||||
async fn fetch_all(&self) -> Result<Self::FetchAllReader, Error> {
|
||||
self.file.fetch_all().await
|
||||
}
|
||||
async fn fetch(&self, position: u64, size: u64) -> Result<Self::FetchReader, Error> {
|
||||
self.file.fetch(position, size).await
|
||||
}
|
||||
}
|
||||
343
core/src/registry/context.rs
Normal file
343
core/src/registry/context.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use cookie::{Cookie, Expiration, SameSite};
|
||||
use http::HeaderMap;
|
||||
use imbl_value::InternedString;
|
||||
use patch_db::PatchDb;
|
||||
use patch_db::json_ptr::ROOT;
|
||||
use reqwest::{Client, Proxy};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{CallRemote, Context, Empty, RpcRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::context::config::{CONFIG_PATH, ContextConfig};
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::middleware::auth::DbContext;
|
||||
use crate::middleware::auth::local::LocalAuthContext;
|
||||
use crate::middleware::auth::signature::SignatureAuthContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::RegistryDatabase;
|
||||
use crate::registry::device_info::{DEVICE_INFO_HEADER, DeviceInfo};
|
||||
use crate::registry::migrations::run_migrations;
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::rpc_continuations::RpcContinuations;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::util::io::{append_file, read_file_to_string};
|
||||
|
||||
const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
||||
SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), 5959);
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct RegistryConfig {
|
||||
#[arg(short = 'c', long = "config")]
|
||||
pub config: Option<PathBuf>,
|
||||
#[arg(short = 'l', long = "listen")]
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
#[arg(short = 'H', long = "hostname")]
|
||||
pub registry_hostname: Vec<InternedString>,
|
||||
#[arg(short = 'p', long = "tor-proxy")]
|
||||
pub tor_proxy: Option<Url>,
|
||||
#[arg(short = 'd', long = "datadir")]
|
||||
pub datadir: Option<PathBuf>,
|
||||
#[arg(short = 'u', long = "pg-connection-url")]
|
||||
pub pg_connection_url: Option<String>,
|
||||
}
|
||||
impl ContextConfig for RegistryConfig {
|
||||
fn next(&mut self) -> Option<PathBuf> {
|
||||
self.config.take()
|
||||
}
|
||||
fn merge_with(&mut self, mut other: Self) {
|
||||
self.registry_listen = self.registry_listen.take().or(other.registry_listen);
|
||||
self.registry_hostname.append(&mut other.registry_hostname);
|
||||
self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy);
|
||||
self.datadir = self.datadir.take().or(other.datadir);
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryConfig {
|
||||
pub fn load(mut self) -> Result<Self, Error> {
|
||||
let path = self.next();
|
||||
self.load_path_rec(path)?;
|
||||
self.load_path_rec(Some(CONFIG_PATH))?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegistryContextSeed {
|
||||
pub hostnames: Vec<InternedString>,
|
||||
pub listen: SocketAddr,
|
||||
pub db: TypedPatchDb<RegistryDatabase>,
|
||||
pub datadir: PathBuf,
|
||||
pub rpc_continuations: RpcContinuations,
|
||||
pub client: Client,
|
||||
pub shutdown: Sender<()>,
|
||||
pub pool: Option<PgPool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegistryContext(Arc<RegistryContextSeed>);
|
||||
impl RegistryContext {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn init(config: &RegistryConfig) -> Result<Self, Error> {
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
let datadir = config
|
||||
.datadir
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| Path::new("/var/lib/startos"))
|
||||
.to_owned();
|
||||
if tokio::fs::metadata(&datadir).await.is_err() {
|
||||
tokio::fs::create_dir_all(&datadir).await?;
|
||||
}
|
||||
let db_path = datadir.join("registry.db");
|
||||
let db = TypedPatchDb::<RegistryDatabase>::load_unchecked(PatchDb::open(&db_path).await?);
|
||||
if db.dump(&ROOT).await.value.is_null() {
|
||||
db.put(&ROOT, &RegistryDatabase::init()).await?;
|
||||
}
|
||||
db.mutate(|db| run_migrations(db)).await.result?;
|
||||
|
||||
Self::init_auth_cookie().await?;
|
||||
|
||||
let tor_proxy_url = config
|
||||
.tor_proxy
|
||||
.clone()
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| "socks5h://localhost:9050".parse())?;
|
||||
let pool: Option<PgPool> = match &config.pg_connection_url {
|
||||
Some(url) => match PgPool::connect(url.as_str()).await {
|
||||
Ok(pool) => Some(pool),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if config.registry_hostname.is_empty() {
|
||||
return Err(Error::new(
|
||||
eyre!("missing required configuration: registry-hostname"),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
Ok(Self(Arc::new(RegistryContextSeed {
|
||||
hostnames: config.registry_hostname.clone(),
|
||||
listen: config.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN),
|
||||
db,
|
||||
datadir,
|
||||
rpc_continuations: RpcContinuations::new(),
|
||||
client: Client::builder()
|
||||
.proxy(Proxy::custom(move |url| {
|
||||
if url.host_str().map_or(false, |h| h.ends_with(".onion")) {
|
||||
Some(tor_proxy_url.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
.with_kind(crate::ErrorKind::ParseUrl)?,
|
||||
shutdown,
|
||||
pool,
|
||||
})))
|
||||
}
|
||||
}
|
||||
impl AsRef<RpcContinuations> for RegistryContext {
|
||||
fn as_ref(&self) -> &RpcContinuations {
|
||||
&self.rpc_continuations
|
||||
}
|
||||
}
|
||||
|
||||
impl Context for RegistryContext {}
|
||||
impl Deref for RegistryContext {
|
||||
type Target = RegistryContextSeed;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
pub struct RegistryUrlParams {
|
||||
pub registry: Url,
|
||||
}
|
||||
|
||||
impl CallRemote<RegistryContext> for CliContext {
|
||||
async fn call_remote(
|
||||
&self,
|
||||
mut method: &str,
|
||||
params: Value,
|
||||
_: Empty,
|
||||
) -> Result<Value, RpcError> {
|
||||
let cookie = read_file_to_string(RegistryContext::LOCAL_AUTH_COOKIE_PATH).await;
|
||||
|
||||
let url = if let Some(url) = self.registry_url.clone() {
|
||||
url
|
||||
} else if cookie.is_ok() || !self.registry_hostname.is_empty() {
|
||||
let mut url: Url = format!(
|
||||
"http://{}",
|
||||
self.registry_listen.unwrap_or(DEFAULT_REGISTRY_LISTEN)
|
||||
)
|
||||
.parse()
|
||||
.map_err(Error::from)?;
|
||||
url.path_segments_mut()
|
||||
.map_err(|_| eyre!("Url cannot be base"))
|
||||
.with_kind(crate::ErrorKind::ParseUrl)?
|
||||
.push("rpc")
|
||||
.push("v0");
|
||||
url
|
||||
} else {
|
||||
return Err(
|
||||
Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest).into(),
|
||||
);
|
||||
};
|
||||
|
||||
if let Ok(local) = cookie {
|
||||
let cookie_url = match url.host() {
|
||||
Some(url::Host::Ipv4(ip)) if ip.is_loopback() => url.clone(),
|
||||
Some(url::Host::Ipv6(ip)) if ip.is_loopback() => url.clone(),
|
||||
_ => format!("http://{DEFAULT_REGISTRY_LISTEN}").parse()?,
|
||||
};
|
||||
self.cookie_store
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_raw(
|
||||
&Cookie::build(("local", local))
|
||||
.domain(cookie_url.host_str().unwrap_or("localhost"))
|
||||
.expires(Expiration::Session)
|
||||
.same_site(SameSite::Strict)
|
||||
.build(),
|
||||
&cookie_url,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
method = method.strip_prefix("registry.").unwrap_or(method);
|
||||
let sig_context = self
|
||||
.registry_hostname
|
||||
.get(0)
|
||||
.cloned()
|
||||
.or_else(|| url.host().as_ref().map(InternedString::from_display));
|
||||
|
||||
crate::middleware::auth::signature::call_remote(
|
||||
self,
|
||||
url,
|
||||
HeaderMap::new(),
|
||||
sig_context.as_deref(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl CallRemote<RegistryContext, RegistryUrlParams> for RpcContext {
|
||||
async fn call_remote(
|
||||
&self,
|
||||
mut method: &str,
|
||||
params: Value,
|
||||
RegistryUrlParams { mut registry }: RegistryUrlParams,
|
||||
) -> Result<Value, RpcError> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
DEVICE_INFO_HEADER,
|
||||
DeviceInfo::load(self).await?.to_header_value(),
|
||||
);
|
||||
|
||||
registry
|
||||
.path_segments_mut()
|
||||
.map_err(|_| Error::new(eyre!("cannot extend URL path"), ErrorKind::ParseUrl))?
|
||||
.push("rpc")
|
||||
.push("v0");
|
||||
|
||||
method = method.strip_prefix("registry.").unwrap_or(method);
|
||||
let sig_context = registry.host_str().map(InternedString::from);
|
||||
|
||||
crate::middleware::auth::signature::call_remote(
|
||||
self,
|
||||
registry,
|
||||
headers,
|
||||
sig_context.as_deref(),
|
||||
method,
|
||||
params,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RegistryAuthMetadata {
|
||||
#[serde(default)]
|
||||
admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TS)]
|
||||
pub struct AdminLogRecord {
|
||||
pub timestamp: String,
|
||||
pub name: String,
|
||||
#[ts(type = "{ id: string | number | null; method: string; params: any }")]
|
||||
pub request: RpcRequest,
|
||||
pub key: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
impl DbContext for RegistryContext {
|
||||
type Database = RegistryDatabase;
|
||||
fn db(&self) -> &TypedPatchDb<Self::Database> {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
impl LocalAuthContext for RegistryContext {
|
||||
const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/registry.authcookie";
|
||||
const LOCAL_AUTH_COOKIE_OWNERSHIP: &str = "root:root";
|
||||
}
|
||||
impl SignatureAuthContext for RegistryContext {
|
||||
type AdditionalMetadata = RegistryAuthMetadata;
|
||||
type CheckPubkeyRes = Option<(AnyVerifyingKey, SignerInfo)>;
|
||||
async fn sig_context(
|
||||
&self,
|
||||
) -> impl IntoIterator<Item = Result<impl AsRef<str> + Send, Error>> + Send {
|
||||
self.hostnames.iter().map(Ok)
|
||||
}
|
||||
fn check_pubkey(
|
||||
db: &Model<Self::Database>,
|
||||
pubkey: Option<&AnyVerifyingKey>,
|
||||
metadata: Self::AdditionalMetadata,
|
||||
) -> Result<Self::CheckPubkeyRes, Error> {
|
||||
if let Some(pubkey) = pubkey {
|
||||
let (guid, admin) = db.as_index().as_signers().get_signer_info(pubkey)?;
|
||||
if !metadata.admin || db.as_admins().de()?.contains(&guid) {
|
||||
return Ok(Some((pubkey.clone(), admin)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
|
||||
}
|
||||
async fn post_auth_hook(
|
||||
&self,
|
||||
check_pubkey_res: Self::CheckPubkeyRes,
|
||||
request: &RpcRequest,
|
||||
) -> Result<(), Error> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
if let Some((pubkey, admin)) = check_pubkey_res {
|
||||
let mut log = append_file(self.datadir.join("admin.log")).await?;
|
||||
log.write_all(
|
||||
(serde_json::to_string(&AdminLogRecord {
|
||||
timestamp: Utc::now().to_rfc3339(),
|
||||
name: admin.name,
|
||||
request: request.clone(),
|
||||
key: pubkey,
|
||||
})
|
||||
.with_kind(ErrorKind::Serialization)?
|
||||
+ "\n")
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
183
core/src/registry/db.rs
Normal file
183
core/src/registry/db.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use itertools::Itertools;
|
||||
use patch_db::Dump;
|
||||
use patch_db::json_ptr::{JsonPointer, ROOT};
|
||||
use rpc_toolkit::yajrc::RpcError;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::RegistryDatabase;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||
|
||||
pub fn db_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"dump",
|
||||
from_fn_async(cli_dump)
|
||||
.with_display_serializable()
|
||||
.with_about("Filter/query db to display tables and records"),
|
||||
)
|
||||
.subcommand(
|
||||
"dump",
|
||||
from_fn_async(dump)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"apply",
|
||||
from_fn_async(cli_apply)
|
||||
.no_display()
|
||||
.with_about("Update a db record"),
|
||||
)
|
||||
.subcommand(
|
||||
"apply",
|
||||
from_fn_async(apply)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct CliDumpParams {
|
||||
#[arg(long = "pointer", short = 'p')]
|
||||
pointer: Option<JsonPointer>,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_dump(
|
||||
HandlerArgs {
|
||||
context,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliDumpParams { pointer, path },
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliDumpParams>,
|
||||
) -> Result<Dump, RpcError> {
|
||||
let dump = if let Some(path) = path {
|
||||
PatchDb::open(path).await?.dump(&ROOT).await
|
||||
} else {
|
||||
let method = parent_method.into_iter().chain(method).join(".");
|
||||
from_value::<Dump>(
|
||||
context
|
||||
.call_remote::<RegistryContext>(&method, imbl_value::json!({ "pointer": pointer }))
|
||||
.await?,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(dump)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct DumpParams {
|
||||
#[arg(long = "pointer", short = 'p')]
|
||||
#[ts(type = "string | null")]
|
||||
pointer: Option<JsonPointer>,
|
||||
}
|
||||
|
||||
pub async fn dump(ctx: RegistryContext, DumpParams { pointer }: DumpParams) -> Result<Dump, Error> {
|
||||
Ok(ctx
|
||||
.db
|
||||
.dump(&pointer.as_ref().map_or(ROOT, |p| p.borrowed()))
|
||||
.await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct CliApplyParams {
|
||||
expr: String,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
async fn cli_apply(
|
||||
HandlerArgs {
|
||||
context,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliApplyParams { expr, path },
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliApplyParams>,
|
||||
) -> Result<(), RpcError> {
|
||||
if let Some(path) = path {
|
||||
PatchDb::open(path)
|
||||
.await?
|
||||
.apply_function(|db| {
|
||||
let res = apply_expr(
|
||||
serde_json::to_value(patch_db::Value::from(db))
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.into(),
|
||||
&expr,
|
||||
)?;
|
||||
|
||||
Ok::<_, Error>((
|
||||
to_value(
|
||||
&serde_json::from_value::<RegistryDatabase>(res.clone().into()).with_ctx(
|
||||
|_| {
|
||||
(
|
||||
crate::ErrorKind::Deserialization,
|
||||
"result does not match database model",
|
||||
)
|
||||
},
|
||||
)?,
|
||||
)?,
|
||||
(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
} else {
|
||||
let method = parent_method.into_iter().chain(method).join(".");
|
||||
context
|
||||
.call_remote::<RegistryContext>(&method, imbl_value::json!({ "expr": expr }))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ApplyParams {
|
||||
expr: String,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub async fn apply(
|
||||
ctx: RegistryContext,
|
||||
ApplyParams { expr, .. }: ApplyParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let res = apply_expr(
|
||||
serde_json::to_value(patch_db::Value::from(db.clone()))
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.into(),
|
||||
&expr,
|
||||
)?;
|
||||
|
||||
db.ser(
|
||||
&serde_json::from_value::<RegistryDatabase>(res.clone().into()).with_ctx(|_| {
|
||||
(
|
||||
crate::ErrorKind::Deserialization,
|
||||
"result does not match database model",
|
||||
)
|
||||
})?,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
188
core/src/registry/device_info.rs
Normal file
188
core/src/registry/device_info.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::identity;
|
||||
use std::ops::Deref;
|
||||
|
||||
use axum::extract::Request;
|
||||
use axum::response::Response;
|
||||
use exver::{Version, VersionRange};
|
||||
use http::HeaderValue;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{Middleware, RpcRequest, RpcResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::util::VersionString;
|
||||
use crate::util::lshw::{LshwDevice, LshwDisplay, LshwProcessor};
|
||||
use crate::version::VersionT;
|
||||
|
||||
pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceInfo {
|
||||
pub os: OsInfo,
|
||||
pub hardware: HardwareInfo,
|
||||
}
|
||||
impl DeviceInfo {
|
||||
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
os: OsInfo::from(ctx),
|
||||
hardware: HardwareInfo::load(ctx).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
impl DeviceInfo {
|
||||
pub fn to_header_value(&self) -> HeaderValue {
|
||||
let mut url: Url = "http://localhost".parse().unwrap();
|
||||
url.query_pairs_mut()
|
||||
.append_pair("os.version", &self.os.version.to_string())
|
||||
.append_pair("os.compat", &self.os.compat.to_string())
|
||||
.append_pair("os.platform", &*self.os.platform)
|
||||
.append_pair("hardware.arch", &*self.hardware.arch)
|
||||
.append_pair("hardware.ram", &self.hardware.ram.to_string());
|
||||
|
||||
for device in &self.hardware.devices {
|
||||
url.query_pairs_mut().append_pair(
|
||||
&format!("hardware.device.{}", device.class()),
|
||||
device.product(),
|
||||
);
|
||||
}
|
||||
|
||||
HeaderValue::from_str(url.query().unwrap_or_default()).unwrap()
|
||||
}
|
||||
pub fn from_header_value(header: &HeaderValue) -> Result<Self, Error> {
|
||||
let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect();
|
||||
Ok(Self {
|
||||
os: OsInfo {
|
||||
version: query
|
||||
.get("os.version")
|
||||
.or_not_found("os.version")?
|
||||
.parse()?,
|
||||
compat: query.get("os.compat").or_not_found("os.compat")?.parse()?,
|
||||
platform: query
|
||||
.get("os.platform")
|
||||
.or_not_found("os.platform")?
|
||||
.deref()
|
||||
.into(),
|
||||
},
|
||||
hardware: HardwareInfo {
|
||||
arch: query
|
||||
.get("hardware.arch")
|
||||
.or_not_found("hardware.arch")?
|
||||
.parse()?,
|
||||
ram: query
|
||||
.get("hardware.ram")
|
||||
.or_not_found("hardware.ram")?
|
||||
.parse()?,
|
||||
devices: identity(query)
|
||||
.split_off("hardware.device.")
|
||||
.into_iter()
|
||||
.filter_map(|(k, v)| match k.strip_prefix("hardware.device.") {
|
||||
Some("processor") => Some(LshwDevice::Processor(LshwProcessor {
|
||||
product: v.into_owned(),
|
||||
})),
|
||||
Some("display") => Some(LshwDevice::Display(LshwDisplay {
|
||||
product: v.into_owned(),
|
||||
})),
|
||||
Some(class) => {
|
||||
tracing::warn!("unknown device class: {class}");
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OsInfo {
|
||||
#[ts(as = "VersionString")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub compat: VersionRange,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
}
|
||||
impl From<&RpcContext> for OsInfo {
|
||||
fn from(_: &RpcContext) -> Self {
|
||||
Self {
|
||||
version: crate::version::Current::default().semver(),
|
||||
compat: crate::version::Current::default().compat().clone(),
|
||||
platform: InternedString::intern(&*crate::PLATFORM),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HardwareInfo {
|
||||
#[ts(type = "string")]
|
||||
pub arch: InternedString,
|
||||
#[ts(type = "number")]
|
||||
pub ram: u64,
|
||||
pub devices: Vec<LshwDevice>,
|
||||
}
|
||||
impl HardwareInfo {
|
||||
pub async fn load(ctx: &RpcContext) -> Result<Self, Error> {
|
||||
let s = ctx.db.peek().await.into_public().into_server_info();
|
||||
Ok(Self {
|
||||
arch: s.as_arch().de()?,
|
||||
ram: s.as_ram().de()?,
|
||||
devices: s.as_devices().de()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Metadata {
|
||||
#[serde(default)]
|
||||
get_device_info: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceInfoMiddleware {
|
||||
device_info: Option<HeaderValue>,
|
||||
}
|
||||
impl DeviceInfoMiddleware {
|
||||
pub fn new() -> Self {
|
||||
Self { device_info: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Middleware<RegistryContext> for DeviceInfoMiddleware {
|
||||
type Metadata = Metadata;
|
||||
async fn process_http_request(
|
||||
&mut self,
|
||||
_: &RegistryContext,
|
||||
request: &mut Request,
|
||||
) -> Result<(), Response> {
|
||||
self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER);
|
||||
Ok(())
|
||||
}
|
||||
async fn process_rpc_request(
|
||||
&mut self,
|
||||
_: &RegistryContext,
|
||||
metadata: Self::Metadata,
|
||||
request: &mut RpcRequest,
|
||||
) -> Result<(), RpcResponse> {
|
||||
async move {
|
||||
if metadata.get_device_info {
|
||||
if let Some(device_info) = &self.device_info {
|
||||
request.params["__DeviceInfo_device_info"] =
|
||||
to_value(&DeviceInfo::from_header_value(device_info)?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await
|
||||
.map_err(|e| RpcResponse::from_result(Err(e)))
|
||||
}
|
||||
}
|
||||
128
core/src/registry/info.rs
Normal file
128
core/src/registry/info.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::package::index::Category;
|
||||
use crate::util::DataUrl;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat};
|
||||
|
||||
pub fn info_api<C: Context>() -> ParentHandler<C, WithIoFormat<Empty>> {
|
||||
ParentHandler::<C, WithIoFormat<Empty>>::new()
|
||||
.root_handler(
|
||||
from_fn_async(get_info)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_about("Display registry name, icon, and package categories")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-name",
|
||||
from_fn_async(set_name)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Set the name for the registry")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-icon",
|
||||
from_fn_async(set_icon)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"set-icon",
|
||||
from_fn_async(cli_set_icon)
|
||||
.no_display()
|
||||
.with_about("Set the icon for the registry"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RegistryInfo {
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<DataUrl<'static>>,
|
||||
pub categories: BTreeMap<InternedString, Category>,
|
||||
}
|
||||
|
||||
pub async fn get_info(ctx: RegistryContext) -> Result<RegistryInfo, Error> {
|
||||
let peek = ctx.db.peek().await.into_index();
|
||||
Ok(RegistryInfo {
|
||||
name: peek.as_name().de()?,
|
||||
icon: peek.as_icon().de()?,
|
||||
categories: peek.as_package().as_categories().de()?,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetNameParams {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub async fn set_name(
|
||||
ctx: RegistryContext,
|
||||
SetNameParams { name }: SetNameParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| db.as_index_mut().as_name_mut().ser(&Some(name)))
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SetIconParams {
|
||||
pub icon: DataUrl<'static>,
|
||||
}
|
||||
|
||||
pub async fn set_icon(
|
||||
ctx: RegistryContext,
|
||||
SetIconParams { icon }: SetIconParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| db.as_index_mut().as_icon_mut().ser(&Some(icon)))
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct CliSetIconParams {
|
||||
pub icon: PathBuf,
|
||||
}
|
||||
|
||||
pub async fn cli_set_icon(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliSetIconParams { icon },
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliSetIconParams>,
|
||||
) -> Result<(), Error> {
|
||||
let data_url = DataUrl::from_path(icon).await?;
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
imbl_value::json!({
|
||||
"icon": data_url,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
25
core/src/registry/metrics-db/registry-sqlx-data.sh
Executable file
25
core/src/registry/metrics-db/registry-sqlx-data.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
TMP_DIR=$(mktemp -d)
|
||||
mkdir $TMP_DIR/pgdata
|
||||
docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres
|
||||
|
||||
(
|
||||
set -e
|
||||
ctr=0
|
||||
until docker exec tmp_postgres psql -U postgres 2> /dev/null || [ $ctr -ge 5 ]; do
|
||||
ctr=$[ctr + 1]
|
||||
sleep 5;
|
||||
done
|
||||
|
||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres)
|
||||
|
||||
cat "./registry_schema.sql" | docker exec -i tmp_postgres psql -U postgres -d postgres -f-
|
||||
cd ../../..
|
||||
DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace
|
||||
echo "Subscript Complete"
|
||||
)
|
||||
|
||||
docker stop tmp_postgres
|
||||
sudo rm -rf $TMP_DIR
|
||||
828
core/src/registry/metrics-db/registry_schema.sql
Normal file
828
core/src/registry/metrics-db/registry_schema.sql
Normal file
@@ -0,0 +1,828 @@
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
-- Dumped from database version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
||||
-- Dumped by pg_dump version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1)
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET lock_timeout = 0;
|
||||
SET idle_in_transaction_session_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SELECT pg_catalog.set_config('search_path', '', false);
|
||||
SET check_function_bodies = false;
|
||||
SET xmloption = content;
|
||||
SET client_min_messages = warning;
|
||||
SET row_security = off;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_table_access_method = heap;
|
||||
|
||||
--
|
||||
-- Name: admin; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.admin (
|
||||
id character varying NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
pass_hash character varying NOT NULL,
|
||||
deleted_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.admin OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.admin_pkgs (
|
||||
id bigint NOT NULL,
|
||||
admin character varying NOT NULL,
|
||||
pkg_id character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.admin_pkgs OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.admin_pkgs_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.admin_pkgs_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.admin_pkgs_id_seq OWNED BY public.admin_pkgs.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: category; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.category (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
name character varying NOT NULL,
|
||||
description character varying NOT NULL,
|
||||
priority bigint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.category OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.category_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.category_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: eos_hash; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.eos_hash (
|
||||
id bigint NOT NULL,
|
||||
version character varying NOT NULL,
|
||||
hash character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.eos_hash OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: eos_hash_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.eos_hash_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.eos_hash_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: eos_hash_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.eos_hash_id_seq OWNED BY public.eos_hash.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: error_log_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.error_log_record (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
epoch character varying NOT NULL,
|
||||
commit_hash character varying NOT NULL,
|
||||
source_file character varying NOT NULL,
|
||||
line bigint NOT NULL,
|
||||
target character varying NOT NULL,
|
||||
level character varying NOT NULL,
|
||||
message character varying NOT NULL,
|
||||
incidents bigint NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.error_log_record OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: error_log_record_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.error_log_record_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.error_log_record_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: error_log_record_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.error_log_record_id_seq OWNED BY public.error_log_record.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: metric; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.metric (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
version character varying NOT NULL,
|
||||
pkg_id character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.metric OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: metric_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.metric_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.metric_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: metric_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.metric_id_seq OWNED BY public.metric.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: os_version; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.os_version (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
number character varying NOT NULL,
|
||||
headline character varying NOT NULL,
|
||||
release_notes character varying NOT NULL,
|
||||
arch character varying
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.os_version OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: os_version_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.os_version_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.os_version_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: os_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.os_version_id_seq OWNED BY public.os_version.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: persistent_migration; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.persistent_migration (
|
||||
id integer NOT NULL,
|
||||
version integer NOT NULL,
|
||||
label character varying,
|
||||
"timestamp" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.persistent_migration OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: persistent_migration_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.persistent_migration_id_seq
|
||||
AS integer
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.persistent_migration_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: persistent_migration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.persistent_migration_id_seq OWNED BY public.persistent_migration.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.pkg_category (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
category_id bigint NOT NULL,
|
||||
pkg_id character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.pkg_category OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.pkg_dependency (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
pkg_id character varying NOT NULL,
|
||||
pkg_version character varying NOT NULL,
|
||||
dep_id character varying NOT NULL,
|
||||
dep_version_range character varying NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.pkg_dependency OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.pkg_dependency_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.pkg_dependency_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.pkg_dependency_id_seq OWNED BY public.pkg_dependency.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_record; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.pkg_record (
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone,
|
||||
pkg_id character varying NOT NULL,
|
||||
hidden boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.pkg_record OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: service_category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.service_category_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.service_category_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: service_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.service_category_id_seq OWNED BY public.pkg_category.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: upload; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.upload (
|
||||
id bigint NOT NULL,
|
||||
uploader character varying NOT NULL,
|
||||
pkg_id character varying NOT NULL,
|
||||
pkg_version character varying NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.upload OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: upload_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.upload_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.upload_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: upload_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.upload_id_seq OWNED BY public.upload.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_activity; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.user_activity (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
server_id character varying NOT NULL,
|
||||
os_version character varying,
|
||||
arch character varying
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.user_activity OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: user_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE SEQUENCE public.user_activity_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
ALTER TABLE public.user_activity_id_seq OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: user_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER SEQUENCE public.user_activity_id_seq OWNED BY public.user_activity.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: version; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.version (
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone,
|
||||
number character varying NOT NULL,
|
||||
release_notes character varying NOT NULL,
|
||||
os_version character varying NOT NULL,
|
||||
pkg_id character varying NOT NULL,
|
||||
title character varying NOT NULL,
|
||||
desc_short character varying NOT NULL,
|
||||
desc_long character varying NOT NULL,
|
||||
icon_type character varying NOT NULL,
|
||||
deprecated_at timestamp with time zone
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.version OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: version_platform; Type: TABLE; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE TABLE public.version_platform (
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone,
|
||||
pkg_id character varying NOT NULL,
|
||||
version_number character varying NOT NULL,
|
||||
arch character varying NOT NULL,
|
||||
ram bigint,
|
||||
device jsonb
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE public.version_platform OWNER TO alpha_admin;
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.admin_pkgs ALTER COLUMN id SET DEFAULT nextval('public.admin_pkgs_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: eos_hash id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.eos_hash ALTER COLUMN id SET DEFAULT nextval('public.eos_hash_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: error_log_record id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.error_log_record ALTER COLUMN id SET DEFAULT nextval('public.error_log_record_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: metric id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.metric ALTER COLUMN id SET DEFAULT nextval('public.metric_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: os_version id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.os_version ALTER COLUMN id SET DEFAULT nextval('public.os_version_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: persistent_migration id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.persistent_migration ALTER COLUMN id SET DEFAULT nextval('public.persistent_migration_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_category ALTER COLUMN id SET DEFAULT nextval('public.service_category_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_dependency ALTER COLUMN id SET DEFAULT nextval('public.pkg_dependency_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: upload id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.upload ALTER COLUMN id SET DEFAULT nextval('public.upload_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_activity id; Type: DEFAULT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.user_activity ALTER COLUMN id SET DEFAULT nextval('public.user_activity_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.admin
|
||||
ADD CONSTRAINT admin_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs admin_pkgs_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.admin_pkgs
|
||||
ADD CONSTRAINT admin_pkgs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.category
|
||||
ADD CONSTRAINT category_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: eos_hash eos_hash_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.eos_hash
|
||||
ADD CONSTRAINT eos_hash_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: error_log_record error_log_record_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.error_log_record
|
||||
ADD CONSTRAINT error_log_record_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: metric metric_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.metric
|
||||
ADD CONSTRAINT metric_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: os_version os_version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.os_version
|
||||
ADD CONSTRAINT os_version_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: persistent_migration persistent_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.persistent_migration
|
||||
ADD CONSTRAINT persistent_migration_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category pkg_category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_category
|
||||
ADD CONSTRAINT pkg_category_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency pkg_dependency_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_dependency
|
||||
ADD CONSTRAINT pkg_dependency_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs unique_admin_pkg; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.admin_pkgs
|
||||
ADD CONSTRAINT unique_admin_pkg UNIQUE (pkg_id, admin);
|
||||
|
||||
|
||||
--
|
||||
-- Name: error_log_record unique_log_record; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.error_log_record
|
||||
ADD CONSTRAINT unique_log_record UNIQUE (epoch, commit_hash, source_file, line, target, level, message);
|
||||
|
||||
|
||||
--
|
||||
-- Name: category unique_name; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.category
|
||||
ADD CONSTRAINT unique_name UNIQUE (name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category unique_pkg_category; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_category
|
||||
ADD CONSTRAINT unique_pkg_category UNIQUE (pkg_id, category_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency unique_pkg_dep_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_dependency
|
||||
ADD CONSTRAINT unique_pkg_dep_version UNIQUE (pkg_id, pkg_version, dep_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: eos_hash unique_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.eos_hash
|
||||
ADD CONSTRAINT unique_version UNIQUE (version);
|
||||
|
||||
|
||||
--
|
||||
-- Name: upload upload_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.upload
|
||||
ADD CONSTRAINT upload_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: user_activity user_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.user_activity
|
||||
ADD CONSTRAINT user_activity_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.version
|
||||
ADD CONSTRAINT version_pkey PRIMARY KEY (pkg_id, number);
|
||||
|
||||
|
||||
--
|
||||
-- Name: version_platform version_platform_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.version_platform
|
||||
ADD CONSTRAINT version_platform_pkey PRIMARY KEY (pkg_id, version_number, arch);
|
||||
|
||||
|
||||
--
|
||||
-- Name: category_name_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX category_name_idx ON public.category USING btree (name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_record_pkg_id_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX pkg_record_pkg_id_idx ON public.pkg_record USING btree (pkg_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: version_number_idx; Type: INDEX; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
CREATE INDEX version_number_idx ON public.version USING btree (number);
|
||||
|
||||
|
||||
--
|
||||
-- Name: admin_pkgs admin_pkgs_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.admin_pkgs
|
||||
ADD CONSTRAINT admin_pkgs_admin_fkey FOREIGN KEY (admin) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: metric metric_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.metric
|
||||
ADD CONSTRAINT metric_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category pkg_category_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_category
|
||||
ADD CONSTRAINT pkg_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_category pkg_category_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_category
|
||||
ADD CONSTRAINT pkg_category_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency pkg_dependency_dep_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_dependency
|
||||
ADD CONSTRAINT pkg_dependency_dep_id_fkey FOREIGN KEY (dep_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: pkg_dependency pkg_dependency_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.pkg_dependency
|
||||
ADD CONSTRAINT pkg_dependency_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: upload upload_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.upload
|
||||
ADD CONSTRAINT upload_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: upload upload_uploader_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.upload
|
||||
ADD CONSTRAINT upload_uploader_fkey FOREIGN KEY (uploader) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: version version_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.version
|
||||
ADD CONSTRAINT version_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- Name: version_platform version_platform_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.version_platform
|
||||
ADD CONSTRAINT version_platform_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT;
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
||||
30
core/src/registry/migrations/m_00_package_signer_scope.rs
Normal file
30
core/src/registry/migrations/m_00_package_signer_scope.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use imbl_value::json;
|
||||
|
||||
use super::RegistryMigration;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub struct PackageSignerScopeMigration;
|
||||
impl RegistryMigration for PackageSignerScopeMigration {
|
||||
fn name(&self) -> &'static str {
|
||||
"PackageSignerScopeMigration"
|
||||
}
|
||||
fn action(&self, db: &mut Value) -> Result<(), Error> {
|
||||
for (_, info) in db["index"]["package"]["packages"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
{
|
||||
let prev = info["authorized"].clone();
|
||||
if let Some(prev) = prev.as_array() {
|
||||
info["authorized"] = Value::Object(
|
||||
prev.iter()
|
||||
.filter_map(|g| g.as_str())
|
||||
.map(|g| (g.into(), json!("*")))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
29
core/src/registry/migrations/mod.rs
Normal file
29
core/src/registry/migrations/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use patch_db::ModelExt;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::registry::RegistryDatabase;
|
||||
|
||||
mod m_00_package_signer_scope;
|
||||
|
||||
pub trait RegistryMigration {
|
||||
fn name(&self) -> &'static str;
|
||||
fn action(&self, db: &mut Value) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub const MIGRATIONS: &[&dyn RegistryMigration] =
|
||||
&[&m_00_package_signer_scope::PackageSignerScopeMigration];
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn run_migrations(db: &mut Model<RegistryDatabase>) -> Result<(), Error> {
|
||||
let mut migrations = db.as_migrations().de().unwrap_or_default();
|
||||
for migration in MIGRATIONS {
|
||||
if !migrations.contains(migration.name()) {
|
||||
migration.action(ModelExt::as_value_mut(db))?;
|
||||
migrations.insert(migration.name().into());
|
||||
}
|
||||
}
|
||||
let mut db_deser = db.de()?;
|
||||
db_deser.migrations = migrations;
|
||||
db.ser(&db_deser)?;
|
||||
Ok(())
|
||||
}
|
||||
157
core/src/registry/mod.rs
Normal file
157
core/src/registry/mod.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::Router;
|
||||
use futures::future::ready;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, Server, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::middleware::auth::Auth;
|
||||
use crate::middleware::cors::Cors;
|
||||
use crate::net::static_server::{bad_request, not_found, server_error};
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::device_info::DeviceInfoMiddleware;
|
||||
use crate::registry::os::index::OsIndex;
|
||||
use crate::registry::package::index::PackageIndex;
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::DataUrl;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub mod admin;
|
||||
pub mod asset;
|
||||
pub mod context;
|
||||
pub mod db;
|
||||
pub mod device_info;
|
||||
pub mod info;
|
||||
mod migrations;
|
||||
pub mod os;
|
||||
pub mod package;
|
||||
pub mod signer;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
pub struct RegistryDatabase {
|
||||
#[serde(default)]
|
||||
pub migrations: BTreeSet<InternedString>,
|
||||
pub admins: BTreeSet<Guid>,
|
||||
pub index: FullIndex,
|
||||
}
|
||||
|
||||
impl RegistryDatabase {
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
migrations: migrations::MIGRATIONS
|
||||
.iter()
|
||||
.map(|m| m.name().into())
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct FullIndex {
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<DataUrl<'static>>,
|
||||
pub package: PackageIndex,
|
||||
pub os: OsIndex,
|
||||
pub signers: BTreeMap<Guid, SignerInfo>,
|
||||
}
|
||||
|
||||
pub async fn get_full_index(ctx: RegistryContext) -> Result<FullIndex, Error> {
|
||||
ctx.db.peek().await.into_index().de()
|
||||
}
|
||||
|
||||
pub fn registry_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"index",
|
||||
from_fn_async(get_full_index)
|
||||
.with_display_serializable()
|
||||
.with_about("List info including registry name and packages")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand("info", info::info_api::<C>())
|
||||
// set info and categories
|
||||
.subcommand(
|
||||
"os",
|
||||
os::os_api::<C>().with_about("Commands related to OS assets and versions"),
|
||||
)
|
||||
.subcommand(
|
||||
"package",
|
||||
package::package_api::<C>().with_about("Commands to index, add, or get packages"),
|
||||
)
|
||||
.subcommand(
|
||||
"admin",
|
||||
admin::admin_api::<C>().with_about("Commands to add or list admins or signers"),
|
||||
)
|
||||
.subcommand(
|
||||
"db",
|
||||
db::db_api::<C>().with_about("Commands to interact with the db i.e. dump and apply"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn registry_router(ctx: RegistryContext) -> Router {
|
||||
use axum::extract as x;
|
||||
use axum::routing::{any, get};
|
||||
Router::new()
|
||||
.route("/rpc/{*path}", {
|
||||
let ctx = ctx.clone();
|
||||
any(
|
||||
Server::new(move || ready(Ok(ctx.clone())), registry_api())
|
||||
.middleware(Cors::new())
|
||||
.middleware(Auth::new().with_local_auth().with_signature_auth())
|
||||
.middleware(DeviceInfoMiddleware::new()),
|
||||
)
|
||||
})
|
||||
.route(
|
||||
"/ws/rpc/{*path}",
|
||||
get({
|
||||
let ctx = ctx.clone();
|
||||
move |x::Path(path): x::Path<String>,
|
||||
ws: axum::extract::ws::WebSocketUpgrade| async move {
|
||||
match Guid::from(&path) {
|
||||
None => {
|
||||
tracing::debug!("No Guid Path");
|
||||
bad_request()
|
||||
}
|
||||
Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await {
|
||||
Some(cont) => ws.on_upgrade(cont),
|
||||
_ => not_found(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/rest/rpc/{*path}",
|
||||
any({
|
||||
let ctx = ctx.clone();
|
||||
move |request: x::Request| async move {
|
||||
let path = request
|
||||
.uri()
|
||||
.path()
|
||||
.strip_prefix("/rest/rpc/")
|
||||
.unwrap_or_default();
|
||||
match Guid::from(&path) {
|
||||
None => {
|
||||
tracing::debug!("No Guid Path");
|
||||
bad_request()
|
||||
}
|
||||
Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await {
|
||||
None => not_found(),
|
||||
Some(cont) => cont(request).await.unwrap_or_else(server_error),
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
353
core/src/registry/os/asset/add.rs
Normal file
353
core/src/registry/os/asset/add.rs
Normal file
@@ -0,0 +1,353 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::panic::UnwindSafe;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, ProgressUnits};
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::os::SIG_CONTEXT;
|
||||
use crate::registry::os::index::OsVersionInfo;
|
||||
use crate::s9pk::merkle_archive::hash::VerifyingWriter;
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::sign::commitment::blake3::Blake3Commitment;
|
||||
use crate::sign::ed25519::Ed25519;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::serde::Base64;
|
||||
|
||||
pub fn add_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"iso",
|
||||
from_fn_async(add_iso)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"img",
|
||||
from_fn_async(add_img)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"squashfs",
|
||||
from_fn_async(add_squashfs)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn remove_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"iso",
|
||||
from_fn_async(remove_iso)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"img",
|
||||
from_fn_async(remove_img)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"squashfs",
|
||||
from_fn_async(remove_squashfs)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddAssetParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
#[ts(type = "string")]
|
||||
pub url: Url,
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
#[ts(skip)]
|
||||
pub signer: AnyVerifyingKey,
|
||||
pub signature: AnySignature,
|
||||
pub commitment: Blake3Commitment,
|
||||
}
|
||||
|
||||
async fn add_asset(
|
||||
ctx: RegistryContext,
|
||||
AddAssetParams {
|
||||
version,
|
||||
platform,
|
||||
url,
|
||||
signer,
|
||||
signature,
|
||||
commitment,
|
||||
}: AddAssetParams,
|
||||
accessor: impl FnOnce(
|
||||
&mut Model<OsVersionInfo>,
|
||||
) -> &mut Model<BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>>
|
||||
+ UnwindSafe
|
||||
+ Send,
|
||||
) -> Result<(), Error> {
|
||||
signer
|
||||
.scheme()
|
||||
.verify_commitment(&signer, &commitment, SIG_CONTEXT, &signature)?;
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let signer_guid = db.as_index().as_signers().get_signer(&signer)?;
|
||||
if db
|
||||
.as_index()
|
||||
.as_os()
|
||||
.as_versions()
|
||||
.as_idx(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.contains(&signer_guid)
|
||||
|| db.as_admins().de()?.contains(&signer_guid)
|
||||
{
|
||||
accessor(
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.as_idx_mut(&version)
|
||||
.or_not_found(&version)?,
|
||||
)
|
||||
.upsert(&platform, || {
|
||||
Ok(RegistryAsset {
|
||||
published_at: Utc::now(),
|
||||
url,
|
||||
commitment: commitment.clone(),
|
||||
signatures: HashMap::new(),
|
||||
})
|
||||
})?
|
||||
.mutate(|s| {
|
||||
if s.commitment != commitment {
|
||||
Err(Error::new(
|
||||
eyre!("commitment does not match"),
|
||||
ErrorKind::InvalidSignature,
|
||||
))
|
||||
} else {
|
||||
s.signatures.insert(signer, signature);
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_iso(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> {
|
||||
add_asset(ctx, params, |m| m.as_iso_mut()).await
|
||||
}
|
||||
|
||||
pub async fn add_img(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> {
|
||||
add_asset(ctx, params, |m| m.as_img_mut()).await
|
||||
}
|
||||
|
||||
pub async fn add_squashfs(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> {
|
||||
add_asset(ctx, params, |m| m.as_squashfs_mut()).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliAddAssetParams {
|
||||
#[arg(short = 'p', long = "platform")]
|
||||
pub platform: InternedString,
|
||||
#[arg(short = 'v', long = "version")]
|
||||
pub version: Version,
|
||||
pub file: PathBuf,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
pub async fn cli_add_asset(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliAddAssetParams {
|
||||
platform,
|
||||
version,
|
||||
file: path,
|
||||
url,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddAssetParams>,
|
||||
) -> Result<(), Error> {
|
||||
let ext = match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("iso") => "iso",
|
||||
Some("img") => "img",
|
||||
Some("squashfs") => "squashfs",
|
||||
_ => {
|
||||
return Err(Error::new(
|
||||
eyre!("Unknown extension"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let file = MultiCursorFile::from(open_file(&path).await?);
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10));
|
||||
let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100));
|
||||
let mut index_phase = progress.add_phase(
|
||||
InternedString::intern("Adding File to Registry Index"),
|
||||
Some(1),
|
||||
);
|
||||
|
||||
let progress_task =
|
||||
progress.progress_bar_task(&format!("Adding {} to registry...", path.display()));
|
||||
|
||||
sign_phase.start();
|
||||
let blake3 = file.blake3_mmap().await?;
|
||||
let size = file
|
||||
.size()
|
||||
.await
|
||||
.ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?;
|
||||
let commitment = Blake3Commitment {
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
};
|
||||
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
|
||||
ctx.developer_key()?,
|
||||
&commitment,
|
||||
SIG_CONTEXT,
|
||||
)?);
|
||||
sign_phase.complete();
|
||||
|
||||
verify_phase.start();
|
||||
let src = HttpSource::new(ctx.client.clone(), url.clone()).await?;
|
||||
if let Some(size) = src.size().await {
|
||||
verify_phase.set_total(size);
|
||||
}
|
||||
verify_phase.set_units(Some(ProgressUnits::Bytes));
|
||||
let mut writer = verify_phase.writer(VerifyingWriter::new(
|
||||
tokio::io::sink(),
|
||||
Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)),
|
||||
));
|
||||
src.copy_all_to(&mut writer).await?;
|
||||
let (verifier, mut verify_phase) = writer.into_inner();
|
||||
verifier.verify().await?;
|
||||
verify_phase.complete();
|
||||
|
||||
index_phase.start();
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method
|
||||
.into_iter()
|
||||
.chain(method)
|
||||
.chain([ext])
|
||||
.join("."),
|
||||
imbl_value::json!({
|
||||
"platform": platform,
|
||||
"version": version,
|
||||
"url": &url,
|
||||
"signature": signature,
|
||||
"commitment": commitment,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
index_phase.complete();
|
||||
|
||||
progress.complete();
|
||||
|
||||
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemoveAssetParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
#[ts(skip)]
|
||||
pub signer: AnyVerifyingKey,
|
||||
}
|
||||
|
||||
async fn remove_asset(
|
||||
ctx: RegistryContext,
|
||||
RemoveAssetParams {
|
||||
version,
|
||||
platform,
|
||||
signer,
|
||||
}: RemoveAssetParams,
|
||||
accessor: impl FnOnce(
|
||||
&mut Model<OsVersionInfo>,
|
||||
) -> &mut Model<BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>>
|
||||
+ UnwindSafe
|
||||
+ Send,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let signer_guid = db.as_index().as_signers().get_signer(&signer)?;
|
||||
if db
|
||||
.as_index()
|
||||
.as_os()
|
||||
.as_versions()
|
||||
.as_idx(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.contains(&signer_guid)
|
||||
|| db.as_admins().de()?.contains(&signer_guid)
|
||||
{
|
||||
accessor(
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.as_idx_mut(&version)
|
||||
.or_not_found(&version)?,
|
||||
)
|
||||
.remove(&platform)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_iso(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
|
||||
remove_asset(ctx, params, |m| m.as_iso_mut()).await
|
||||
}
|
||||
|
||||
pub async fn remove_img(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
|
||||
remove_asset(ctx, params, |m| m.as_img_mut()).await
|
||||
}
|
||||
|
||||
pub async fn remove_squashfs(ctx: RegistryContext, params: RemoveAssetParams) -> Result<(), Error> {
|
||||
remove_asset(ctx, params, |m| m.as_squashfs_mut()).await
|
||||
}
|
||||
213
core/src/registry/os/asset/get.rs
Normal file
213
core/src/registry/os/asset/get.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::panic::UnwindSafe;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use imbl_value::{InternedString, json};
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, ProgressUnits};
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::os::SIG_CONTEXT;
|
||||
use crate::registry::os::index::OsVersionInfo;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::sign::commitment::Commitment;
|
||||
use crate::sign::commitment::blake3::Blake3Commitment;
|
||||
use crate::util::io::{AtomicFile, open_file};
|
||||
|
||||
pub fn get_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"iso",
|
||||
from_fn_async(get_iso)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"iso",
|
||||
from_fn_async(cli_get_os_asset)
|
||||
.no_display()
|
||||
.with_about("Download iso"),
|
||||
)
|
||||
.subcommand(
|
||||
"img",
|
||||
from_fn_async(get_img)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"img",
|
||||
from_fn_async(cli_get_os_asset)
|
||||
.no_display()
|
||||
.with_about("Download img"),
|
||||
)
|
||||
.subcommand(
|
||||
"squashfs",
|
||||
from_fn_async(get_squashfs)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"squashfs",
|
||||
from_fn_async(cli_get_os_asset)
|
||||
.no_display()
|
||||
.with_about("Download squashfs"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetOsAssetParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
#[ts(type = "string")]
|
||||
pub platform: InternedString,
|
||||
}
|
||||
|
||||
async fn get_os_asset(
|
||||
ctx: RegistryContext,
|
||||
GetOsAssetParams { version, platform }: GetOsAssetParams,
|
||||
accessor: impl FnOnce(
|
||||
&Model<OsVersionInfo>,
|
||||
) -> &Model<BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>>
|
||||
+ UnwindSafe
|
||||
+ Send,
|
||||
) -> Result<RegistryAsset<Blake3Commitment>, Error> {
|
||||
accessor(
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.as_index()
|
||||
.as_os()
|
||||
.as_versions()
|
||||
.as_idx(&version)
|
||||
.or_not_found(&version)?,
|
||||
)
|
||||
.as_idx(&platform)
|
||||
.or_not_found(&platform)?
|
||||
.de()
|
||||
}
|
||||
|
||||
pub async fn get_iso(
|
||||
ctx: RegistryContext,
|
||||
params: GetOsAssetParams,
|
||||
) -> Result<RegistryAsset<Blake3Commitment>, Error> {
|
||||
get_os_asset(ctx, params, |info| info.as_iso()).await
|
||||
}
|
||||
|
||||
pub async fn get_img(
|
||||
ctx: RegistryContext,
|
||||
params: GetOsAssetParams,
|
||||
) -> Result<RegistryAsset<Blake3Commitment>, Error> {
|
||||
get_os_asset(ctx, params, |info| info.as_img()).await
|
||||
}
|
||||
|
||||
pub async fn get_squashfs(
|
||||
ctx: RegistryContext,
|
||||
params: GetOsAssetParams,
|
||||
) -> Result<RegistryAsset<Blake3Commitment>, Error> {
|
||||
get_os_asset(ctx, params, |info| info.as_squashfs()).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliGetOsAssetParams {
|
||||
pub version: Version,
|
||||
pub platform: InternedString,
|
||||
#[arg(
|
||||
long = "download",
|
||||
short = 'd',
|
||||
help = "The path of the directory to download to"
|
||||
)]
|
||||
pub download: Option<PathBuf>,
|
||||
#[arg(
|
||||
long = "reverify",
|
||||
short = 'r',
|
||||
help = "verify the hash of the file a second time after download"
|
||||
)]
|
||||
pub reverify: bool,
|
||||
}
|
||||
|
||||
async fn cli_get_os_asset(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliGetOsAssetParams {
|
||||
version,
|
||||
platform,
|
||||
download,
|
||||
reverify,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliGetOsAssetParams>,
|
||||
) -> Result<RegistryAsset<Blake3Commitment>, Error> {
|
||||
let ext = method
|
||||
.iter()
|
||||
.last()
|
||||
.or_else(|| parent_method.iter().last())
|
||||
.unwrap_or(&"bin");
|
||||
|
||||
let res = from_value::<RegistryAsset<Blake3Commitment>>(
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.iter().chain(&method).join("."),
|
||||
json!({
|
||||
"version": version,
|
||||
"platform": platform,
|
||||
}),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
|
||||
res.validate(SIG_CONTEXT, res.all_signers())?;
|
||||
|
||||
if let Some(download) = download {
|
||||
let download = download.join(format!("startos-{version}_{platform}.{ext}"));
|
||||
let mut file = AtomicFile::new(&download, None::<&Path>).await?;
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut download_phase =
|
||||
progress.add_phase(InternedString::intern("Downloading File"), Some(100));
|
||||
download_phase.set_total(res.commitment.size);
|
||||
download_phase.set_units(Some(ProgressUnits::Bytes));
|
||||
let reverify_phase = if reverify {
|
||||
Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let progress_task = progress.progress_bar_task("Downloading...");
|
||||
|
||||
download_phase.start();
|
||||
let mut download_writer = download_phase.writer(&mut *file);
|
||||
res.download(ctx.client.clone(), &mut download_writer)
|
||||
.await?;
|
||||
let (_, mut download_phase) = download_writer.into_inner();
|
||||
file.save().await?;
|
||||
download_phase.complete();
|
||||
|
||||
if let Some(mut reverify_phase) = reverify_phase {
|
||||
reverify_phase.start();
|
||||
res.commitment
|
||||
.check(&MultiCursorFile::from(open_file(download).await?))
|
||||
.await?;
|
||||
reverify_phase.complete();
|
||||
}
|
||||
|
||||
progress.complete();
|
||||
|
||||
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
28
core/src/registry/os/asset/mod.rs
Normal file
28
core/src/registry/os/asset/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
|
||||
pub mod add;
|
||||
pub mod get;
|
||||
pub mod sign;
|
||||
|
||||
pub fn asset_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("add", add::add_api::<C>())
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add::cli_add_asset)
|
||||
.no_display()
|
||||
.with_about("Add asset to registry"),
|
||||
)
|
||||
.subcommand("remove", add::remove_api::<C>())
|
||||
.subcommand("sign", sign::sign_api::<C>())
|
||||
.subcommand(
|
||||
"sign",
|
||||
from_fn_async(sign::cli_sign_asset)
|
||||
.no_display()
|
||||
.with_about("Sign file and add to registry index"),
|
||||
)
|
||||
.subcommand(
|
||||
"get",
|
||||
get::get_api::<C>().with_about("Commands to download image, iso, or squashfs files"),
|
||||
)
|
||||
}
|
||||
222
core/src/registry/os/asset/sign.rs
Normal file
222
core/src/registry/os/asset/sign.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::panic::UnwindSafe;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, HandlerArgs, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::FullProgressTracker;
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::os::SIG_CONTEXT;
|
||||
use crate::registry::os::index::OsVersionInfo;
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::sign::commitment::blake3::Blake3Commitment;
|
||||
use crate::sign::ed25519::Ed25519;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::io::open_file;
|
||||
use crate::util::serde::Base64;
|
||||
|
||||
pub fn sign_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"iso",
|
||||
from_fn_async(sign_iso)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"img",
|
||||
from_fn_async(sign_img)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"squashfs",
|
||||
from_fn_async(sign_squashfs)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct SignAssetParams {
|
||||
#[ts(type = "string")]
|
||||
version: Version,
|
||||
#[ts(type = "string")]
|
||||
platform: InternedString,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
signer: AnyVerifyingKey,
|
||||
signature: AnySignature,
|
||||
}
|
||||
|
||||
async fn sign_asset(
|
||||
ctx: RegistryContext,
|
||||
SignAssetParams {
|
||||
version,
|
||||
platform,
|
||||
signer,
|
||||
signature,
|
||||
}: SignAssetParams,
|
||||
accessor: impl FnOnce(
|
||||
&mut Model<OsVersionInfo>,
|
||||
) -> &mut Model<BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>>
|
||||
+ UnwindSafe
|
||||
+ Send,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let guid = db.as_index().as_signers().get_signer(&signer)?;
|
||||
if !db
|
||||
.as_index()
|
||||
.as_os()
|
||||
.as_versions()
|
||||
.as_idx(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.contains(&guid)
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("signer {guid} is not authorized"),
|
||||
ErrorKind::Authorization,
|
||||
));
|
||||
}
|
||||
|
||||
accessor(
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.as_idx_mut(&version)
|
||||
.or_not_found(&version)?,
|
||||
)
|
||||
.as_idx_mut(&platform)
|
||||
.or_not_found(&platform)?
|
||||
.mutate(|s| {
|
||||
signer.scheme().verify_commitment(
|
||||
&signer,
|
||||
&s.commitment,
|
||||
SIG_CONTEXT,
|
||||
&signature,
|
||||
)?;
|
||||
s.signatures.insert(signer, signature);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
pub async fn sign_iso(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
|
||||
sign_asset(ctx, params, |m| m.as_iso_mut()).await
|
||||
}
|
||||
|
||||
pub async fn sign_img(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
|
||||
sign_asset(ctx, params, |m| m.as_img_mut()).await
|
||||
}
|
||||
|
||||
pub async fn sign_squashfs(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> {
|
||||
sign_asset(ctx, params, |m| m.as_squashfs_mut()).await
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliSignAssetParams {
|
||||
#[arg(short = 'p', long = "platform")]
|
||||
pub platform: InternedString,
|
||||
#[arg(short = 'v', long = "version")]
|
||||
pub version: Version,
|
||||
pub file: PathBuf,
|
||||
}
|
||||
|
||||
pub async fn cli_sign_asset(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliSignAssetParams {
|
||||
platform,
|
||||
version,
|
||||
file: path,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliSignAssetParams>,
|
||||
) -> Result<(), Error> {
|
||||
let ext = match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("iso") => "iso",
|
||||
Some("img") => "img",
|
||||
Some("squashfs") => "squashfs",
|
||||
_ => {
|
||||
return Err(Error::new(
|
||||
eyre!("Unknown extension"),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let file = MultiCursorFile::from(open_file(&path).await?);
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10));
|
||||
let mut index_phase = progress.add_phase(
|
||||
InternedString::intern("Adding Signature to Registry Index"),
|
||||
Some(1),
|
||||
);
|
||||
|
||||
let progress_task =
|
||||
progress.progress_bar_task(&format!("Adding {} to registry...", path.display()));
|
||||
|
||||
sign_phase.start();
|
||||
let blake3 = file.blake3_mmap().await?;
|
||||
let size = file
|
||||
.size()
|
||||
.await
|
||||
.ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?;
|
||||
let commitment = Blake3Commitment {
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
};
|
||||
let signature = AnySignature::Ed25519(Ed25519.sign_commitment(
|
||||
ctx.developer_key()?,
|
||||
&commitment,
|
||||
SIG_CONTEXT,
|
||||
)?);
|
||||
sign_phase.complete();
|
||||
|
||||
index_phase.start();
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method
|
||||
.into_iter()
|
||||
.chain(method)
|
||||
.chain([ext])
|
||||
.join("."),
|
||||
imbl_value::json!({
|
||||
"platform": platform,
|
||||
"version": version,
|
||||
"signature": signature,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
index_phase.complete();
|
||||
|
||||
progress.complete();
|
||||
|
||||
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
54
core/src/registry/os/index.rs
Normal file
54
core/src/registry/os/index.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use exver::{Version, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::sign::commitment::blake3::Blake3Commitment;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct OsIndex {
|
||||
pub versions: OsVersionInfoMap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, TS)]
|
||||
pub struct OsVersionInfoMap(
|
||||
#[ts(as = "BTreeMap::<String, OsVersionInfo>")] pub BTreeMap<Version, OsVersionInfo>,
|
||||
);
|
||||
impl Map for OsVersionInfoMap {
|
||||
type Key = Version;
|
||||
type Value = OsVersionInfo;
|
||||
fn key_str(key: &Self::Key) -> Result<impl AsRef<str>, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
fn key_string(key: &Self::Key) -> Result<InternedString, Error> {
|
||||
Ok(InternedString::from_display(key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct OsVersionInfo {
|
||||
pub headline: String,
|
||||
pub release_notes: String,
|
||||
#[ts(type = "string")]
|
||||
pub source_version: VersionRange,
|
||||
pub authorized: BTreeSet<Guid>,
|
||||
pub iso: BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>, // platform (i.e. x86_64-nonfree) -> asset
|
||||
pub squashfs: BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>, // platform (i.e. x86_64-nonfree) -> asset
|
||||
pub img: BTreeMap<InternedString, RegistryAsset<Blake3Commitment>>, // platform (i.e. raspberrypi) -> asset
|
||||
}
|
||||
|
||||
pub async fn get_os_index(ctx: RegistryContext) -> Result<OsIndex, Error> {
|
||||
ctx.db.peek().await.into_index().into_os().de()
|
||||
}
|
||||
32
core/src/registry/os/mod.rs
Normal file
32
core/src/registry/os/mod.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub const SIG_CONTEXT: &str = "startos";
|
||||
|
||||
pub mod asset;
|
||||
pub mod index;
|
||||
pub mod version;
|
||||
|
||||
pub fn os_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"index",
|
||||
from_fn_async(index::get_os_index)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_about("List index of OS versions")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"asset",
|
||||
asset::asset_api::<C>().with_about("Commands to add, sign, or get registry assets"),
|
||||
)
|
||||
.subcommand(
|
||||
"version",
|
||||
version::version_api::<C>()
|
||||
.with_about("Commands to add, remove, or list versions or version signers"),
|
||||
)
|
||||
}
|
||||
257
core/src/registry/os/version/mod.rs
Normal file
257
core/src/registry/os/version/mod.rs
Normal file
@@ -0,0 +1,257 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
|
||||
use clap::Parser;
|
||||
use exver::{Version, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::device_info::DeviceInfo;
|
||||
use crate::registry::os::index::OsVersionInfo;
|
||||
use crate::sign::AnyVerifyingKey;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
|
||||
pub mod signer;
|
||||
|
||||
pub fn version_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_version)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add OS version")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_version)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove OS version")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"signer",
|
||||
signer::signer_api::<C>().with_about("Add, remove, and list version signers"),
|
||||
)
|
||||
.subcommand(
|
||||
"get",
|
||||
from_fn_async(get_version)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_metadata("get_device_info", Value::Bool(true))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| {
|
||||
display_version_info(handle.params, result)
|
||||
})
|
||||
.with_about("Get OS versions and related version info")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddVersionParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
pub headline: String,
|
||||
pub release_notes: String,
|
||||
#[ts(type = "string")]
|
||||
pub source_version: VersionRange,
|
||||
#[arg(skip)]
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub signer: Option<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
pub async fn add_version(
|
||||
ctx: RegistryContext,
|
||||
AddVersionParams {
|
||||
version,
|
||||
headline,
|
||||
release_notes,
|
||||
source_version,
|
||||
signer,
|
||||
}: AddVersionParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
let signer = signer
|
||||
.map(|s| db.as_index().as_signers().get_signer(&s))
|
||||
.transpose()?;
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.upsert(&version, || Ok(OsVersionInfo::default()))?
|
||||
.mutate(|i| {
|
||||
i.headline = headline;
|
||||
i.release_notes = release_notes;
|
||||
i.source_version = source_version;
|
||||
i.authorized.extend(signer);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemoveVersionParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
pub async fn remove_version(
|
||||
ctx: RegistryContext,
|
||||
RemoveVersionParams { version }: RemoveVersionParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.remove(&version)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetOsVersionParams {
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long = "src")]
|
||||
pub source_version: Option<Version>,
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long)]
|
||||
pub target_version: Option<VersionRange>,
|
||||
#[arg(long = "id")]
|
||||
server_id: Option<String>,
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long)]
|
||||
platform: Option<InternedString>,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__DeviceInfo_device_info")]
|
||||
pub device_info: Option<DeviceInfo>,
|
||||
}
|
||||
|
||||
struct PgDateTime(DateTime<Utc>);
|
||||
impl sqlx::Type<sqlx::Postgres> for PgDateTime {
|
||||
fn type_info() -> <sqlx::Postgres as sqlx::Database>::TypeInfo {
|
||||
sqlx::postgres::PgTypeInfo::with_oid(sqlx::postgres::types::Oid(1184))
|
||||
}
|
||||
}
|
||||
impl sqlx::Encode<'_, sqlx::Postgres> for PgDateTime {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
buf: &mut <sqlx::Postgres as sqlx::Database>::ArgumentBuffer<'_>,
|
||||
) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
||||
fn postgres_epoch_datetime() -> NaiveDateTime {
|
||||
NaiveDate::from_ymd_opt(2000, 1, 1)
|
||||
.expect("expected 2000-01-01 to be a valid NaiveDate")
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.expect("expected 2000-01-01T00:00:00 to be a valid NaiveDateTime")
|
||||
}
|
||||
let micros = (self.0.naive_utc() - postgres_epoch_datetime())
|
||||
.num_microseconds()
|
||||
.ok_or_else(|| format!("NaiveDateTime out of range for Postgres: {:?}", self.0))?;
|
||||
micros.encode(buf)
|
||||
}
|
||||
fn size_hint(&self) -> usize {
|
||||
std::mem::size_of::<i64>()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_version(
|
||||
ctx: RegistryContext,
|
||||
GetOsVersionParams {
|
||||
source_version: source,
|
||||
target_version: target,
|
||||
server_id,
|
||||
platform,
|
||||
device_info,
|
||||
}: GetOsVersionParams,
|
||||
) -> Result<BTreeMap<Version, OsVersionInfo>, Error> {
|
||||
let source = source.or_else(|| device_info.as_ref().map(|d| d.os.version.clone()));
|
||||
let platform = platform.or_else(|| device_info.as_ref().map(|d| d.os.platform.clone()));
|
||||
if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, &platform) {
|
||||
let created_at = Utc::now();
|
||||
|
||||
sqlx::query("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)")
|
||||
.bind(PgDateTime(created_at))
|
||||
.bind(server_id)
|
||||
.bind(&**arch)
|
||||
.execute(pool)
|
||||
.await
|
||||
.with_kind(ErrorKind::Database)?;
|
||||
}
|
||||
let target = target.unwrap_or(VersionRange::Any);
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.into_index()
|
||||
.into_os()
|
||||
.into_versions()
|
||||
.into_entries()?
|
||||
.into_iter()
|
||||
.map(|(v, i)| i.de().map(|i| (v, i)))
|
||||
.filter_ok(|(version, info)| {
|
||||
platform
|
||||
.as_ref()
|
||||
.map_or(true, |p| info.squashfs.contains_key(p))
|
||||
&& version.satisfies(&target)
|
||||
&& source
|
||||
.as_ref()
|
||||
.map_or(true, |s| s.satisfies(&info.source_version))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn display_version_info<T>(
|
||||
params: WithIoFormat<T>,
|
||||
info: BTreeMap<Version, OsVersionInfo>,
|
||||
) -> Result<(), Error> {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, info);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"VERSION",
|
||||
"HEADLINE",
|
||||
"RELEASE NOTES",
|
||||
"ISO PLATFORMS",
|
||||
"IMG PLATFORMS",
|
||||
"SQUASHFS PLATFORMS",
|
||||
]);
|
||||
for (version, info) in &info {
|
||||
table.add_row(row![
|
||||
&version.to_string(),
|
||||
&info.headline,
|
||||
&info.release_notes,
|
||||
&info.iso.keys().into_iter().join(", "),
|
||||
&info.img.keys().into_iter().join(", "),
|
||||
&info.squashfs.keys().into_iter().join(", "),
|
||||
]);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
}
|
||||
138
core/src/registry/os/version/signer.rs
Normal file
138
core/src/registry/os/version/signer.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::Version;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::admin::display_signers;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub fn signer_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_version_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add version signer")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_version_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove version signer")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_version_signers)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| display_signers(handle.params, result))
|
||||
.with_about("List version signers and related signer info")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct VersionSignerParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
pub signer: Guid,
|
||||
}
|
||||
|
||||
pub async fn add_version_signer(
|
||||
ctx: RegistryContext,
|
||||
VersionSignerParams { version, signer }: VersionSignerParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
ensure_code!(
|
||||
db.as_index().as_signers().contains_key(&signer)?,
|
||||
ErrorKind::InvalidRequest,
|
||||
"unknown signer {signer}"
|
||||
);
|
||||
|
||||
db.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.as_idx_mut(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized_mut()
|
||||
.mutate(|s| Ok(s.insert(signer)))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
pub async fn remove_version_signer(
|
||||
ctx: RegistryContext,
|
||||
VersionSignerParams { version, signer }: VersionSignerParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if !db
|
||||
.as_index_mut()
|
||||
.as_os_mut()
|
||||
.as_versions_mut()
|
||||
.as_idx_mut(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized_mut()
|
||||
.mutate(|s| Ok(s.remove(&signer)))?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("signer {signer} is not authorized to sign for v{version}"),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ListVersionSignersParams {
|
||||
#[ts(type = "string")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
pub async fn list_version_signers(
|
||||
ctx: RegistryContext,
|
||||
ListVersionSignersParams { version }: ListVersionSignersParams,
|
||||
) -> Result<BTreeMap<Guid, SignerInfo>, Error> {
|
||||
let db = ctx.db.peek().await;
|
||||
db.as_index()
|
||||
.as_os()
|
||||
.as_versions()
|
||||
.as_idx(&version)
|
||||
.or_not_found(&version)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.into_iter()
|
||||
.filter_map(|guid| {
|
||||
db.as_index()
|
||||
.as_signers()
|
||||
.as_idx(&guid)
|
||||
.map(|s| s.de().map(|s| (guid, s)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
213
core/src/registry/package/add.rs
Normal file
213
core/src/registry/package/add.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use rpc_toolkit::HandlerArgs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, ProgressTrackerWriter, ProgressUnits};
|
||||
use crate::registry::asset::BufferedHttpSource;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::package::index::PackageVersionInfo;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::s9pk::merkle_archive::source::http::HttpSource;
|
||||
use crate::s9pk::v2::SIG_CONTEXT;
|
||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||
use crate::sign::ed25519::Ed25519;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::VersionString;
|
||||
use crate::util::io::TrackingIO;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddPackageParams {
|
||||
#[ts(type = "string")]
|
||||
pub url: Url,
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub uploader: AnyVerifyingKey,
|
||||
pub commitment: MerkleArchiveCommitment,
|
||||
pub signature: AnySignature,
|
||||
}
|
||||
|
||||
pub async fn add_package(
|
||||
ctx: RegistryContext,
|
||||
AddPackageParams {
|
||||
url,
|
||||
uploader,
|
||||
commitment,
|
||||
signature,
|
||||
}: AddPackageParams,
|
||||
) -> Result<(), Error> {
|
||||
uploader
|
||||
.scheme()
|
||||
.verify_commitment(&uploader, &commitment, SIG_CONTEXT, &signature)?;
|
||||
let peek = ctx.db.peek().await;
|
||||
let uploader_guid = peek.as_index().as_signers().get_signer(&uploader)?;
|
||||
let s9pk = S9pk::deserialize(
|
||||
&Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?),
|
||||
Some(&commitment),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let manifest = s9pk.as_manifest();
|
||||
|
||||
let mut info = PackageVersionInfo::from_s9pk(&s9pk, url).await?;
|
||||
if !info.s9pk.signatures.contains_key(&uploader) {
|
||||
info.s9pk.signatures.insert(uploader.clone(), signature);
|
||||
}
|
||||
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if db.as_admins().de()?.contains(&uploader_guid)
|
||||
|| db
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&manifest.id)
|
||||
.or_not_found(&manifest.id)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.get(&uploader_guid)
|
||||
.map_or(false, |v| manifest.version.satisfies(v))
|
||||
{
|
||||
let package = db
|
||||
.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.upsert(&manifest.id, || Ok(Default::default()))?;
|
||||
package.as_versions_mut().insert(&manifest.version, &info)?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliAddPackageParams {
|
||||
pub file: PathBuf,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
pub async fn cli_add_package(
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params: CliAddPackageParams { file, url },
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddPackageParams>,
|
||||
) -> Result<(), Error> {
|
||||
let s9pk = S9pk::open(&file, None).await?;
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
||||
let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100));
|
||||
let mut index_phase = progress.add_phase(
|
||||
InternedString::intern("Adding File to Registry Index"),
|
||||
Some(1),
|
||||
);
|
||||
|
||||
let progress_task =
|
||||
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
||||
|
||||
sign_phase.start();
|
||||
let commitment = s9pk.as_archive().commitment().await?;
|
||||
let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?;
|
||||
sign_phase.complete();
|
||||
|
||||
verify_phase.start();
|
||||
let source = BufferedHttpSource::new(ctx.client.clone(), url.clone(), verify_phase).await?;
|
||||
let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?;
|
||||
src.serialize(&mut TrackingIO::new(0, &mut tokio::io::sink()), true)
|
||||
.await?;
|
||||
|
||||
index_phase.start();
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
imbl_value::json!({
|
||||
"url": &url,
|
||||
"signature": AnySignature::Ed25519(signature),
|
||||
"commitment": commitment,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
index_phase.complete();
|
||||
|
||||
progress.complete();
|
||||
|
||||
progress_task.await.with_kind(ErrorKind::Unknown)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemovePackageParams {
|
||||
pub id: PackageId,
|
||||
pub version: VersionString,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__Auth_signer")]
|
||||
pub signer: Option<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
pub async fn remove_package(
|
||||
ctx: RegistryContext,
|
||||
RemovePackageParams {
|
||||
id,
|
||||
version,
|
||||
signer,
|
||||
}: RemovePackageParams,
|
||||
) -> Result<(), Error> {
|
||||
let peek = ctx.db.peek().await;
|
||||
let signer =
|
||||
signer.ok_or_else(|| Error::new(eyre!("missing signer"), ErrorKind::InvalidRequest))?;
|
||||
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
|
||||
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if db.as_admins().de()?.contains(&signer_guid)
|
||||
|| db
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.or_not_found(&id)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.get(&signer_guid)
|
||||
.map_or(false, |v| version.satisfies(v))
|
||||
{
|
||||
if let Some(package) = db
|
||||
.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.as_idx_mut(&id)
|
||||
{
|
||||
package.as_versions_mut().remove(&version)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
203
core/src/registry/package/category.rs
Normal file
203
core/src/registry/package/category.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::Parser;
|
||||
use imbl_value::InternedString;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::package::index::Category;
|
||||
use crate::util::serde::{HandlerExtSerde, WithIoFormat, display_serializable};
|
||||
|
||||
pub fn category_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_category)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a category to the registry")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_category)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove a category from the registry")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"add-package",
|
||||
from_fn_async(add_package)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add a package to a category")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove-package",
|
||||
from_fn_async(remove_package)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove a package from a category")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_categories)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|params, categories| {
|
||||
display_categories(params.params, categories)
|
||||
})
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddCategoryParams {
|
||||
#[ts(type = "string")]
|
||||
pub id: InternedString,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub async fn add_category(
|
||||
ctx: RegistryContext,
|
||||
AddCategoryParams { id, name }: AddCategoryParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_categories_mut()
|
||||
.insert(&id, &Category { name })
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemoveCategoryParams {
|
||||
#[ts(type = "string")]
|
||||
pub id: InternedString,
|
||||
}
|
||||
|
||||
pub async fn remove_category(
|
||||
ctx: RegistryContext,
|
||||
RemoveCategoryParams { id }: RemoveCategoryParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_categories_mut()
|
||||
.remove(&id)
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddPackageToCategoryParams {
|
||||
#[ts(type = "string")]
|
||||
pub id: InternedString,
|
||||
pub package: PackageId,
|
||||
}
|
||||
|
||||
pub async fn add_package(
|
||||
ctx: RegistryContext,
|
||||
AddPackageToCategoryParams { id, package }: AddPackageToCategoryParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_categories_mut()
|
||||
.mutate(|c| Ok(c.insert(id)))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemovePackageFromCategoryParams {
|
||||
#[ts(type = "string")]
|
||||
pub id: InternedString,
|
||||
pub package: PackageId,
|
||||
}
|
||||
|
||||
pub async fn remove_package(
|
||||
ctx: RegistryContext,
|
||||
RemovePackageFromCategoryParams { id, package }: RemovePackageFromCategoryParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
db.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.as_idx_mut(&package)
|
||||
.or_not_found(&package)?
|
||||
.as_categories_mut()
|
||||
.mutate(|c| Ok(c.remove(&id)))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_categories(
|
||||
ctx: RegistryContext,
|
||||
) -> Result<BTreeMap<InternedString, Category>, Error> {
|
||||
ctx.db
|
||||
.peek()
|
||||
.await
|
||||
.into_index()
|
||||
.into_package()
|
||||
.into_categories()
|
||||
.de()
|
||||
}
|
||||
|
||||
pub fn display_categories<T>(
|
||||
params: WithIoFormat<T>,
|
||||
categories: BTreeMap<InternedString, Category>,
|
||||
) -> Result<(), Error> {
|
||||
use prettytable::*;
|
||||
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, categories);
|
||||
}
|
||||
|
||||
let mut table = Table::new();
|
||||
table.add_row(row![bc =>
|
||||
"ID",
|
||||
"NAME",
|
||||
]);
|
||||
for (id, info) in categories {
|
||||
table.add_row(row![&*id, &info.name]);
|
||||
}
|
||||
table.print_tty(false)?;
|
||||
Ok(())
|
||||
}
|
||||
482
core/src/registry/package/get.rs
Normal file
482
core/src/registry/package/get.rs
Normal file
@@ -0,0 +1,482 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
use exver::{ExtendedVersion, VersionRange};
|
||||
use imbl_value::{InternedString, json};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::progress::{FullProgressTracker, ProgressUnits};
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::device_info::DeviceInfo;
|
||||
use crate::registry::package::index::{PackageIndex, PackageVersionInfo};
|
||||
use crate::s9pk::merkle_archive::source::ArchiveSource;
|
||||
use crate::s9pk::v2::SIG_CONTEXT;
|
||||
use crate::util::VersionString;
|
||||
use crate::util::io::{TrackingIO, to_tmp_path};
|
||||
use crate::util::serde::{WithIoFormat, display_serializable};
|
||||
use crate::util::tui::choose;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum,
|
||||
)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum PackageDetailLevel {
|
||||
None,
|
||||
Short,
|
||||
Full,
|
||||
}
|
||||
impl Default for PackageDetailLevel {
|
||||
fn default() -> Self {
|
||||
Self::Short
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct PackageInfoShort {
|
||||
pub release_notes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[ts(export)]
|
||||
pub struct GetPackageParams {
|
||||
pub id: Option<PackageId>,
|
||||
#[ts(type = "string | null")]
|
||||
#[arg(long, short = 'v')]
|
||||
pub target_version: Option<VersionRange>,
|
||||
#[arg(long)]
|
||||
pub source_version: Option<VersionString>,
|
||||
#[ts(skip)]
|
||||
#[arg(skip)]
|
||||
#[serde(rename = "__DeviceInfo_device_info")]
|
||||
pub device_info: Option<DeviceInfo>,
|
||||
#[serde(default)]
|
||||
#[arg(default_value = "none")]
|
||||
pub other_versions: PackageDetailLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetPackageResponse {
|
||||
#[ts(type = "string[]")]
|
||||
pub categories: BTreeSet<InternedString>,
|
||||
pub best: BTreeMap<VersionString, PackageVersionInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub other_versions: Option<BTreeMap<VersionString, PackageInfoShort>>,
|
||||
}
|
||||
impl GetPackageResponse {
|
||||
pub fn tables(&self) -> Vec<prettytable::Table> {
|
||||
use prettytable::*;
|
||||
|
||||
let mut res = Vec::with_capacity(self.best.len());
|
||||
|
||||
for (version, info) in &self.best {
|
||||
let mut table = info.table(version);
|
||||
|
||||
let lesser_versions: BTreeMap<_, _> = self
|
||||
.other_versions
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|(v, _)| ***v < **version)
|
||||
.collect();
|
||||
|
||||
if !lesser_versions.is_empty() {
|
||||
table.add_row(row![bc => "OLDER VERSIONS"]);
|
||||
table.add_row(row![bc => "VERSION", "RELEASE NOTES"]);
|
||||
for (version, info) in lesser_versions {
|
||||
table.add_row(row![AsRef::<str>::as_ref(version), &info.release_notes]);
|
||||
}
|
||||
}
|
||||
|
||||
res.push(table);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct GetPackageResponseFull {
|
||||
#[ts(type = "string[]")]
|
||||
pub categories: BTreeSet<InternedString>,
|
||||
pub best: BTreeMap<VersionString, PackageVersionInfo>,
|
||||
pub other_versions: BTreeMap<VersionString, PackageVersionInfo>,
|
||||
}
|
||||
impl GetPackageResponseFull {
|
||||
pub fn tables(&self) -> Vec<prettytable::Table> {
|
||||
let mut res = Vec::with_capacity(self.best.len());
|
||||
|
||||
let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect();
|
||||
|
||||
for (version, info) in all {
|
||||
res.push(info.table(version));
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
pub type GetPackagesResponse = BTreeMap<PackageId, GetPackageResponse>;
|
||||
pub type GetPackagesResponseFull = BTreeMap<PackageId, GetPackageResponseFull>;
|
||||
|
||||
fn get_matching_models<'a>(
|
||||
db: &'a Model<PackageIndex>,
|
||||
GetPackageParams {
|
||||
id,
|
||||
source_version,
|
||||
device_info,
|
||||
..
|
||||
}: &GetPackageParams,
|
||||
) -> Result<Vec<(PackageId, ExtendedVersion, &'a Model<PackageVersionInfo>)>, Error> {
|
||||
if let Some(id) = id {
|
||||
if let Some(pkg) = db.as_packages().as_idx(id) {
|
||||
vec![(id.clone(), pkg)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
db.as_packages().as_entries()?
|
||||
}
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
Ok(v.as_versions()
|
||||
.as_entries()?
|
||||
.into_iter()
|
||||
.map(|(v, info)| {
|
||||
Ok::<_, Error>(
|
||||
if source_version.as_ref().map_or(Ok(true), |source_version| {
|
||||
Ok::<_, Error>(
|
||||
source_version.satisfies(
|
||||
&info
|
||||
.as_source_version()
|
||||
.de()?
|
||||
.unwrap_or(VersionRange::any()),
|
||||
),
|
||||
)
|
||||
})? && device_info
|
||||
.as_ref()
|
||||
.map_or(Ok(true), |device_info| info.works_for_device(device_info))?
|
||||
{
|
||||
Some((k.clone(), ExtendedVersion::from(v), info))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
})
|
||||
.flatten_ok())
|
||||
})
|
||||
.flatten_ok()
|
||||
.map(|res| res.and_then(|a| a))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result<Value, Error> {
|
||||
use patch_db::ModelExt;
|
||||
|
||||
let peek = ctx.db.peek().await;
|
||||
let mut best: BTreeMap<PackageId, BTreeMap<VersionString, &Model<PackageVersionInfo>>> =
|
||||
Default::default();
|
||||
let mut other: BTreeMap<PackageId, BTreeMap<VersionString, &Model<PackageVersionInfo>>> =
|
||||
Default::default();
|
||||
for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? {
|
||||
let package_best = best.entry(id.clone()).or_default();
|
||||
let package_other = other.entry(id.clone()).or_default();
|
||||
if params
|
||||
.target_version
|
||||
.as_ref()
|
||||
.map_or(true, |v| version.satisfies(v))
|
||||
&& package_best.keys().all(|k| !(**k > version))
|
||||
{
|
||||
for worse_version in package_best
|
||||
.keys()
|
||||
.filter(|k| ***k < version)
|
||||
.cloned()
|
||||
.collect_vec()
|
||||
{
|
||||
if let Some(info) = package_best.remove(&worse_version) {
|
||||
package_other.insert(worse_version, info);
|
||||
}
|
||||
}
|
||||
package_best.insert(version.into(), info);
|
||||
} else {
|
||||
package_other.insert(version.into(), info);
|
||||
}
|
||||
}
|
||||
if let Some(id) = params.id {
|
||||
let categories = peek
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.map(|p| p.as_categories().de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let best = best
|
||||
.remove(&id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?;
|
||||
let other = other.remove(&id).unwrap_or_default();
|
||||
match params.other_versions {
|
||||
PackageDetailLevel::None => to_value(&GetPackageResponse {
|
||||
categories,
|
||||
best,
|
||||
other_versions: None,
|
||||
}),
|
||||
PackageDetailLevel::Short => to_value(&GetPackageResponse {
|
||||
categories,
|
||||
best,
|
||||
other_versions: Some(
|
||||
other
|
||||
.into_iter()
|
||||
.map(|(k, v)| from_value(v.as_value().clone()).map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
),
|
||||
}),
|
||||
PackageDetailLevel::Full => to_value(&GetPackageResponseFull {
|
||||
categories,
|
||||
best,
|
||||
other_versions: other
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
match params.other_versions {
|
||||
PackageDetailLevel::None => to_value(
|
||||
&best
|
||||
.into_iter()
|
||||
.map(|(id, best)| {
|
||||
let categories = peek
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.map(|p| p.as_categories().de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
Ok::<_, Error>((
|
||||
id,
|
||||
GetPackageResponse {
|
||||
categories,
|
||||
best: best
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
other_versions: None,
|
||||
},
|
||||
))
|
||||
})
|
||||
.try_collect::<_, GetPackagesResponse, _>()?,
|
||||
),
|
||||
PackageDetailLevel::Short => to_value(
|
||||
&best
|
||||
.into_iter()
|
||||
.map(|(id, best)| {
|
||||
let categories = peek
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.map(|p| p.as_categories().de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let other = other.remove(&id).unwrap_or_default();
|
||||
Ok::<_, Error>((
|
||||
id,
|
||||
GetPackageResponse {
|
||||
categories,
|
||||
best: best
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
other_versions: Some(
|
||||
other
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
from_value(v.as_value().clone()).map(|v| (k, v))
|
||||
})
|
||||
.try_collect()?,
|
||||
),
|
||||
},
|
||||
))
|
||||
})
|
||||
.try_collect::<_, GetPackagesResponse, _>()?,
|
||||
),
|
||||
PackageDetailLevel::Full => to_value(
|
||||
&best
|
||||
.into_iter()
|
||||
.map(|(id, best)| {
|
||||
let categories = peek
|
||||
.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.map(|p| p.as_categories().de())
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let other = other.remove(&id).unwrap_or_default();
|
||||
Ok::<_, Error>((
|
||||
id,
|
||||
GetPackageResponseFull {
|
||||
categories,
|
||||
best: best
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
other_versions: other
|
||||
.into_iter()
|
||||
.map(|(k, v)| v.de().map(|v| (k, v)))
|
||||
.try_collect()?,
|
||||
},
|
||||
))
|
||||
})
|
||||
.try_collect::<_, GetPackagesResponseFull, _>()?,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_package_info(
|
||||
params: WithIoFormat<GetPackageParams>,
|
||||
info: Value,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(format) = params.format {
|
||||
return display_serializable(format, info);
|
||||
}
|
||||
|
||||
if let Some(_) = params.rest.id {
|
||||
if params.rest.other_versions == PackageDetailLevel::Full {
|
||||
for table in from_value::<GetPackageResponseFull>(info)?.tables() {
|
||||
table.print_tty(false)?;
|
||||
println!();
|
||||
}
|
||||
} else {
|
||||
for table in from_value::<GetPackageResponse>(info)?.tables() {
|
||||
table.print_tty(false)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if params.rest.other_versions == PackageDetailLevel::Full {
|
||||
for (_, package) in from_value::<GetPackagesResponseFull>(info)? {
|
||||
for table in package.tables() {
|
||||
table.print_tty(false)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (_, package) in from_value::<GetPackagesResponse>(info)? {
|
||||
for table in package.tables() {
|
||||
table.print_tty(false)?;
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, TS, Parser)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CliDownloadParams {
|
||||
pub id: PackageId,
|
||||
#[arg(long, short = 'v')]
|
||||
#[ts(type = "string | null")]
|
||||
pub target_version: Option<VersionRange>,
|
||||
#[arg(short, long)]
|
||||
pub dest: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub async fn cli_download(
|
||||
ctx: CliContext,
|
||||
CliDownloadParams {
|
||||
ref id,
|
||||
target_version,
|
||||
dest,
|
||||
}: CliDownloadParams,
|
||||
) -> Result<(), Error> {
|
||||
let progress_tracker = FullProgressTracker::new();
|
||||
let mut fetching_progress = progress_tracker.add_phase("Fetching".into(), Some(1));
|
||||
let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100));
|
||||
let mut verify_progress = progress_tracker.add_phase("Verifying".into(), Some(10));
|
||||
|
||||
let progress = progress_tracker.progress_bar_task("Downloading S9PK...");
|
||||
|
||||
fetching_progress.start();
|
||||
let mut res: GetPackageResponse = from_value(
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
"package.get",
|
||||
json!({
|
||||
"id": &id,
|
||||
"targetVersion": &target_version,
|
||||
}),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
let PackageVersionInfo { s9pk, .. } = match res.best.len() {
|
||||
0 => {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"Could not find a version of {id} that satisfies {}",
|
||||
target_version.unwrap_or(VersionRange::Any)
|
||||
),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
1 => res.best.pop_first().unwrap().1,
|
||||
_ => {
|
||||
let choices = res.best.keys().cloned().collect::<Vec<_>>();
|
||||
let version = choose(
|
||||
&format!("Multiple flavors of {id} available. Choose a version to download:"),
|
||||
&choices,
|
||||
)
|
||||
.await?;
|
||||
res.best.remove(version).unwrap()
|
||||
}
|
||||
};
|
||||
s9pk.validate(SIG_CONTEXT, s9pk.all_signers())?;
|
||||
fetching_progress.complete();
|
||||
|
||||
let dest = dest.unwrap_or_else(|| Path::new(".").join(id).with_extension("s9pk"));
|
||||
let dest_tmp = to_tmp_path(&dest)?;
|
||||
let (mut parsed, source) = s9pk
|
||||
.download_to(&dest_tmp, ctx.client.clone(), download_progress)
|
||||
.await?;
|
||||
if let Some(size) = source.size().await {
|
||||
verify_progress.set_total(size);
|
||||
}
|
||||
verify_progress.set_units(Some(ProgressUnits::Bytes));
|
||||
let mut progress_sink = verify_progress.writer(tokio::io::sink());
|
||||
parsed
|
||||
.serialize(&mut TrackingIO::new(0, &mut progress_sink), true)
|
||||
.await?;
|
||||
progress_sink.into_inner().1.complete();
|
||||
|
||||
source.wait_for_buffered().await?;
|
||||
tokio::fs::rename(dest_tmp, dest).await?;
|
||||
|
||||
progress_tracker.complete();
|
||||
progress.await.unwrap();
|
||||
|
||||
println!("Download Complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
206
core/src/registry/package/index.rs
Normal file
206
core/src/registry/package/index.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use chrono::Utc;
|
||||
use exver::{Version, VersionRange};
|
||||
use imbl_value::InternedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::asset::RegistryAsset;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::device_info::DeviceInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::s9pk::S9pk;
|
||||
use crate::s9pk::git_hash::GitHash;
|
||||
use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements};
|
||||
use crate::s9pk::merkle_archive::source::FileSource;
|
||||
use crate::sign::commitment::merkle_archive::MerkleArchiveCommitment;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey};
|
||||
use crate::util::{DataUrl, VersionString};
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct PackageIndex {
|
||||
pub categories: BTreeMap<InternedString, Category>,
|
||||
pub packages: BTreeMap<PackageId, PackageInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct PackageInfo {
|
||||
#[ts(as = "BTreeMap::<Guid, String>")]
|
||||
pub authorized: BTreeMap<Guid, VersionRange>,
|
||||
pub versions: BTreeMap<VersionString, PackageVersionInfo>,
|
||||
#[ts(type = "string[]")]
|
||||
pub categories: BTreeSet<InternedString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct Category {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct DependencyMetadata {
|
||||
#[ts(type = "string | null")]
|
||||
pub title: Option<InternedString>,
|
||||
pub icon: Option<DataUrl<'static>>,
|
||||
pub description: Option<String>,
|
||||
pub optional: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct PackageVersionInfo {
|
||||
#[ts(type = "string")]
|
||||
pub title: InternedString,
|
||||
pub icon: DataUrl<'static>,
|
||||
pub description: Description,
|
||||
pub release_notes: String,
|
||||
pub git_hash: Option<GitHash>,
|
||||
#[ts(type = "string")]
|
||||
pub license: InternedString,
|
||||
#[ts(type = "string")]
|
||||
pub wrapper_repo: Url,
|
||||
#[ts(type = "string")]
|
||||
pub upstream_repo: Url,
|
||||
#[ts(type = "string")]
|
||||
pub support_site: Url,
|
||||
#[ts(type = "string")]
|
||||
pub marketing_site: Url,
|
||||
#[ts(type = "string | null")]
|
||||
pub donation_url: Option<Url>,
|
||||
#[ts(type = "string | null")]
|
||||
pub docs_url: Option<Url>,
|
||||
pub alerts: Alerts,
|
||||
pub dependency_metadata: BTreeMap<PackageId, DependencyMetadata>,
|
||||
#[ts(type = "string")]
|
||||
pub os_version: Version,
|
||||
#[ts(type = "string | null")]
|
||||
pub sdk_version: Option<Version>,
|
||||
pub hardware_requirements: HardwareRequirements,
|
||||
#[ts(type = "string | null")]
|
||||
pub source_version: Option<VersionRange>,
|
||||
pub s9pk: RegistryAsset<MerkleArchiveCommitment>,
|
||||
}
|
||||
impl PackageVersionInfo {
|
||||
pub async fn from_s9pk<S: FileSource + Clone>(s9pk: &S9pk<S>, url: Url) -> Result<Self, Error> {
|
||||
let manifest = s9pk.as_manifest();
|
||||
let mut dependency_metadata = BTreeMap::new();
|
||||
for (id, info) in &manifest.dependencies.0 {
|
||||
let metadata = s9pk.dependency_metadata(id).await?;
|
||||
dependency_metadata.insert(
|
||||
id.clone(),
|
||||
DependencyMetadata {
|
||||
title: metadata.map(|m| m.title),
|
||||
icon: s9pk.dependency_icon_data_url(id).await?,
|
||||
description: info.description.clone(),
|
||||
optional: info.optional,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(Self {
|
||||
title: manifest.title.clone(),
|
||||
icon: s9pk.icon_data_url().await?,
|
||||
description: manifest.description.clone(),
|
||||
release_notes: manifest.release_notes.clone(),
|
||||
git_hash: manifest.git_hash.clone(),
|
||||
license: manifest.license.clone(),
|
||||
wrapper_repo: manifest.wrapper_repo.clone(),
|
||||
upstream_repo: manifest.upstream_repo.clone(),
|
||||
support_site: manifest.support_site.clone(),
|
||||
marketing_site: manifest.marketing_site.clone(),
|
||||
donation_url: manifest.donation_url.clone(),
|
||||
docs_url: manifest.docs_url.clone(),
|
||||
alerts: manifest.alerts.clone(),
|
||||
dependency_metadata,
|
||||
os_version: manifest.os_version.clone(),
|
||||
sdk_version: manifest.sdk_version.clone(),
|
||||
hardware_requirements: manifest.hardware_requirements.clone(),
|
||||
source_version: None, // TODO
|
||||
s9pk: RegistryAsset {
|
||||
published_at: Utc::now(),
|
||||
url,
|
||||
commitment: s9pk.as_archive().commitment().await?,
|
||||
signatures: [(
|
||||
AnyVerifyingKey::Ed25519(s9pk.as_archive().signer()),
|
||||
AnySignature::Ed25519(s9pk.as_archive().signature().await?),
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
pub fn table(&self, version: &VersionString) -> prettytable::Table {
|
||||
use prettytable::*;
|
||||
|
||||
let mut table = Table::new();
|
||||
|
||||
table.add_row(row![bc => &self.title]);
|
||||
table.add_row(row![br -> "VERSION", AsRef::<str>::as_ref(version)]);
|
||||
table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]);
|
||||
table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]);
|
||||
table.add_row(row![
|
||||
br -> "DESCRIPTION",
|
||||
&textwrap::wrap(&self.description.long, 80).join("\n")
|
||||
]);
|
||||
table.add_row(row![br -> "GIT HASH", self.git_hash.as_deref().unwrap_or("N/A")]);
|
||||
table.add_row(row![br -> "LICENSE", &self.license]);
|
||||
table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]);
|
||||
table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]);
|
||||
table.add_row(row![br -> "WEBSITE", &self.marketing_site.to_string()]);
|
||||
table.add_row(row![br -> "SUPPORT", &self.support_site.to_string()]);
|
||||
|
||||
table
|
||||
}
|
||||
}
|
||||
impl Model<PackageVersionInfo> {
|
||||
pub fn works_for_device(&self, device_info: &DeviceInfo) -> Result<bool, Error> {
|
||||
if !self.as_os_version().de()?.satisfies(&device_info.os.compat) {
|
||||
return Ok(false);
|
||||
}
|
||||
let hw = self.as_hardware_requirements().de()?;
|
||||
if let Some(arch) = hw.arch {
|
||||
if !arch.contains(&device_info.hardware.arch) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
if let Some(ram) = hw.ram {
|
||||
if device_info.hardware.ram < ram {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
for device_filter in hw.device {
|
||||
if !device_info
|
||||
.hardware
|
||||
.devices
|
||||
.iter()
|
||||
.filter(|d| d.class() == &*device_filter.class)
|
||||
.any(|d| device_filter.pattern.as_ref().is_match(d.product()))
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_package_index(ctx: RegistryContext) -> Result<PackageIndex, Error> {
|
||||
ctx.db.peek().await.into_index().into_package().de()
|
||||
}
|
||||
70
core/src/registry/package/mod.rs
Normal file
70
core/src/registry/package/mod.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async, from_fn_async_local};
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub mod add;
|
||||
pub mod category;
|
||||
pub mod get;
|
||||
pub mod index;
|
||||
pub mod signer;
|
||||
|
||||
pub fn package_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"index",
|
||||
from_fn_async(index::get_package_index)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_about("List packages and categories")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add::add_package)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add::cli_add_package)
|
||||
.no_display()
|
||||
.with_about("Add package to registry index"),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(add::remove_package)
|
||||
.with_metadata("get_signer", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove package from registry index")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"signer",
|
||||
signer::signer_api::<C>().with_about("Add, remove, and list package signers"),
|
||||
)
|
||||
.subcommand(
|
||||
"get",
|
||||
from_fn_async(get::get_package)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_metadata("get_device_info", Value::Bool(true))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| {
|
||||
get::display_package_info(handle.params, result)
|
||||
})
|
||||
.with_about("List installation candidate package(s)")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"download",
|
||||
from_fn_async_local(get::cli_download)
|
||||
.no_display()
|
||||
.with_about("Download an s9pk"),
|
||||
)
|
||||
.subcommand(
|
||||
"category",
|
||||
category::category_api::<C>()
|
||||
.with_about("Update the categories for packages on the registry"),
|
||||
)
|
||||
}
|
||||
156
core/src/registry/package/signer.rs
Normal file
156
core/src/registry/package/signer.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use clap::Parser;
|
||||
use exver::VersionRange;
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::PackageId;
|
||||
use crate::context::CliContext;
|
||||
use crate::prelude::*;
|
||||
use crate::registry::admin::display_package_signers;
|
||||
use crate::registry::context::RegistryContext;
|
||||
use crate::registry::signer::SignerInfo;
|
||||
use crate::rpc_continuations::Guid;
|
||||
use crate::util::serde::HandlerExtSerde;
|
||||
|
||||
pub fn signer_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand(
|
||||
"add",
|
||||
from_fn_async(add_package_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Add package signer")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"remove",
|
||||
from_fn_async(remove_package_signer)
|
||||
.with_metadata("admin", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("Remove package signer")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
"list",
|
||||
from_fn_async(list_package_signers)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_custom_display_fn(|handle, result| {
|
||||
display_package_signers(handle.params, result)
|
||||
})
|
||||
.with_about("List package signers and related signer info")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct AddPackageSignerParams {
|
||||
pub id: PackageId,
|
||||
pub signer: Guid,
|
||||
#[arg(long)]
|
||||
#[ts(type = "string | null")]
|
||||
pub versions: Option<VersionRange>,
|
||||
}
|
||||
|
||||
pub async fn add_package_signer(
|
||||
ctx: RegistryContext,
|
||||
AddPackageSignerParams {
|
||||
id,
|
||||
signer,
|
||||
versions,
|
||||
}: AddPackageSignerParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
ensure_code!(
|
||||
db.as_index().as_signers().contains_key(&signer)?,
|
||||
ErrorKind::InvalidRequest,
|
||||
"unknown signer {signer}"
|
||||
);
|
||||
|
||||
db.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.as_idx_mut(&id)
|
||||
.or_not_found(&id)?
|
||||
.as_authorized_mut()
|
||||
.insert(&signer, &versions.unwrap_or_default())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct RemovePackageSignerParams {
|
||||
pub id: PackageId,
|
||||
pub signer: Guid,
|
||||
}
|
||||
|
||||
pub async fn remove_package_signer(
|
||||
ctx: RegistryContext,
|
||||
RemovePackageSignerParams { id, signer }: RemovePackageSignerParams,
|
||||
) -> Result<(), Error> {
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
if db
|
||||
.as_index_mut()
|
||||
.as_package_mut()
|
||||
.as_packages_mut()
|
||||
.as_idx_mut(&id)
|
||||
.or_not_found(&id)?
|
||||
.as_authorized_mut()
|
||||
.remove(&signer)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!("signer {signer} is not authorized to sign for {id}"),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.result
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Parser, TS)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub struct ListPackageSignersParams {
|
||||
pub id: PackageId,
|
||||
}
|
||||
|
||||
pub async fn list_package_signers(
|
||||
ctx: RegistryContext,
|
||||
ListPackageSignersParams { id }: ListPackageSignersParams,
|
||||
) -> Result<BTreeMap<Guid, (SignerInfo, VersionRange)>, Error> {
|
||||
let db = ctx.db.peek().await;
|
||||
db.as_index()
|
||||
.as_package()
|
||||
.as_packages()
|
||||
.as_idx(&id)
|
||||
.or_not_found(&id)?
|
||||
.as_authorized()
|
||||
.de()?
|
||||
.into_iter()
|
||||
.filter_map(|(guid, versions)| {
|
||||
db.as_index()
|
||||
.as_signers()
|
||||
.as_idx(&guid)
|
||||
.map(|s| s.de().map(|s| (guid, (s, versions))))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
151
core/src/registry/signer.rs
Normal file
151
core/src/registry/signer.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use clap::builder::ValueParserFactory;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::sign::commitment::Digestable;
|
||||
use crate::sign::{AnySignature, AnyVerifyingKey, SignatureScheme};
|
||||
use crate::util::FromStrParser;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, HasModel, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[model = "Model<Self>"]
|
||||
#[ts(export)]
|
||||
pub struct SignerInfo {
|
||||
pub name: String,
|
||||
pub contact: Vec<ContactInfo>,
|
||||
pub keys: HashSet<AnyVerifyingKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
// TODO: better types
|
||||
pub enum ContactInfo {
|
||||
Email(String),
|
||||
Matrix(String),
|
||||
Website(#[ts(type = "string")] Url),
|
||||
}
|
||||
impl std::fmt::Display for ContactInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Email(e) => write!(f, "mailto:{e}"),
|
||||
Self::Matrix(m) => write!(f, "https://matrix.to/#/{m}"),
|
||||
Self::Website(w) => write!(f, "{w}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromStr for ContactInfo {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(if let Some(s) = s.strip_prefix("mailto:") {
|
||||
Self::Email(s.to_owned())
|
||||
} else if let Some(s) = s.strip_prefix("https://matrix.to/#/") {
|
||||
Self::Matrix(s.to_owned())
|
||||
} else {
|
||||
Self::Website(s.parse()?)
|
||||
})
|
||||
}
|
||||
}
|
||||
impl ValueParserFactory for ContactInfo {
|
||||
type Parser = FromStrParser<Self>;
|
||||
fn value_parser() -> Self::Parser {
|
||||
Self::Parser::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export)]
|
||||
pub enum AcceptSigners {
|
||||
#[serde(skip)]
|
||||
Accepted,
|
||||
Signer(AnyVerifyingKey),
|
||||
Any(Vec<AcceptSigners>),
|
||||
All(Vec<AcceptSigners>),
|
||||
}
|
||||
impl AcceptSigners {
|
||||
const fn null() -> Self {
|
||||
Self::Any(Vec::new())
|
||||
}
|
||||
pub fn flatten(self) -> Self {
|
||||
match self {
|
||||
Self::Any(mut s) | Self::All(mut s) if s.len() == 1 => s.swap_remove(0).flatten(),
|
||||
s => s,
|
||||
}
|
||||
}
|
||||
pub fn accepted(&self) -> bool {
|
||||
match self {
|
||||
Self::Accepted => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub fn try_accept(self) -> Result<(), Error> {
|
||||
if self.accepted() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("signer(s) not accepted"),
|
||||
ErrorKind::InvalidSignature,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub fn process_signature(
|
||||
&mut self,
|
||||
signer: &AnyVerifyingKey,
|
||||
commitment: &impl Digestable,
|
||||
context: &str,
|
||||
signature: &AnySignature,
|
||||
) -> Result<(), Error> {
|
||||
let mut res = Ok(());
|
||||
let new = match std::mem::replace(self, Self::null()) {
|
||||
Self::Accepted => Self::Accepted,
|
||||
Self::Signer(s) => {
|
||||
if &s == signer {
|
||||
res = signer
|
||||
.scheme()
|
||||
.verify_commitment(signer, commitment, context, signature);
|
||||
Self::Accepted
|
||||
} else {
|
||||
Self::Signer(s)
|
||||
}
|
||||
}
|
||||
Self::All(mut s) => {
|
||||
res = s
|
||||
.iter_mut()
|
||||
.map(|s| s.process_signature(signer, commitment, context, signature))
|
||||
.collect();
|
||||
if s.iter().all(|s| s.accepted()) {
|
||||
Self::Accepted
|
||||
} else {
|
||||
Self::All(s)
|
||||
}
|
||||
}
|
||||
Self::Any(mut s) => {
|
||||
match s
|
||||
.iter_mut()
|
||||
.map(|s| {
|
||||
s.process_signature(signer, commitment, context, signature)?;
|
||||
Ok(s)
|
||||
})
|
||||
.filter_ok(|s| s.accepted())
|
||||
.next()
|
||||
{
|
||||
Some(Ok(s)) => std::mem::replace(s, Self::null()),
|
||||
Some(Err(e)) => {
|
||||
res = Err(e);
|
||||
Self::Any(s)
|
||||
}
|
||||
None => Self::Any(s),
|
||||
}
|
||||
}
|
||||
};
|
||||
*self = new;
|
||||
res
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user