Implement hierarchical recipient policies (#5)

This commit is contained in:
Hermes Agent
2026-08-09 22:12:43 +00:00
parent 08bc244a4a
commit 34ce731716
7 changed files with 1284 additions and 0 deletions

View File

@@ -327,6 +327,10 @@ impl RepositorySnapshot {
}
}
pub fn recipient_policy_at(&self, directory: &DirectoryPath) -> Option<&RecipientPolicy> {
self.recipients.get(directory)
}
pub fn git_repository(&self, entry: &EntryPath) -> Option<&GitRepository> {
let mut directory = entry.parent();
loop {
@@ -409,6 +413,95 @@ impl Repository {
Ok(EncryptedEntry(bytes))
}
pub(crate) fn read_policy_file(
&self,
directory: &DirectoryPath,
signature: bool,
) -> Result<Option<Vec<u8>>, RepositoryError> {
let opened = match self.open_directory(&directory.0) {
Ok(opened) => opened,
Err(RepositoryError::NotFound { .. }) => return Ok(None),
Err(error) => return Err(error),
};
let name = if signature {
RECIPIENT_SIGNATURE_FILE
} else {
RECIPIENT_FILE
};
let path = directory.0.join(name);
let Some(metadata) = child_metadata(&opened, name, &path)? else {
return Ok(None);
};
require_regular_file(metadata, &path)?;
let mut file = opened
.open(name)
.map_err(|error| io_error("open recipient policy", &path, error))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)
.map_err(|error| io_error("read recipient policy", &path, error))?;
Ok(Some(bytes))
}
pub(crate) fn replace_policy_file(
&self,
directory: &DirectoryPath,
signature: bool,
contents: Option<&[u8]>,
) -> Result<(), RepositoryError> {
let name = if signature {
RECIPIENT_SIGNATURE_FILE
} else {
RECIPIENT_FILE
};
let path = directory.0.join(name);
let (opened, created) = if contents.is_some() {
self.create_directory_path(directory)?
} else {
match self.open_directory(&directory.0) {
Ok(opened) => (opened, Vec::new()),
Err(RepositoryError::NotFound { .. }) => return Ok(()),
Err(error) => return Err(error),
}
};
let result = (|| {
if let Some(contents) = contents {
if let Some(metadata) = child_metadata(&opened, name, &path)? {
require_regular_file(metadata, &path)?;
}
let mut temporary = TempFile::new(&opened)
.map_err(|error| io_error("create atomic recipient policy", &path, error))?;
set_private_permissions(&temporary, &path)?;
temporary
.write_all(contents)
.map_err(|error| io_error("write atomic recipient policy", &path, error))?;
temporary
.as_file()
.sync_all()
.map_err(|error| io_error("sync atomic recipient policy", &path, error))?;
temporary
.replace(name)
.map_err(|error| io_error("replace recipient policy", &path, error))?;
sync_directory(&opened, &directory.0)
.map_err(|_| RepositoryError::DurabilityUncertain { path: path.clone() })
} else {
let Some(metadata) = child_metadata(&opened, name, &path)? else {
return Ok(());
};
require_regular_file(metadata, &path)?;
opened
.remove_file(name)
.map_err(|error| io_error("remove recipient policy", &path, error))?;
sync_directory(&opened, &directory.0)
.map_err(|_| RepositoryError::DurabilityUncertain { path: path.clone() })
}
})();
match result {
Ok(()) => Ok(()),
Err(error) if error.committed() => Err(error),
Err(error) => self.rollback_created(created, error),
}
}
/// Atomically replace an encrypted entry and durably commit its containing directory.
pub fn write_entry(
&self,
@@ -604,6 +697,54 @@ impl Repository {
unreachable!("EntryPath has at least one component")
}
fn create_directory_path(
&self,
path: &DirectoryPath,
) -> Result<(Dir, Vec<PathBuf>), RepositoryError> {
let mut directory = self
.root
.try_clone()
.map_err(|error| io_error("clone repository root", Path::new("."), error))?;
let mut relative = PathBuf::new();
let mut created = Vec::new();
for component in path.0.components() {
let Component::Normal(name) = component else {
unreachable!("DirectoryPath is validated")
};
if let Err(error) = reject_entry_directory_collision(&directory, name, &relative) {
return self.rollback_created(created, error);
}
relative.push(name);
match child_metadata(&directory, name, &relative) {
Ok(Some(metadata)) => {
if let Err(error) = require_directory(metadata, &relative) {
return self.rollback_created(created, error);
}
}
Ok(None) => {
if let Err(error) = create_private_directory(&directory, name, &relative) {
return self.rollback_created(created, error);
}
created.push(relative.clone());
if let Err(error) = sync_directory(&directory, &relative) {
return self.rollback_created(created, error);
}
}
Err(error) => return self.rollback_created(created, error),
}
match directory.open_dir(name) {
Ok(opened) => directory = opened,
Err(error) => {
return self.rollback_created(
created,
io_error("open recipient policy directory", &relative, error),
);
}
}
}
Ok((directory, created))
}
fn rollback_created<T>(
&self,
mut created: Vec<PathBuf>,