use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context as PollContext, Poll};
use std::time::{SystemTime, UNIX_EPOCH};
use alkblobs::ops::{stat_pair, OpsConfig, OpsContext, PinConverter, BLOB_MISSING};
use alkblobs::{Key, Pin, Store as BlobStore};
use alkcall::core::auth::Identity;
use alkcall::core::ownership::OwnershipProvider;
use alkcall::core::types::Capabilities;
use alkcall::registry::context::{AbortPolicy, OperationContext, ScopedPeerEnv};
use alkcall::registry::env::{LocalOperationEnv, OperationEnv};
use alkcall::registry::registration::OperationRegistry;
use alkcall::OperationRegistryBuilder;
use sha2::{Digest as _, Sha256};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::mpsc;
use crate::error::OpenError;
use crate::lfs::LfsRows;
use crate::liveness::lfs_media_key;
use crate::registry::RegistryRead;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsAdapterConfig {
pub window_bytes: u64,
pub max_object_bytes: u64,
}
impl Default for LfsAdapterConfig {
fn default() -> Self {
LfsAdapterConfig {
window_bytes: 32 * 1024 * 1024,
max_object_bytes: 64 * 1024 * 1024,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LfsBatchOperation {
Upload,
Download,
}
impl LfsBatchOperation {
pub fn from_wire(wire: &str) -> Option<Self> {
match wire {
"upload" => Some(LfsBatchOperation::Upload),
"download" => Some(LfsBatchOperation::Download),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsBatchObject {
pub oid: String,
pub size: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsBatchRequest {
pub operation: LfsBatchOperation,
pub hash_algo: Option<String>,
pub objects: Vec<LfsBatchObject>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsHrefMint {
pub repo_id: String,
pub oid: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsActions {
pub upload: Option<LfsHrefMint>,
pub download: Option<LfsHrefMint>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsObjectError {
pub code: u16,
pub message: &'static str,
}
impl LfsObjectError {
fn refused() -> Self {
LfsObjectError {
code: 403,
message: "refused by grant",
}
}
fn not_found() -> Self {
LfsObjectError {
code: 404,
message: "object not found",
}
}
fn malformed() -> Self {
LfsObjectError {
code: 422,
message: "malformed oid",
}
}
fn disagree() -> Self {
LfsObjectError {
code: 422,
message: "size disagreement",
}
}
fn server() -> Self {
LfsObjectError {
code: 500,
message: "server error",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsObjectAnswer {
pub oid: String,
pub size: u64,
pub authenticated: bool,
pub error: Option<LfsObjectError>,
pub actions: Option<LfsActions>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LfsBatchResponse {
pub transfer: &'static str,
pub hash_algo: &'static str,
pub objects: Vec<LfsObjectAnswer>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LfsRange {
pub offset: u64,
pub len: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum LfsAdapterError {
#[error("forbidden: {0}")]
Forbidden(String),
#[error("unsupported hash_algo {0}: the server serves sha256 only")]
HashAlgo(String),
#[error("invalid LFS input: {0}")]
Invalid(String),
#[error("object of {0} bytes exceeds the configured download ceiling")]
TooLarge(u64),
#[error("the requested range is not satisfiable")]
NotSatisfiable,
#[error("storage: {0}")]
Storage(String),
#[error("lfs rows: {0}")]
Rows(String),
#[error("size-less offer refused: the pre-keyed put requires the batch's size")]
SizelessOffer,
#[error("mislabeled body: the stream's digest contradicts the claimed oid {0}")]
MislabeledBody(String),
#[error("custody: {0}")]
Custody(String),
#[error("mint refused at the token registry's admission cap: {0}")]
MintRefused(String),
}
pub struct LfsGrantProvider {
provider: Arc<crate::registry::CachedRegistryProvider>,
}
impl LfsGrantProvider {
pub fn new(provider: Arc<crate::registry::CachedRegistryProvider>) -> Self {
LfsGrantProvider { provider }
}
}
impl OwnershipProvider for LfsGrantProvider {
fn owns(
&self,
identity: &Identity,
resource_type: &str,
resource_id: &str,
action: &str,
) -> bool {
if resource_type != "blob-namespace" {
return false;
}
let action = match action {
"read" => alkgit::AccessAction::Read,
"write" => alkgit::AccessAction::Write,
_ => return false,
};
match self.provider.resolve(resource_id) {
Some(record) => alkgit::authorize(&record, Some(identity), action),
None => false,
}
}
fn owned_resources(&self, identity: &Identity, resource_type: &str) -> Vec<String> {
if resource_type != "blob-namespace" {
return Vec::new();
}
let snapshot = self.provider.snapshot();
let mut owned = Vec::new();
for (repo_id, record) in snapshot.iter() {
if alkgit::authorize(record, Some(identity), alkgit::AccessAction::Read) {
owned.push(repo_id.clone());
}
}
owned.sort();
owned
}
fn owns_any(&self, identity: &Identity, resource_type: &str) -> bool {
!self.owned_resources(identity, resource_type).is_empty()
}
}
struct RowsAssertedConverter;
impl PinConverter for RowsAssertedConverter {
fn convert(&self, _key: &Key) {}
}
pub struct LfsAdapter {
ops: Arc<OpsContext>,
registry: Arc<OperationRegistry>,
env: Arc<LocalOperationEnv>,
rows: Arc<dyn LfsRows>,
ownership: Arc<dyn OwnershipProvider>,
config: LfsAdapterConfig,
requests: AtomicU64,
}
impl LfsAdapter {
pub fn new(
store: Arc<BlobStore>,
rows: Arc<dyn LfsRows>,
ownership: Arc<dyn OwnershipProvider>,
config: LfsAdapterConfig,
) -> Result<Self, OpenError> {
if config.window_bytes == 0 {
return Err(OpenError::Invalid(
"the LFS adapter's window_bytes must be non-zero".to_owned(),
));
}
let ops = Arc::new(OpsContext::new(Arc::clone(&store), OpsConfig::default()));
let (spec, handler) = stat_pair(Arc::clone(&ops));
let registry = Arc::new(
OperationRegistryBuilder::new()
.with_local(spec, handler, None, None, Capabilities::new())
.map_err(|e| OpenError::Invalid(format!("the ops registry assembly: {e}")))?
.build(),
);
let env = Arc::new(LocalOperationEnv::new(Arc::clone(®istry)));
Ok(LfsAdapter {
ops,
registry,
env,
rows,
ownership,
config,
requests: AtomicU64::new(0),
})
}
pub fn config(&self) -> &LfsAdapterConfig {
&self.config
}
fn context(&self, identity: Option<Identity>) -> OperationContext {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default();
OperationContext {
request_id: format!(
"lfs-adapter-{}-{nanos}",
self.requests.fetch_add(1, Ordering::Relaxed)
),
parent_request_id: None,
identity,
handler_identity: None,
forwarded_for: None,
capabilities: Capabilities::new(),
metadata: std::collections::HashMap::new(),
scoped_env: ScopedPeerEnv::empty(),
env: Arc::clone(&self.env) as Arc<dyn OperationEnv>,
abort_policy: AbortPolicy::default(),
deadline: None,
internal: false,
ownership: Some(Arc::clone(&self.ownership)),
}
}
pub async fn check(
&self,
identity: Option<&Identity>,
repo_id: &str,
request: LfsBatchRequest,
) -> Result<LfsBatchResponse, LfsAdapterError> {
let identity = require_identity(identity)?;
if let Some(algo) = &request.hash_algo {
if algo != "sha256" {
return Err(LfsAdapterError::HashAlgo(algo.clone()));
}
}
let action = match request.operation {
LfsBatchOperation::Upload => "write",
LfsBatchOperation::Download => "read",
};
if !self
.ownership
.owns(&identity, "blob-namespace", repo_id, action)
{
return Err(LfsAdapterError::Forbidden(format!(
"not granted: blob-namespace/{repo_id} {action}"
)));
}
for object in &request.objects {
if lfs_media_key(&object.oid).is_ok() {
self.rows
.assert(repo_id, &lower_hex(&object.oid), object.size)
.await
.map_err(|e| LfsAdapterError::Rows(e.to_string()))?;
}
}
let mut objects = Vec::with_capacity(request.objects.len());
for object in &request.objects {
let oid = lower_hex(&object.oid);
let answer = match lfs_media_key(&oid) {
Err(_) => LfsObjectAnswer {
oid,
size: object.size,
authenticated: true,
error: Some(LfsObjectError::malformed()),
actions: None,
},
Ok(key) => {
self.answers(
&identity,
repo_id,
request.operation,
&oid,
object.size,
&key,
)
.await
}
};
objects.push(answer);
}
Ok(LfsBatchResponse {
transfer: "basic",
hash_algo: "sha256",
objects,
})
}
async fn answers(
&self,
identity: &Identity,
repo_id: &str,
operation: LfsBatchOperation,
oid: &str,
size: u64,
key: &Key,
) -> LfsObjectAnswer {
match self.stat(identity, repo_id, key).await {
StatProbe::Missing => match operation {
LfsBatchOperation::Upload => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: None,
actions: Some(LfsActions {
upload: Some(LfsHrefMint {
repo_id: repo_id.to_owned(),
oid: oid.to_owned(),
}),
download: None,
}),
},
LfsBatchOperation::Download => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: Some(LfsObjectError::not_found()),
actions: None,
},
},
StatProbe::Length(len) if len == size => match operation {
LfsBatchOperation::Upload => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: None,
actions: None,
},
LfsBatchOperation::Download => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: None,
actions: Some(LfsActions {
upload: None,
download: Some(LfsHrefMint {
repo_id: repo_id.to_owned(),
oid: oid.to_owned(),
}),
}),
},
},
StatProbe::Length(_) => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: Some(LfsObjectError::disagree()),
actions: None,
},
StatProbe::Forbidden => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: false,
error: Some(LfsObjectError::refused()),
actions: None,
},
StatProbe::Failed => LfsObjectAnswer {
oid: oid.to_owned(),
size,
authenticated: true,
error: Some(LfsObjectError::server()),
actions: None,
},
}
}
async fn stat(&self, identity: &Identity, namespace: &str, key: &Key) -> StatProbe {
let envelope = self
.registry
.invoke(
"blobs/stat",
serde_json::json!({
"namespace": namespace,
"digest": key_hex(key),
}),
self.context(Some(identity.clone())),
)
.await;
match envelope.result {
Ok(payload) => {
let len = payload
.get("len")
.and_then(serde_json::Value::as_u64)
.unwrap_or_default();
StatProbe::Length(len)
}
Err(error) if error.code == BLOB_MISSING => StatProbe::Missing,
Err(error) if error.code == "FORBIDDEN" => StatProbe::Forbidden,
Err(_) => StatProbe::Failed,
}
}
pub async fn upload<R>(
&self,
identity: Option<&Identity>,
repo_id: &str,
oid: &str,
size: Option<u64>,
body: R,
) -> Result<(), LfsAdapterError>
where
R: AsyncRead + Unpin + Send,
{
let identity = require_identity(identity)?;
if !self
.ownership
.owns(&identity, "blob-namespace", repo_id, "write")
{
return Err(LfsAdapterError::Forbidden(format!(
"not granted: blob-namespace/{repo_id} write"
)));
}
let size = size.ok_or(LfsAdapterError::SizelessOffer)?;
let oid = lower_hex(oid);
let expected = lfs_media_key(&oid).map_err(|e| LfsAdapterError::Invalid(e.to_string()))?;
self.rows
.assert(repo_id, &oid, size)
.await
.map_err(|e| LfsAdapterError::Rows(e.to_string()))?;
let (key, pin) =
pre_keyed_put(Arc::clone(self.ops.store()), &expected, &oid, size, body).await?;
self.hand_over(repo_id, key, pin).await
}
async fn hand_over(&self, namespace: &str, key: Key, pin: Pin) -> Result<(), LfsAdapterError> {
let refused = |key: &Key| {
LfsAdapterError::MintRefused(format!(
"no token issued, no custody taken; the committed key {} stands — the re-put is \
CAS-idempotent",
hex_of(key.digest().bytes()),
))
};
match self.ops.fleet_owner() {
Some(fleet) => match self.ops.mint_fleet(namespace, key.clone(), &fleet) {
Ok(token) => {
self.ops
.bind_converter(namespace, Arc::new(RowsAssertedConverter));
if self.ops.confirm(namespace, &token) {
drop(pin);
Ok(())
} else {
drop(pin);
Err(LfsAdapterError::Custody(
"the confirm conversion refused (the mapping stands intact)".to_owned(),
))
}
}
Err(alkblobs::Error::RegistryFull) => Err(refused(&key)),
Err(e) => {
drop(pin);
Err(LfsAdapterError::Custody(e.to_string()))
}
},
None => {
self.ops
.bind_converter(namespace, Arc::new(RowsAssertedConverter));
match self.ops.mint_custodial(namespace, key.clone(), pin) {
Ok(token) if self.ops.confirm(namespace, &token) => Ok(()),
Ok(_token) => Err(LfsAdapterError::Custody(
"the confirm conversion refused (the mapping stands intact)".to_owned(),
)),
Err(alkblobs::Error::RegistryFull) => Err(refused(&key)),
Err(e) => Err(LfsAdapterError::Custody(e.to_string())),
}
}
}
}
pub async fn download(
&self,
identity: Option<&Identity>,
repo_id: &str,
oid: &str,
range: Option<LfsRange>,
) -> Result<LfsDownload, LfsAdapterError> {
let identity = require_identity(identity)?;
if !self
.ownership
.owns(&identity, "blob-namespace", repo_id, "read")
{
return Err(LfsAdapterError::Forbidden(format!(
"not granted: blob-namespace/{repo_id} read"
)));
}
let oid = lower_hex(oid);
let key = lfs_media_key(&oid).map_err(|e| LfsAdapterError::Invalid(e.to_string()))?;
let meta = self
.ops
.store()
.stat(key.key_bytes().as_slice())
.await
.map_err(|e| LfsAdapterError::Storage(e.to_string()))?;
let entry_len = match meta {
Some(meta) => meta.len,
None => {
return Err(LfsAdapterError::Storage(
"the media entry is absent".to_owned(),
))
}
};
let (from, until) = match range {
None => (0, entry_len),
Some(range) => {
if range.offset >= entry_len {
return Err(LfsAdapterError::NotSatisfiable);
}
let end = match range.len {
Some(len) => range.offset.saturating_add(len).min(entry_len),
None => entry_len,
};
(range.offset, end)
}
};
if until - from > self.config.max_object_bytes {
return Err(LfsAdapterError::TooLarge(entry_len));
}
let whole_span = from == 0 && until == entry_len;
Ok(LfsDownload::start(
Arc::clone(self.ops.store()),
key,
from,
until,
self.config.window_bytes,
whole_span,
))
}
}
enum StatProbe {
Missing,
Length(u64),
Forbidden,
Failed,
}
fn lower_hex(oid: &str) -> String {
oid.chars().map(|c| c.to_ascii_lowercase()).collect()
}
fn key_hex(key: &Key) -> String {
let bytes = key.as_bytes();
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in &bytes {
hex.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
hex.push(char::from_digit((byte & 0xf) as u32, 16).unwrap_or('0'));
}
hex
}
fn hex_of(bytes: &[u8]) -> String {
let mut hex = String::with_capacity(bytes.len() * 2);
for byte in bytes {
hex.push(char::from_digit((byte >> 4) as u32, 16).unwrap_or('0'));
hex.push(char::from_digit((byte & 0xf) as u32, 16).unwrap_or('0'));
}
hex
}
fn require_identity(identity: Option<&Identity>) -> Result<Identity, LfsAdapterError> {
identity
.cloned()
.ok_or_else(|| LfsAdapterError::Forbidden("authentication required".to_owned()))
}
async fn pre_keyed_put<R>(
store: Arc<BlobStore>,
expected: &Key,
oid: &str,
size: u64,
body: R,
) -> Result<(Key, Pin), LfsAdapterError>
where
R: AsyncRead + Unpin + Send,
{
const PIPE_BOUND: usize = 64;
const READ_CHUNK: usize = 16 * 1024;
enum StreamEnd {
Eof,
BodyFailed(tokio::io::Error),
PutEnded,
}
let fail = Arc::new(AtomicBool::new(false));
let (tx, rx) = mpsc::channel::<Arc<Vec<u8>>>(PIPE_BOUND);
let put = {
let store = Arc::clone(&store);
let fail = Arc::clone(&fail);
tokio::spawn(async move {
store
.put_as(
alkblobs::Algorithm::GitObjectSha256,
Some(size),
PutPipe {
rx,
fail,
buffer: None,
offset: 0,
},
)
.await
})
};
let mut body = body;
let mut hasher = Sha256::new();
let mut window = vec![0u8; READ_CHUNK];
let end = loop {
if put.is_finished() {
break StreamEnd::PutEnded;
}
match body.read(&mut window).await {
Ok(0) => break StreamEnd::Eof,
Ok(n) => {
hasher.update(&window[..n]);
if tx.send(Arc::new(window[..n].to_vec())).await.is_err() {
break StreamEnd::PutEnded;
}
}
Err(e) => break StreamEnd::BodyFailed(e),
}
};
match end {
StreamEnd::Eof => {
let derived = hex_of(&hasher.finalize());
if derived != oid {
fail.store(true, Ordering::Release);
drop(tx);
let _ = put.await;
return Err(LfsAdapterError::MislabeledBody(format!(
"no stored content, no held pin — {oid}"
)));
}
drop(tx);
match put.await {
Ok(Ok(landing)) => {
if landing.0 != *expected {
return Err(LfsAdapterError::MislabeledBody(oid.to_owned()));
}
Ok(landing)
}
Ok(Err(e)) => Err(LfsAdapterError::Storage(e.to_string())),
Err(e) => Err(LfsAdapterError::Storage(format!(
"the put's store task failed: {e}"
))),
}
}
StreamEnd::BodyFailed(e) => {
fail.store(true, Ordering::Release);
drop(tx);
let _ = put.await;
Err(LfsAdapterError::Storage(format!(
"the upload body failed mid-stream: {e}"
)))
}
StreamEnd::PutEnded => match put.await {
Ok(Ok(_)) => Err(LfsAdapterError::Storage(
"the put's store arm completed against a torn stream".to_owned(),
)),
Ok(Err(e)) => Err(LfsAdapterError::Storage(e.to_string())),
Err(e) => Err(LfsAdapterError::Storage(format!(
"the put's store task failed: {e}"
))),
},
}
}
struct PutPipe {
rx: mpsc::Receiver<Arc<Vec<u8>>>,
fail: Arc<AtomicBool>,
buffer: Option<Arc<Vec<u8>>>,
offset: usize,
}
impl AsyncRead for PutPipe {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut PollContext<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<tokio::io::Result<()>> {
let this = self.get_mut();
loop {
if this.fail.load(Ordering::Acquire) {
return Poll::Ready(Err(tokio::io::Error::new(
tokio::io::ErrorKind::BrokenPipe,
"the put's feed failed pre-commit (the stream errored)",
)));
}
if let Some(chunk) = &this.buffer {
if this.offset < chunk.len() {
let n = (chunk.len() - this.offset).min(buf.remaining());
buf.put_slice(&chunk[this.offset..this.offset + n]);
this.offset += n;
return Poll::Ready(Ok(()));
}
this.buffer = None;
this.offset = 0;
}
match this.rx.poll_recv(cx) {
Poll::Ready(Some(chunk)) => {
this.buffer = Some(chunk);
this.offset = 0;
}
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => return Poll::Pending,
}
}
}
}
pub struct LfsDownload {
rx: mpsc::Receiver<tokio::io::Result<Vec<u8>>>,
buffer: Option<Vec<u8>>,
position: usize,
}
impl LfsDownload {
fn start(
store: Arc<BlobStore>,
key: Key,
from: u64,
until: u64,
window_bytes: u64,
whole_span: bool,
) -> Self {
let (tx, rx) = mpsc::channel(2);
tokio::spawn(verified_windows(
store,
key,
from,
until,
window_bytes,
whole_span,
tx,
));
LfsDownload {
rx,
buffer: None,
position: 0,
}
}
}
impl AsyncRead for LfsDownload {
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut PollContext<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<tokio::io::Result<()>> {
let this = self.get_mut();
loop {
if let Some(chunk) = this.buffer.as_ref() {
if this.position < chunk.len() {
let n = (chunk.len() - this.position).min(buf.remaining());
buf.put_slice(&chunk[this.position..this.position + n]);
this.position += n;
return Poll::Ready(Ok(()));
}
this.buffer = None;
this.position = 0;
}
match this.rx.poll_recv(cx) {
Poll::Ready(Some(Ok(chunk))) => {
this.buffer = Some(chunk);
this.position = 0;
}
Poll::Ready(Some(Err(e))) => return Poll::Ready(Err(e)),
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => return Poll::Pending,
}
}
}
}
async fn verified_windows(
store: Arc<BlobStore>,
key: Key,
from: u64,
until: u64,
window_bytes: u64,
whole_span: bool,
tx: mpsc::Sender<tokio::io::Result<Vec<u8>>>,
) {
let key_bytes = key.key_bytes();
let expected = whole_span.then(|| key.digest().bytes().to_vec());
let mut hasher = whole_span.then(Sha256::new);
let mut offset = from;
while offset < until {
let window_len = window_bytes.min(until - offset);
let last = offset + window_len == until;
match store
.read_range(key_bytes.as_slice(), offset, window_len)
.await
{
Ok((slice, digest)) => {
let hash = Sha256::digest(&slice);
if slice.len() as u64 != window_len || hex_of(hash.as_slice()) != hex_of(&digest) {
let _ = tx
.send(Err(tokio::io::Error::other(
"the verified window's slice digest mismatched mid-flight",
)))
.await;
return;
}
if let Some(h) = hasher.as_mut() {
h.update(&slice);
}
if last && whole_span {
if let (Some(h), Some(expected_bytes)) = (hasher.as_ref(), expected.as_deref())
{
let digest_now = h.clone().finalize();
if digest_now.as_slice() != expected_bytes {
let _ = tx
.send(Err(tokio::io::Error::other(
"the served media's whole-content digest mismatched the oid",
)))
.await;
return;
}
}
}
offset += window_len;
if tx.send(Ok(slice)).await.is_err() {
return;
}
}
Err(e) => {
let _ = tx
.send(Err(tokio::io::Error::other(format!(
"the media read failed: {e}"
))))
.await;
return;
}
}
}
}
#[cfg(test)]
mod battery {
use std::collections::HashMap;
use tokio::io::AsyncReadExt;
use alkgit::{AccessAction, RepoRecord, Visibility};
use super::*;
use crate::registry::{CachedRegistryProvider, RegistryRow};
fn oid_of(content: &[u8]) -> String {
let digest = Sha256::digest(content);
hex_of(digest.as_slice())
}
fn identity(id: &str) -> Identity {
Identity {
id: id.to_owned(),
scopes: Vec::new(),
resources: HashMap::new(),
}
}
fn identity_with_static_claim(id: &str) -> Identity {
let mut resources = HashMap::new();
resources.insert(
"blob-namespace".to_owned(),
vec!["read".to_owned(), "write".to_owned()],
);
Identity {
id: id.to_owned(),
scopes: Vec::new(),
resources,
}
}
fn record(
repo_id: &str,
visibility: Visibility,
grants: &[(&str, &[AccessAction])],
) -> RepoRecord {
RepoRecord {
repo_id: repo_id.to_owned(),
visibility,
grants: grants
.iter()
.map(|(id, actions)| ((*id).to_owned(), actions.to_vec()))
.collect(),
}
}
fn registry_provider() -> Arc<CachedRegistryProvider> {
let provider = Arc::new(CachedRegistryProvider::new());
provider.install(vec![
RegistryRow {
record: record(
"owner/granted",
Visibility::Private,
&[("u-writer", &[AccessAction::Read, AccessAction::Write])],
),
seq: 1,
},
RegistryRow {
record: record(
"owner/readonly",
Visibility::Private,
&[
("u-writer", &[AccessAction::Read, AccessAction::Write]),
("u-reader", &[AccessAction::Read]),
],
),
seq: 2,
},
RegistryRow {
record: record(
"owner/public",
Visibility::Public,
&[("u-writer", &[AccessAction::Read, AccessAction::Write])],
),
seq: 3,
},
RegistryRow {
record: record("owner/closed", Visibility::Private, &[]),
seq: 4,
},
]);
provider
}
fn adapter(
provider: Arc<dyn OwnershipProvider>,
config: LfsAdapterConfig,
) -> (LfsAdapter, Arc<BlobStore>, Arc<crate::lfs::InMemoryLfsRows>) {
let rows = Arc::new(crate::lfs::InMemoryLfsRows::new());
let store = Arc::new(BlobStore::mem_only());
let adapter = LfsAdapter::new(Arc::clone(&store), rows.clone(), provider, config).unwrap();
(adapter, store, rows)
}
fn grant_provider() -> Arc<dyn OwnershipProvider> {
Arc::new(LfsGrantProvider::new(registry_provider()))
}
fn object(oid: &str, size: u64) -> LfsBatchObject {
LfsBatchObject {
oid: oid.to_owned(),
size,
}
}
fn upload_request(objects: Vec<LfsBatchObject>) -> LfsBatchRequest {
LfsBatchRequest {
operation: LfsBatchOperation::Upload,
hash_algo: None,
objects,
}
}
fn download_request(objects: Vec<LfsBatchObject>) -> LfsBatchRequest {
LfsBatchRequest {
operation: LfsBatchOperation::Download,
hash_algo: None,
objects,
}
}
async fn upload_media(
adapter: &LfsAdapter,
repo_id: &str,
content: &[u8],
) -> Result<(), LfsAdapterError> {
adapter
.upload(
Some(&identity("u-writer")),
repo_id,
&oid_of(content),
Some(content.len() as u64),
content,
)
.await
}
async fn read_all(mut download: LfsDownload) -> Vec<u8> {
let mut out = Vec::new();
download
.read_to_end(&mut out)
.await
.expect("the download reads");
out
}
async fn download_whole(adapter: &LfsAdapter, repo_id: &str, oid: &str) -> Vec<u8> {
read_all(
adapter
.download(Some(&identity("u-writer")), repo_id, oid, None)
.await
.unwrap(),
)
.await
}
#[tokio::test]
async fn the_upload_batch_and_the_pre_keyed_put() {
let (adapter, store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
let writer = identity("u-writer");
let content_a = b"the first LFS media\n".to_vec();
let content_b = b"the second LFS media\n".to_vec();
let (oid_a, oid_b) = (oid_of(&content_a), oid_of(&content_b));
let answer = adapter
.check(
Some(&writer),
"owner/granted",
upload_request(vec![object(&oid_a, content_a.len() as u64)]),
)
.await
.unwrap();
assert_eq!(answer.transfer, "basic");
assert_eq!(answer.hash_algo, "sha256");
let first = &answer.objects[0];
assert_eq!(first.oid, oid_a);
assert!(first.authenticated);
assert!(first.error.is_none());
let mint = first
.actions
.as_ref()
.and_then(|a| a.upload.as_ref())
.expect("the upload mint input");
assert_eq!(mint.repo_id, "owner/granted");
assert_eq!(mint.oid, oid_a);
assert!(first
.actions
.as_ref()
.and_then(|a| a.download.as_ref())
.is_none());
let asserted = rows.repo_rows("owner/granted").await.unwrap();
assert_eq!(
asserted,
vec![crate::lfs::LfsRow {
repo_id: "owner/granted".to_owned(),
oid: oid_a.clone(),
size: content_a.len() as u64,
refcount: 1,
}]
);
upload_media(&adapter, "owner/granted", &content_a)
.await
.unwrap();
assert_eq!(
download_whole(&adapter, "owner/granted", &oid_a).await,
content_a
);
let stat = rows.repo_rows("owner/granted").await.unwrap();
assert_eq!(
stat.iter().find(|r| r.oid == oid_a).unwrap().size,
content_a.len() as u64,
"the upload refreshed the assertion"
);
let answer = adapter
.check(
Some(&writer),
"owner/granted",
upload_request(vec![
object(&oid_a, content_a.len() as u64),
object(&oid_b, content_b.len() as u64),
]),
)
.await
.unwrap();
assert!(
answer.objects[0].actions.is_none(),
"the present object: nothing to do"
);
assert!(answer.objects[0].error.is_none());
assert!(
answer.objects[1]
.actions
.as_ref()
.and_then(|a| a.upload.as_ref())
.is_some(),
"the absent object still offers the upload mint"
);
upload_media(&adapter, "owner/granted", &content_a)
.await
.unwrap();
assert_eq!(adapter.ops.tokens().len(), 0);
let key = crate::liveness::lfs_media_key(&oid_a).unwrap();
assert!(
store
.stat(key.key_bytes().as_slice())
.await
.unwrap()
.is_some(),
"the committed content stands post-confirm"
);
}
#[tokio::test]
async fn the_download_batch_shapes() {
let (adapter, _store, _rows) = adapter(grant_provider(), LfsAdapterConfig::default());
let content = b"the downloadable media\n".to_vec();
let oid = oid_of(&content);
upload_media(&adapter, "owner/public", &content)
.await
.unwrap();
let answer = adapter
.check(
Some(&identity("anon")),
"owner/public",
download_request(vec![
object(&oid, content.len() as u64),
object(&oid_of(b"never stored"), 10),
]),
)
.await
.unwrap();
let present = &answer.objects[0];
assert!(present.authenticated);
assert!(present.error.is_none());
let mint = present
.actions
.as_ref()
.and_then(|a| a.download.as_ref())
.expect("the download mint input");
assert_eq!(mint.repo_id, "owner/public");
assert_eq!(mint.oid, oid);
let absent = &answer.objects[1];
assert_eq!(absent.error.as_ref().unwrap().code, 404);
assert!(absent.actions.is_none());
assert!(matches!(
adapter
.check(
Some(&identity("anon")),
"owner/public",
upload_request(vec![object(&oid, 10)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
));
assert!(matches!(
adapter
.check(
Some(&identity("anon")),
"owner/closed",
download_request(vec![object(&oid, 10)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
));
let answer = adapter
.check(
Some(&identity("u-writer")),
"owner/granted",
download_request(vec![object(&oid, content.len() as u64 + 5)]),
)
.await
.unwrap();
assert_eq!(answer.objects[0].error.as_ref().unwrap().code, 422);
assert_eq!(
answer.objects[0].error.as_ref().unwrap().message,
"size disagreement"
);
}
#[tokio::test]
async fn the_hash_algo_gate_and_the_sizeless_refusal() {
let (adapter, _store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
let writer = identity("u-writer");
let oid = oid_of(b"gate media");
let mut request = upload_request(vec![object(&oid, 10)]);
request.hash_algo = Some("sha1".to_owned());
assert!(
matches!(
adapter.check(Some(&writer), "owner/granted", request).await,
Err(LfsAdapterError::HashAlgo(algo)) if algo == "sha1"
),
"a non-sha256 batch is the pinned 409"
);
assert!(
rows.repo_rows("owner/granted").await.unwrap().is_empty(),
"the refused batch asserted nothing"
);
let mut request = upload_request(vec![object(&oid, 10)]);
request.hash_algo = Some("sha256".to_owned());
adapter
.check(Some(&writer), "owner/granted", request)
.await
.unwrap();
adapter
.check(
Some(&writer),
"owner/granted",
upload_request(vec![object(&oid, 10)]),
)
.await
.unwrap();
assert_eq!(rows.repo_rows("owner/granted").await.unwrap().len(), 1);
assert!(
matches!(
adapter
.upload(Some(&writer), "owner/granted", &oid, None, &b"media"[..])
.await,
Err(LfsAdapterError::SizelessOffer)
),
"the size-less offer is a protocol error"
);
assert_eq!(
rows.repo_rows("owner/granted").await.unwrap().len(),
1,
"the size-less refusal asserted nothing"
);
}
#[tokio::test]
async fn the_mislabeled_body_refusal() {
let (adapter, store, rows) = adapter(grant_provider(), LfsAdapterConfig::default());
let claimed = oid_of(b"the honest content");
let honest = b"the honest content".to_vec();
upload_media(&adapter, "owner/granted", &honest)
.await
.unwrap();
let answer = adapter
.download(Some(&identity("u-writer")), "owner/granted", &claimed, None)
.await
.unwrap();
assert_eq!(read_all(answer).await, honest);
let bad = b"the dishonest content".to_vec();
adapter
.upload(
Some(&identity("u-writer")),
"owner/granted",
&claimed,
Some(bad.len() as u64),
bad.as_slice(),
)
.await
.unwrap_err();
let key = crate::liveness::lfs_media_key(&claimed).unwrap();
let key_borrow = key.key_bytes();
assert_eq!(
store.stat(&key_borrow).await.unwrap().unwrap().len,
honest.len() as u64,
"the honest entry is the committed one — nothing mislabeled landed"
);
let list = store.list().await.unwrap();
assert_eq!(list.len(), 1, "the kill left zero residue");
assert_eq!(
rows.repo_rows("owner/granted").await.unwrap()[0].oid,
claimed
);
assert_eq!(
adapter.ops.tokens().len(),
0,
"no held pin, no mapping left"
);
adapter
.upload(
Some(&identity("u-writer")),
"owner/granted",
&oid_of(&bad),
Some(bad.len() as u64 + 10),
&bad[..bad.len() - 5],
)
.await
.unwrap_err();
assert_eq!(store.list().await.unwrap().len(), 1);
}
#[tokio::test]
async fn the_ranged_download_splits_and_verifies() {
let config = LfsAdapterConfig {
window_bytes: 1024,
max_object_bytes: 8 * 1024,
};
let (adapter, _store, _rows) = adapter(grant_provider(), config);
let mut content = Vec::new();
for i in 0..100u32 {
content.extend_from_slice(format!("{i:064x}").as_bytes());
}
let oid = oid_of(&content);
upload_media(&adapter, "owner/granted", &content)
.await
.unwrap();
assert_eq!(
download_whole(&adapter, "owner/granted", &oid).await,
content
);
let slice = adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&oid,
Some(LfsRange {
offset: 500,
len: Some(1000),
}),
)
.await
.unwrap();
assert_eq!(read_all(slice).await, content[500..1500]);
let tail = adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&oid,
Some(LfsRange {
offset: content.len() as u64 - 100,
len: None,
}),
)
.await
.unwrap();
assert_eq!(read_all(tail).await, content[content.len() - 100..]);
assert!(matches!(
adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&oid,
Some(LfsRange {
offset: content.len() as u64 + 1,
len: None,
})
)
.await,
Err(LfsAdapterError::NotSatisfiable)
));
let big = vec![0u8; 10 * 1024];
let big_oid = oid_of(&big);
upload_media(&adapter, "owner/granted", &big).await.unwrap();
assert!(matches!(
adapter
.download(Some(&identity("u-writer")), "owner/granted", &big_oid, None)
.await,
Err(LfsAdapterError::TooLarge(_))
));
assert!(matches!(
adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&big_oid,
Some(LfsRange {
offset: 0,
len: Some(10 * 1024),
}),
)
.await,
Err(LfsAdapterError::TooLarge(_))
));
let ranged = adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&big_oid,
Some(LfsRange {
offset: 0,
len: Some(4 * 1024),
}),
)
.await
.unwrap();
assert_eq!(read_all(ranged).await, big[..4 * 1024]);
assert!(matches!(
adapter
.download(
Some(&identity("u-writer")),
"owner/granted",
&oid_of(b"absent"),
None
)
.await,
Err(LfsAdapterError::Storage(_))
));
}
#[tokio::test]
async fn the_wired_provider_posture() {
let provider = grant_provider();
let (adapter, _store, _rows) = adapter(provider.clone(), LfsAdapterConfig::default());
let content = b"the gated media\n".to_vec();
let oid = oid_of(&content);
assert!(matches!(
adapter
.check(
None,
"owner/granted",
download_request(vec![object(&oid, 10)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
));
for identity in [identity("u-ghost"), identity_with_static_claim("u-ghost")] {
assert!(matches!(
adapter
.check(
Some(&identity),
"owner/closed",
download_request(vec![object(&oid, 10)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
));
assert!(matches!(
adapter
.upload(Some(&identity), "owner/closed", &oid, Some(10), &b"x"[..])
.await,
Err(LfsAdapterError::Forbidden(_))
));
assert!(matches!(
adapter
.download(Some(&identity), "owner/closed", &oid, None)
.await,
Err(LfsAdapterError::Forbidden(_))
));
}
let claimed = identity_with_static_claim("u-nobody");
assert!(
matches!(
adapter
.check(
Some(&claimed),
"owner/granted",
download_request(vec![object(&oid, 10)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
),
"a resources claim rides nothing when the provider refuses"
);
let reader = identity("u-reader");
upload_media(&adapter, "owner/readonly", &content)
.await
.unwrap();
let answer = adapter
.check(
Some(&reader),
"owner/readonly",
download_request(vec![object(&oid, content.len() as u64)]),
)
.await
.unwrap();
assert!(answer.objects[0].error.is_none());
assert!(answer.objects[0]
.actions
.as_ref()
.unwrap()
.download
.is_some());
assert!(matches!(
adapter
.check(
Some(&reader),
"owner/readonly",
upload_request(vec![object(&oid, 1)])
)
.await,
Err(LfsAdapterError::Forbidden(_))
));
let answer = adapter
.download(Some(&reader), "owner/readonly", &oid, None)
.await
.unwrap();
assert_eq!(read_all(answer).await, content);
let anon = identity("anon");
assert!(provider.owns(&anon, "blob-namespace", "owner/public", "read"));
assert!(!provider.owns(&anon, "blob-namespace", "owner/public", "write"));
let manages = record(
"repo/m",
Visibility::Private,
&[("u-m", &[AccessAction::Manage])],
);
assert!(!manages.grants.is_empty());
let provider2 = Arc::new(CachedRegistryProvider::new());
provider2.install(vec![RegistryRow {
record: manages,
seq: 1,
}]);
let p: Arc<dyn OwnershipProvider> = Arc::new(LfsGrantProvider::new(provider2));
assert!(p.owns(&identity("u-m"), "blob-namespace", "repo/m", "read"));
assert!(p.owns(&identity("u-m"), "blob-namespace", "repo/m", "write"));
assert!(!p.owns(&identity("u-x"), "blob-namespace", "repo/m", "read"));
assert!(!p.owns(&identity("u-x"), "blob-namespace", "repo/m", "write"));
assert!(!p.owns(&identity("u-m"), "blob-namespace", "repo/m", "manage"));
assert!(!p.owns(&identity("u-m"), "blob-namespace", "repo/unknown", "read"));
assert!(!p.owns(&identity("u-m"), "other-type", "repo/m", "read"));
assert_eq!(
p.owned_resources(&identity("u-m"), "blob-namespace"),
vec!["repo/m".to_owned()]
);
}
#[tokio::test]
async fn the_rows_are_asserted_at_check_time() {
let (adapter, _store, _rows) = adapter(grant_provider(), LfsAdapterConfig::default());
let a = oid_of(b"shared media a");
let malformed = "not-an-oid";
adapter
.check(
Some(&identity("u-writer")),
"owner/granted",
upload_request(vec![object(&a, 15)]),
)
.await
.unwrap();
adapter
.check(
Some(&identity("u-writer")),
"owner/readonly",
download_request(vec![object(&a, 15), object(malformed, 15)]),
)
.await
.unwrap();
let all = _rows.rows().await.unwrap();
assert_eq!(all.len(), 2, "the malformed oid asserted nothing");
for row in &all {
assert_eq!(row.refcount, 2, "two repos — one key, two rows: {row:?}");
}
adapter
.check(
Some(&identity("u-writer")),
"owner/granted",
upload_request(vec![object(&a, 15)]),
)
.await
.unwrap();
let all = _rows.rows().await.unwrap();
let mine = all.iter().filter(|r| r.repo_id == "owner/granted").count();
assert_eq!(mine, 1);
}
}
#[cfg(all(test, feature = "postgres"))]
mod pg_fleet_tests {
use std::collections::{BTreeMap, HashMap};
use std::io::Cursor;
use std::time::Duration;
use alkgit::{AccessAction, RepoRecord, Visibility};
use super::*;
use crate::postgres::live_harness::{drop_schema, live_pg, unique_schema};
use crate::registry::{CachedRegistryProvider, RegistryRow};
const REPO: &str = "owner/fleet-granted";
fn media_of(seed: u64, len: usize) -> Vec<u8> {
let pattern = seed.to_le_bytes();
let mut out = Vec::with_capacity(len);
while out.len() < len {
out.extend_from_slice(&pattern);
}
out.truncate(len);
out
}
fn oid_of(content: &[u8]) -> String {
hex_of(Sha256::digest(content).as_slice())
}
fn identity() -> Identity {
Identity {
id: "u-writer".to_owned(),
scopes: Vec::new(),
resources: HashMap::new(),
}
}
fn grant_provider() -> Arc<dyn OwnershipProvider> {
let provider = Arc::new(CachedRegistryProvider::new());
provider.install(vec![RegistryRow {
record: RepoRecord {
repo_id: REPO.to_owned(),
visibility: Visibility::Private,
grants: BTreeMap::from([(
"u-writer".to_owned(),
vec![AccessAction::Read, AccessAction::Write],
)]),
},
seq: 1,
}]);
Arc::new(LfsGrantProvider::new(provider))
}
#[tokio::test(flavor = "multi_thread")]
async fn the_fleet_handover_retires_every_admission_slot_across_repeated_uploads() {
let dsn = live_pg!();
let schema = unique_schema("lfs-fleet");
let blobs_schema = format!("{schema}_blobs");
let rows = Arc::new(crate::lfs::PgLfsRows::new(
crate::postgres::CoTenantPg::open(&dsn, &schema)
.await
.unwrap(),
));
let fleet_store = Arc::new(
alkblobs::Store::kv_only_postgres_fleet(
&dsn,
alkblobs::PgOpts {
schema: blobs_schema.clone(),
..alkblobs::PgOpts::default()
},
1024 * 1024,
Duration::ZERO,
Duration::from_secs(60),
)
.await
.unwrap(),
);
let adapter = LfsAdapter::new(
Arc::clone(&fleet_store),
rows.clone(),
grant_provider(),
LfsAdapterConfig {
window_bytes: 128 * 1024,
..LfsAdapterConfig::default()
},
)
.unwrap();
let identity = identity();
let owner = adapter
.ops
.fleet_owner()
.expect("the fleet store carries the serving instance's id");
const UPLOADS: usize = 12;
for i in 0..UPLOADS as u64 {
let body = media_of(i, 16 * 1024);
let oid = oid_of(&body);
adapter
.upload(
Some(&identity),
REPO,
&oid,
Some(body.len() as u64),
Cursor::new(&body),
)
.await
.expect("the fleet upload lands");
let pairs = rows.repo_rows(REPO).await.unwrap();
assert!(
pairs.iter().any(|row| row.oid == oid),
"upload {i}'s pair asserted its row: {pairs:?}"
);
assert_eq!(
adapter.ops.tokens().len(),
0,
"upload {i}'s mint retired at the confirm — no leaked slot"
);
}
assert_eq!(
rows.repo_rows(REPO).await.unwrap().len(),
UPLOADS,
"one row per upload pair"
);
let hold_body = media_of(0xff, 16 * 1024);
let hold_key = lfs_media_key(&oid_of(&hold_body)).expect("the canonical key");
let held = adapter
.ops
.mint_fleet(REPO, hold_key, &owner)
.expect("the boundary mint");
assert_eq!(adapter.ops.tokens().len(), 1, "the held token counts");
let next = media_of(0xfffe, 16 * 1024);
adapter
.upload(
Some(&identity),
REPO,
&oid_of(&next),
Some(next.len() as u64),
Cursor::new(&next),
)
.await
.expect("the upload around the held token lands");
assert_eq!(
adapter.ops.tokens().len(),
1,
"the upload's mint retired around the held one — no leak"
);
assert!(
adapter.ops.tokens().holds(&held),
"the held token's slot persists (its durability is the pin row's TTL)"
);
drop_schema(&dsn, &schema).await;
drop_schema(&dsn, &blobs_schema).await;
}
}